If you add a short int
and a char
in C++, what is the resulting type? What if you subtract a long int
from an unsigned int
? The answers actually depend on the compiler and the target architecture (int
or unsigned
in the first case and long int
or unsigned long int
in the second). This article lists the rules from the current C++ standard and gives an example of how the type can be resolved at compile time using templates.
Introduction
Let me first note that I will be referring to the current C++ standard from 1998 (with a minor revision in 2003). This standard is described in The C++ Programming Language by Bjarne Stroustrup. It can also be found online.
In C++, the integer types are short int
, int
, long int
and the unsigned versions of these. The integer types together with the boolean type (bool
) and the character types (plain/signed
/unsigned
char
and wchar_t
) are called integral types. The integral types together with the floating-point types (float
, double
, and long double
) are called arithmetic types.
Resolving the Return Type
Consider having the function fct
overloaded with one function for each arithmetic type, for example:
void fct(short v) { std::cout << "short " << v << std::endl; }
void fct(unsigned short v) { std::cout << "unsigned short " << v << std::endl; }
void fct(int v) { std::cout << "int " << v << std::endl; }
void fct(unsigned v) { std::cout << "unsigned " << v << std::endl; }
...
and so on. If you now have the following little routine:
void g(short int a, char b) {
fct(a + b);
}
which instance of fct
is called? This is another way of asking the question that started this article.
When a binary operator (+
, -
, *
, /
, %
) is applied to operands with arithmetic types, the C++ compiler must do the following:
- Integral promotion. Each operand is, if necessary, promoted to at least an
int
(to be made more precise below). - Usual arithmetic conversions. Based on the (possibly promoted) types of the operands, a common type is found. Both operands are converted to this type, which will also be the resulting type.
Integral Promotions
Integral promotions are defined in Section 4.5, page 4-3, from the online standard and in Section C.6.1, page 833, from The C++ Programming Language. If we ignore enumerations and bit-fields, they can be summed up as follows:
- A
char
,signed char
,signed char
,short int
,unsigned short int
is converted toint
ifint
can represent all the values of the source type; otherwise, it is converted to anunsigned int
. wchar_t
is converted to the first of the following types that can represent all the values ofwchar_t
:int
,unsigned int
,long
,unsigned long
.bool
is converted toint
.
Usual Arithmetic Conversions
The rules for usual arithmetic conversions can be found in Section 5, page 5-2, from the online standard and in Section C.6.3, page 836, from The C++ Programming Language. Assuming integral promotions have been performed (if needed), the usual arithmetic conversions are in essense the following:
- If one operand is a
long int
and the otherunsigned int
, then if along int
can represent all the values of anunsigned int
, theunsigned int
shall be converted to along int
; otherwise both operands shall be converted tounsigned long int
. - Otherwise, find the highest ranking type among the operands and convert the other operand to this type. The relevant types listed from high to low rank are:
long double
,double
,float
,unsigned long
,long
,unsigned
,int
.
Using Templates To Get The Type
Those were the rules in written form. Imagine now that we have, e.g., the following routine:
template <typename A, typename B>
[some-type] add(const A& a, const B& b) { return a + b; }
We would like the types of add(a, b)
and a + b
to be identical when both a
and b
are arithmetic types.
First, promotions. By default, a type is not promoted:
template <typename T>
struct promote { typedef T type; };
We then use template specializations for the types that need to be promoted. Some of these are easy:
template <>
struct promote<signed short> { typedef int type; };
template <>
struct promote<bool> { typedef int type; };
For the rest, we need a sort of if-then-else for choosing a type:
template <bool C, typename T, typename F>
struct choose_type { typedef F type; };
template <typename T, typename F>
struct choose_type<true, T, F> { typedef T type; };
So the boolean value of the first argument determines whether to choose the type T
(if true) or F
(if false). We now have:
template <>
struct promote<unsigned short> {
typedef choose_type<sizeof(short) < sizeof(int), int, unsigned>::type type;
};
template <>
struct promote<signed char> {
typedef choose_type<sizeof(char) <= sizeof(int), int, unsigned>::type type;
};
template <>
struct promote<unsigned char> {
typedef choose_type<sizeof(char) < sizeof(int), int, unsigned>::type type;
};
template <>
struct promote<char>
: public promote<choose_type<std::numeric_limits<char>::is_signed,
signed char, unsigned char>::type> {};
This last one for plain char
is needed because C++ considers char
, signed char
, and unsigned char
to be three distinct types. The standard does not specify whether char
is signed or not. (The numeric_limits
template is defined in the limits
header.)
Finally, to promote wchar_t
:
template <>
struct promote<wchar_t> {
typedef choose_type<
std::numeric_limits<wchar_t>::is_signed,
choose_type<sizeof(wchar_t) <= sizeof(int), int, long>::type,
choose_type<sizeof(wchar_t) <= sizeof(int), unsigned, unsigned long>::type
>::type type;
};
We can now turn to the usual arithmetic conversions. First, we promote each type, if necessary:
template <typename A, typename B>
struct resolve_uac : public resolve_uac2<typename promote<A>::type,
typename promote<B>::type> {};
This ensures that the type arguments for resolve_uac2
are at least int
s. We then introduce ranks for those types:
template <typename T> struct type_rank;
template <> struct type_rank<int> { static const int rank = 1; };
template <> struct type_rank<unsigned> { static const int rank = 2; };
template <> struct type_rank<long> { static const int rank = 3; };
template <> struct type_rank<unsigned long> { static const int rank = 4; };
template <> struct type_rank<float> { static const int rank = 5; };
template <> struct type_rank<double> { static const int rank = 6; };
template <> struct type_rank<long double> { static const int rank = 7; };
Now we can pick the type with the highest rank:
template <typename A, typename B>
struct resolve_uac2 {
typedef typename choose_type<
type_rank<A>::rank >= type_rank<B>::rank, A, B
>::type return_type;
};
Finally we need to deal with the special case where one type is long int
and the other is unsigned int
:
template <>
struct resolve_uac2<long, unsigned> {
typedef choose_type<sizeof(long) == sizeof(unsigned),
unsigned long, long>::type return_type;
};
template <>
struct resolve_uac2<unsigned, long> : public resolve_uac2<long, unsigned> {};
We can now write the add
routine from earlier as:
template <typename A, typename B>
typename resolve_uac<A, B>::return_type add(const A& a, const B& b)
{ return a + b; }
and the return type will match that of the +
operation. Note that the arguments to add
have to be arithmetic types (because of the substitution-failure-is-not-an-error principle).
Remarks
The rules and implementation above should be complete with the exception of enumerations and bit-fields, see the links to the standard for the missing pieces. Note also that the rules for promotions and usual arithmetic conversions will change (slightly) in the upcoming C++0x standard, see the C++0x draft, Section 5, page 84.