A Magic Spell: Using auto and decltype in C++11 to Declare Objects Without Spelling Out Their Types
One of the principles of the C++ design philosophy is that whenever there's a choice between inconveniencing the programmer and inconveniencing the compiler, the latter should be preferred. Two new C++11 features, auto and decltype, take this design philosophy one step further, letting you omit the type of an object from its declaration. As if by magic, the compiler deduces the missing type by examining the initializer used in the declaration. A similar mechanism for capturing the type of an expression is provided by a new C++11 operator called decltype. Together, auto and decltype eliminate unnecessary keystrokes and simplify your code without compromising type-safety, code readability, and performance. In the following sections, I'll show how to use these new C++ features.
Automating Type Deduction
To estimate the merits of auto, consider the following C++03 declarations:
int page_no=0; double coeff=3.6666667; const static char error_message[]="unit malfunction"; bool dirty=false;
These four declarations have two things in common:
- They include an explicit initializer.
- They spell out the object's type explicitly.
C++ users noticed a long time ago that spelling out the type in such declarations is redundant, because the initializers already convey the type implicitly. For example, the literal 0 is int, 3.6666667 is double, and false can only be a value of type bool. Furthermore, in some cases, even C lets you omit certain type properties (such as the size of an array) when the compiler is able to retrieve those properties from the initializer, as shown in the declaration of error_message above. If the compiler can retrieve the type information from the initializers, why not free the programmer from this burden completely?
At first, it was believed that automatic type deduction was just a minor convenience. However, contemporary C++ apps make extensive use of Standard Library components such as containers and iterators. In such apps, forcing the programmer to spell out the type of an iterator in every for loop, for example, isn't just a nuisance—it compromises code readability. Consider:
for (std::vector<Widget>::iterator iter = vw.begin(); iter<vw.end(); iter++) { iter->print_name(); }
Omitting the type from the declaration of iter improves the for loop's readability:
for (auto iter = vw.begin(); iter<vw.end(); iter++) { iter->print_name(); }
auto in Action
The new C++11 keyword auto lets you omit the type of an object being declared, so long as the declaration includes an initializer.
Using auto, you can rewrite the four previous declarations like this:
auto page_no=0; auto coeff=3.6666667; auto error_message="unit malfunction"; //notice that the [] were omitted too! auto dirty=false; auto str=std::string("hello");
Let's examine each of these declarations more carefully to understand how the compiler deduces the correct type of each object:
- page_no is easy—the literal 0, which is polymorphic in C++ (it could mean a null pointer, among the rest), is taken to be int by default.
- coeff is similar: A literal number with a decimal point is taken to be a double.
- error_message is a more interesting case. Even without brackets, the compiler knows that this is a char array because text enclosed in double quotes (″″) implies a static const char array. The compiler counts how many characters are enclosed between the pair of double quotes (including a null terminator), and it uses that information to assign the size of the array.
In a similar vein, an auto declaration of an int array looks like this:
auto a={5,8,10};
CV Qualification and Arrays
All well and good so far. However, the former declarations didn't include const or volatile qualifiers (cv qualifiers). What if you want to declare a cv-qualified object using auto? Generally speaking, initializers cannot encode that information. The compiler can't tell whether 3.6666667 is of type double, const double, const volatile double, or volatile double. By default, literals (except quoted strings) are assumed not to be cv-qualified. Therefore, if you want to declare a cv-qualified object using auto, you should state the cv-qualification explicitly in the declaration:
const auto PAGE_SIZE=512; //const int auto const volatile coeff=3.6666667; //const volatile double const volatile auto coeffarr={3.6666667, 2.65, 1.3333}; //const volatile double[]
My compiler, which isn't fully-conformant with the C++11 standard yet, chokes on that last declaration. Expect similar grunts and moans from your compiler in the near future until vendors have fully implemented the new features of C++11.
On that note, two tricks could help here. First, replace the ={} notation for arrays with the new C++11 style brace-enclosed initializer list, which doesn't have the equal (=) sign. In other words, try to rewrite the last declaration like this:
const volatile auto coeff_arr {3.6666667, 2.65, 1.3333}; //C++11 brace-init notation, no=
Another trick that could help in some cases is #including the new C++11 header file <initializer_list> in your program:
auto c='A'; //char #include <initializer_list> //needed for brace-enclosed initializer lists auto a={5,8,10}; //int[3]
According to the C++11 standard, the compiler must #include <initializer_list> implicitly in every translation unit in which that header is required, so essentially you're not supposed to write any #include <initializer_list> directives in your code. (Initializer lists will be the topic of a separate article.) However, this requirement was added to the C++11 standard recently. Therefore, some implementations don't follow the rules yet.
Derived Types
As far as int, char, double, and other fundamental types are concerned, auto declarations seem straightforward. However, what about more complex types such as pointers and pointers to functions? Remember this rule: So long as the initializer itself is an object of the desired type, you can use it in an auto declaration to declare another object with the same type. Put differently, you're not confined to using literals as initializers. The following example uses an existing pointer to Widget to auto-declare another pointer to Widget, and an array thereof:
Widget* pw= new Widget; auto pw2=pw; //Widget* auto pwarr {pw2, new Widget, pw}; //array of three Widget*
Following the same pattern, here's an auto declaration of a pointer to a function that takes bool and returns int:
int func(bool); auto fptr=func; //OK, type is int(*)(bool)
Finally, if you want to declare an array of pointers to functions of this type, use the following initializer:
auto fptr{func, func}; //OK, type is int(*[2])(bool)
Capturing the Type of an Expression Using decltype
The new C++11 operator decltype is similar to auto in the sense that it deduces the type of an expression without any further assistance from the programmer. You can then use the "captured" type to declare another object with the same type, create a typedef, or have the compiler calculate a function's return type for you.
Suppose you want to capture the iterator type that a vector::begin() call returns. Use decltype like this:
vector<Widget> vw; typedef decltype(vw.begin()) MYIT;
MYIT is a typedef name synonymous with the return type of vw.begin(). decltype is chiefly useful in generic programming for capturing the types of complex declarations, particularly nested templates. In a way, decltype complements auto by letting you "uncover" the type of an object that was declared using auto. Consider:
auto iarr {0,1}; //int[2] //uncover the type of iarr and use it to declare a new typedef typedef decltype(iarr) intarray; intarray x; x[0]=3; x[1]=2;
Any valid C++ expression makes a perfect argument for decltype. Here are a few examples:
typedef decltype(5) MYINT; //using a literal float x; typedef decltype(x) MYFLOAT; //using a variable typedef decltype (std::string()) MYSTR; //using a temporary typedef decltype(2.5*0.666) MYDOUBLE; //math expressions will also work
You can even use a comma-separated expression as a decltype argument. Recall that the type of a comma-separated expression is always the type of the rightmost sub-expression:
typedef decltype (5, 'a') MYCHAR; MYCHAR greeting[]="hello";
New Function Declaration Syntax
C++11 also introduced a new notation for function declarations that takes advantage of auto and decltype. Here's an example of a new-style function declaration:
auto func(int x)-> double; //C++11 style function declaration
func()'s return type is double. Generally speaking, the return type of auto functions is called a trailing return type. A trailing return type is preceded by an arrow sign (->)after the parameter list. The arrow sign is made up of two characters: a hyphen (-) and a right angle bracket (>). You probably recognize the -> sign—that's the same arrow sign you use for accessing a member through a pointer to an object, as in the following example:
pobj->func();
Combining auto and decltype enables the compiler to generate a function's return type automatically from an expression or a value. For example, you can use a parameter's name to derive the function's return type:
auto g(double d)-> decltype(d); //return type is double
Similarly, template functions can generate their return types automatically, like this:
template <class T> auto get_end(vector<T>& v) {return v.end()) ->decltype(v.end()); //return type is vector<T>::iterator vector<Widget> vw; get_end(vw); //returns vector<Widget>::iterator template <class T> auto get_end(const vector<T>& v) {return v.end()) ->decltype(v.end()); const vector<Widget> cvw; get_end(cvw); //returns vector<Widget>::const_iterator
In Conclusion
C++ programmers migrating to C++11 often say that auto and decltype are the features they like most. And yet, auto and decltype aren't just a matter of convenience; they prove useful in the design of generic code, where the automatic deduction of a return type, iterator, and so on in accordance with every template specialization is essential. It's no wonder that many other programming languages have started to imitate the C++11 auto.
Danny Kalev is a certified system analyst and software engineer specializing in C, C++ Objective-C and other programming languages. He was a member of the C++ standards committee until 2000 and has since been involved informally in the C++ standardization process. Danny is the author of ANSI/ISO Professional Programmer's Handbook (1999) and The Informit C++ Reference Guide: Techniques, Insight, and Practical Advice on C++ (2005). He was also the Informit C++ Reference Guide. Danny earned an M.A. in linguistics, graduating summa cum laude, and is now pursuing his Ph.D. in applied linguistics.