In this post, and the next I discuss about the C++11 keyword ‘auto’.
A bit of background
The keyword ‘auto’ in C++ 2003 indicates storage duration.Storage duration is the property of an object that defines the minimum potential lifetime of the storage containing the object.
C++03, $3.7.2/1:
Local objects explicitly declared auto or register or not explicitly declared static or extern have automatic storage duration. The storage for these objects lasts until the block in which they are created exits.
In C++11, the keyword auto is no longer used to indicate automatic storage duration. As Bjarne says here,
The old meaning of auto (“this is a local variable”) is now illegal. Several committee members trawled through millions of lines of code finding only a handful of uses — and most of those were in test suites or appeared to be bugs.
Before, we delve into the details of the new meaning of the keyword ‘auto’, it is important to note that C++11 continues with the notion of automatic storage duration. It is just that ‘auto’ keyword is no longer used to describe it. In fact, there is no keyword in C++11 to specify automatic storage duration.
C++11, $3.7.3/1:
Block-scope variables explicitly declared register or not explicitly declared static or extern have automatic storage duration. The storage for these entities lasts until the block in which they are created exits.
The present
A Brief Introduction
Until now, the declaration of a variable requires it’s type to be specified explicitly at compile time. This is because C++ is a statically typed language where the type checking it performed at compile time.
int x = 7;
In the above declaration, it is required to specify the type of the variable ‘x’ so that the necessary type checking can be done by the compiler statically.
But is it really necessary to specify the type of the ‘x’ explicitly as ‘int’?
It turns out that compiler can automatically deduce the type of the variable ‘x’ based on the type of the expression used to initialize ‘x’. The expression used to initialize ‘x’ is the integer literal 7. In accordance with $2.14.2/1, the type of the integer literal 7 is ‘int’. Therefore, the compiler deduces the type of ‘x’ as ‘int’. This is where the ‘auto’ keyword comes into picture. In a way we tell the compiler to go and figure out the right type for the variable being declared based on the type of the initializer expression.
auto x = 7; // x is deduced as 'int' by the compiler.
At this point, we can make the following observations:
-The compiler can only deduce the type of a variable declared with the ‘auto’ specifier, only when it has an initializer.
-The automatic deduction of the variable type does not compromise type-safety of the language. In the above example, once type of ‘x’ is deduced as ‘int’, ‘x’ can not be used in a way which violates the rules of the language. As an example, ‘x’ can not be used to point to a string literal.
auto x = 7; // x is deduced as 'int' by the compiler
x = "ABCD"; // cannot convert from char[5] to int
Here are some more examples of how ‘auto’ type specifier can be used. Note that in contrast to the above examples declaring variable ‘x’ using the copy initialization syntax, the next example shows the versatility of the auto type specifier for declaring variables using the direct initialization syntax
auto y(255.0); // y deduced as ‘double’ by the compiler
auto z(y); // z deduced as ‘double’ by the compiler
The auto type specifier works with more advanced types as well
template bool f(T *p1, T *p2) {
bool bret = false;
// do whatever
return bret;
}
int main() {
bool (*p)(int *, int *) = f;
// C++11 only
auto handler = f;
}
An obvious question now that comes into mind is to know the type that the compiler deduces for the variable ‘handler’. There are many possibilities. One easy way to figure out the deduced type of ‘handler’ is to simply do something absurd with it. For example
handler = 2;
Now notice the diagnostic that the compiler issues
error C2440: ‘=’ : cannot convert from ‘int’ to ‘bool (__cdecl *)(T *,T *)’ 1> with
1> [
1> T=int 1> ]
1> Conversion from integral type to pointer type requires reinterpret_cast, C-style cast or function-style cast
There you go. The compiler clearly spits out the deduced type of ‘handler’ right on our face.
Another possibility is to use the ‘std::type_info::name’ to print out the internal name of the variable. However, the effectiveness of this approach is implementation dependent ($18.7.1/9)
At this point, the C++11 ‘auto’ keyword appears to have the benefit of encouraging less program verbosity. The programmer now has to do far less typing and can now instead comfortably rely on the implementation to go and figure out the right type. Indeed, the code snippet below shows some more examples of convenience that comes with the use of the ‘auto’ type specifier.
int main() {
auto *p1 = new auto(2); // p1 is deduced as int *
auto p2 = new auto(2); // and so is p2
std::vector v {1, 2, 3};
// no need to use vector::iterator it = v.begin()
for(auto it = v.begin(); it != v.end(); ++it) {
std::cout << *it << std::endl;
}
}
Note that there is no need to specify the exact type of the iterator it. With the use of the ‘auto’ keyword, we let the compiler figure it out for itself. Much less typing and type inference on the part of the programmer!
But is it really programmer convenience beneath the C++11 ‘auto’ keyword or is there something yet more powerful to it. Let’s look at an example
template T Addit(T const &t, U const &u) {
T temp = t;
temp = temp + u;
return temp;
}
int main() {
std::cout << Addit(2, 3) << std::endl; // 5
std::cout << Addit(2.2, 3) << std::endl; // 5.2;
std::cout << Addit(3, 2.2) << std::endl; // 5;
}
In the function template ‘Addit’, we return the result of adding variables of type T and U. The return value is of type ‘T’. As shown in the comments, the result of 2.2 + 3 is rightly 5.2 but the result of 3 + 2.2 is 5. This is because in the call to the function template Addit(3, 2,2), T is of type ‘int’ and U is of type ‘double’. The return value of this function call is of type ‘int’. Therefore, the statement ‘temp = temp + u’, drops precision and converts the double value 5.2 into integer value 5 which is eventually returned and printed.
Interchanging ‘T’ and ‘U’ is not a solution because in that case ‘Addit(2.2, 3)’ returns 5 instead of 5.2. The core reason of this issue is that C++ does not have a notion of determining the wider of any two given types ‘T’ and ‘U’. So, what is the solution?
One possible solution is to have the function template accept another template parameter ‘W’. At the site of instantiation, the corresponding template argument for ‘W’ is explicitly specified, since it can not be deduced by the compiler from the function call arguments.
template W Addit(T const &t, U const &u) {
W temp = t;
temp = temp + u;
return temp;
}
int main() {
std::cout << Addit(2, 3) << std::endl; // 5
std::cout << Addit(2.2, 3) << std::endl; // 5.2;
std::cout << Addit(3, 2.2) << std::endl; // 5;
}
Note, how, each of the instantiations specifies the template argument for the template parameter ‘W’ explicitly as ‘double’. So, now that we have solved the problem, but are it’s pros and cons?
The programmer now explicitly needs to specify the return type of ‘Addit’ during instantiation. This gives the programmer much finer control over the behavior of ‘Addit’. The drawback is that programmer now needs to decipher the appropriate return type mandatorily.
C++11 allows a very elegant solution when the ‘auto’ keyword is used in conjunction with the ‘decltype’ keyword (which is new in C++11) and the ‘trailing function return type’ feature (which is new in C++11)
template<typename T, typename U> auto Addit(T const &t, U const &u) -> decltype(t + u) {
auto temp = t + u;
return temp;
}
int main() {
std::cout << Addit(2, 3) << std::endl; // 5
std::cout << Addit(2.2, 3) << std::endl; // 5.2;
std::cout << Addit(3, 2.2) << std::endl; // 5;
}
Note that in the function template above, we let the compiler figure the type of the local variable ‘temp’ based on the initializer expression ‘t + u’. In our context, if either of them is a double, the result of the expression is the wider of the two which is ‘double’. So, in general, instead of the programmer specifying the return type, we let the compiler figure it out based on the return type of the operator + that is selected by overload resolution. Note, that the return type of the function template ‘Addit’ is now specified as ‘auto’ and the exact return type is specified by the type specified by the expression ‘decltype(t + u)’.
The above example is illustrative of the fact that in some situations it is just impossible or too laborious for the programmer to determine a type. It is in these circumstances that the power of the ‘auto’ keyword is unleashed to it’s fullest, much beyond as a mere convenience tool for the programmer.
Under the hood
Before, we see some more examples of ‘auto’, it is important to understand how the ‘auto’ keyword really works. Since the ‘auto’ keyword is involved with type deduction, it is not hard to guess that the rules governing ‘auto’ type deduction are based on template argument deduction rules. $7.1.6.4 specifies the details of the C++11 ‘auto’ keyword.
$7.1.6.4/6:
Once the type of a declarator-id has been determined according to 8.3, the type of the declared variable using the declarator-id is determined from the type of its initializer using the rules for template argument deduction. Let T be the type that has been determined for a variable identifier d. Obtain P from T by replacing the occurrences of auto with either a new invented type template parameter U or, if the initializer is a braced-init-list (8.5.4), with std::initializer_list<U>. The type deduced for the variable d is then the deduced A determined using the rules of template argument deduction from a function call (14.8.2.1), where P is a function template parameter type and the initializer for d is the corresponding argument. If the deduction fails, the declaration is ill-formed.
Here are some of the important statements from $14.8.2.1 that are reproduced here for easy reference.
$14.8.2.1/1:
Template argument deduction is done by comparing each function template parameter type (call it P) with the type of the corresponding argument of the call (call it A) as described below.
$14.8.2.1/2:
If P is not a reference type:
— If A is an array type, the pointer type produced by the array-to-pointer standard conversion (4.2) is used in place of A for type deduction; otherwise,
— If A is a function type, the pointer type produced by the function-to-pointer standard conversion (4.3) is used in place of A for type deduction; otherwise,
— If A is a cv-qualified type, the top level cv-qualifiers of A’s type are ignored for type deduction.
$14.8.2.1/3:
If P is a cv-qualified type, the top level cv-qualifiers of P’s type are ignored for type deduction. If P is a reference type, the type referred to by P is used for type deduction. If P is an rvalue reference to a cvunqualified template parameter and the argument is an lvalue, the type “lvalue reference to A” is used in place of A for type deduction.
Let us now see how these rules kick in to the type deduction process with the ‘auto’ keyword with some examples.
Given the declarations
int x, &lvrx = x, *px = &x;
int const ci = 2, &lvrci = ci, *pci = &ci;
Consider the declarations
- auto const a2 = x;
In accordance with $7.1.6.4, the compiler considers a function template like so:F = template<typename T> void f(T const t);The compiler tries to instantiate the invented function template ‘f’ with the function callf(x);Now the rules for deducing function template arguments kicks in in accordance with $14.8.2.1P = T const, which is the type of the function parameter ‘t’A = int, which is the type of the initializer expression ‘x’In accordance with $14.8.2.1/3, the top level cv qualifiers of P are ignoredTherefore P = T, which is the type of the function parameter ‘t’
Therefore Deduced A = int
Therefore the type of ‘a2’ is ‘int const’.
Deducing A as int successfully instantiates the function template ‘f’ for the function call ‘f(x)’, and the declaration is well-formed.
- auto &a3 = 2;
In accordance with $7.1.6.4, the compiler considers a function template like so:F = template<typename T> void f(T &t);The compiler tries to instantiate the invented function template ‘f’ with the function callf(2);Now the rules for deducing function template arguments kicks in in accordance with $14.8.2.1. “If P is a reference type, the type referred to by P is used for type deduction.“P = T &, which is the type of the function parameter ‘t’A = int, which is the type of the initializer expression ‘2’Therefore Deduced A = int
Therefore, ‘a3’ is a reference to an ‘int’
Deducing A as int fails to instantiate the function template ‘f’ for the function call ‘f(2)’. This is because, we try to bind a non-const lvalue reference to an lvalue. Hence, the declaration is ill-formed.
- auto &a4 = lvrx;
In accordance with $7.1.6.4, the compiler considers a function template like so:F = template<typename T> void f(T &t);The compiler tries to instantiate the invented function template ‘f’ with the function callf(lvrx);Now, the question is ‘What is the type of the initializer expression lvrx?’$5/5:
If an expression initially has the type “reference to T” (8.3.2, 8.5.3), the type is adjusted to T prior to any further analysis. The expression designates the object or function denoted by the reference, and the expression is an lvalue or an xvalue, depending on the expression.This means that the type of the initializer expression is ‘int’.So, now, we have:
P = T &, which is the type of the function parameter ‘t’
A = int, which is the type of the initializer expression ‘lvrx’
Therefore Deduced A = int
Therefore, ‘a4’ is a reference to an ‘int’
Deducing A as int successfully instantiates the function template ‘f’ for the function call ‘f(lvrx)’. This is because; it is perfectly fine to bind a non-const lvalue reference to an lvalue. Hence, the declaration is well-formed.
- auto &&rvr1 = x;
This is an interesting case which involves auto deduction of rvalue references. In accordance with $7.1.6.4, the compiler considers a function template like so:F = template<typename T> void f(T &&t);The compiler tries to instantiate the invented function template ‘f’ with the function callf(x);Now the rules for deducing function template arguments kicks in in accordance with $14.8.2.1.If P is a reference type, the type referred to by P is used for type deduction.P = T &&, which is the type of the function parameter ‘t’A = int, which is the type of the initializer expression ‘x’
Therefore Deduced A = intTherefore, ‘rvr1’ is a rvalue reference to an ‘int’.
But, hold on! . $14.8.2.1/3 further states that if P is an rvalue reference to a cvunqualified template parameter and the argument is an lvalue, the type “lvalue reference to A” is used in place of A for type deduction. So, our analysis above is not correct. Let’s try again in the light of the special deduction rule for rvalue reference to template parameter
P = T &&, which is the type of the function parameter ‘t’
A = int&, which is the type of the initializer expression ‘x’. Note it is no longer treated as ‘int’
Therefore Deduced A = int&
Therefore ‘rvr1’ is of type ‘int & &&’. Hmm, but what is this type? At a first glance it looks like an rvalue reference to ‘int &’.
$8.3.2/5:
There shall be no references to references, no arrays of references, and no pointers to references.
In light of the above, C++ does not allow reference to reference. So what is ‘int & &&’?
$8.3.2/6 has the answer:
If a typedef (7.1.3), a type template-parameter (14.3.1), or a decltype-specifier (7.1.6.2) denotes a type TR that is a reference to a type T, an attempt to create the type “lvalue reference to cv TR” creates the type “lvalue reference to T”, while an attempt to create the type “rvalue reference to cv TR” creates the type TR.
In our case, we are indeed dealing with a template type parameter T who’s type is deduced as ‘int &’. In our case, ‘T’ is ‘int &’ and ‘TR’ is same as ‘T’. In accordance with the above, an attempt to create the type rvalue reference to cv ‘int &’ (i.e. trying to create int & &&), creates the type ‘TR’ which is nothing but ‘int &’.
This sounds confusing and can be summarized neatly as shown below:
For a given type X:
• X& &, X& &&, and X&& & all collapse to type X&
• The type X&& && collapses to X&&
This is known as the reference collapsing rule.
Therefore, in light of the above discussion, ‘rvr1’ is an ‘lvalue reference to int’ and not an ‘rvalue reference to int’
Deducing A as ‘int &’ successfully instantiates the function template ‘f’ for the function call ‘f(x)’. This is because, it is perfectly fine to bind a non-const lvalue reference to an lvalue. Hence, the declaration is well-formed.
- auto &&rvr1 = 2;
In accordance with $7.1.6.4, the compiler considers a function template like so:F = template<typename T> void f(T &&t);The compiler tries to instantiate the invented function template ‘f’ with the function callf(2);Now the rules for deducing function template arguments kicks in in accordance with $14.8.2.1. “If P is a reference type, the type referred to by P is used for type deduction.“P = T &&, which is the type of the function parameter ‘t’A = int, which is the type of the initializer expression ‘2’Therefore Deduced A = intTherefore, ‘a3’ is a rvalue reference to an ‘int’. Deducing A as int successfully instantiates the function template ‘f’ for the function call ‘f(2)’. This is because; it is perfectly fine to bind a rvalue reference to an rvalue. Hence, the declaration is well-formed.
We will now consider one last example illustrating the template argument deduction process with the C++11 ‘auto’ keyword
- auto const &&rvr2 = ci; // ill-formed
This is once again an interesting case which involves auto deduction of rvalue references.In accordance with $7.1.6.4, the compiler considers a function template like so:F = template<typename T> void f(T const &&t);The compiler tries to instantiate the invented function template ‘f’ with the function callf(ci);Now the rules for deducing function template arguments kicks in in accordance with $14.8.2.1. If P is a reference type, the type referred to by P is used for type deduction.P = T const &&, which is the type of the function parameter ‘t’A = int const, which is the type of the initializer expression ‘ci’It is important to note that $14.8.2.1/3 applies here. But ‘P’ is not a cv qualified type. Also note that the ‘P’ is a rvalue reference to cvqualified (and not cvunqualified) template parameter even though the argument is an lvalue. Therefore the following does NOT apply “If P is an rvalue reference to a cvunqualified template parameter and the argument is an lvalue, the type “lvalue reference to A” is used in place of A for type deduction.”Therefore, the deduction process is governed by the following statement “If P is a reference type, the type referred to by P is used for type deduction.”
Therefore Deduced A = int
Therefore ‘rrvr2’ is of type ‘rvalue reference to int’.
Deducing A as int fails to instantiate the function template ‘f’ for the function call ‘f(ci)’. This is because, we can not bind a rvalue reference to an lvalue, even if it is a const lvalue. Hence, the declaration is ill-formed.
Before, we close, I would like to highlight that you can figure out the inner workings of the compiler and deduction process by explicitly making a call to the invented function template and instantiating it with the initializer expression. This may require appropriate compiler flags to be set. This is once again implementation specific behavior and your mileage may wary.
Let’s illustrate this with an example
template<class T> void f(T &&t) {
g(t);
}
int main() {
auto &&a3 = 2;
f(2); // explicit call to function template ‘f’
}
The function call ‘g(t)’ is intentionally added to generate compiler diagnostics at compile time to see the deduced value of ‘T’. Here are the (partial) compiler diagnostics in VS2010 and g++ compiler respectively
From VS2010
see reference to function template instantiation ‘void f<int>(T &&)’ being compiled
1> with
1> [
1> T=int
1> ]
From g++
/home/saxena/TestCode/TestCpp11/main.cpp||In function ‘int main()’:|
/home/saxena/TestCode/TestCpp11/main.cpp|6|warning: unused variable ‘a3′ [-Wunused-variable]|
/home/saxena/TestCode/TestCpp11/main.cpp||In function ‘void f(T&&) [with T = int]‘:|
/home/saxena/TestCode/TestCpp11/main.cpp:7|8|instantiated from here|
/home/saxena/TestCode/TestCpp11/main.cpp|2|error: ‘g’ was not declared in this scope|
||=== Build finished: 1 errors, 1 warnings ===|
In the next post, we will look at some other finer points of the C++11 ‘auto’ type specifer, including some of the other legitimate uses along with the disallowed usages.
I will be glad to hear from you. Please do share your thoughts and feedback.