CodeProject
Guidelines about the auto keyword of C++11 for variable declaration.
The guidelines around the usage of the auto
keyword in C++11 is controversial. One argument is that “auto hides the type”. In practice, I found that this is not a concern, unless you use “notepad.exe” as your IDE. This post lists a collection of examples and guidelines for local variable declaration and initialization with auto.
Initialization in C++ is a mess.
int i;
float f(3.3);
double d = 4.4;
char c = i + 2;
class A {
A() : s(), i() {}
string s;
int i;
};
string t();
All the above variables are declared and initialized using different syntax and semantics.
For example, variable i is Default Initialized. In case of int
, it is effectively not initialized at all.
Variable f
is initialized with 3.3
but it looks more like a function call.
The A::s
and A::i
variables are Value Initialized, A::i
with 0
and A::s
with the default constructor of string
.
On the other hand, the last one is not a variable definition but a function declaration. The t
is a function that takes no arguments and returns a string
.
This is anything but consistent. If I was new in C++ land, I would consider this confusing and I may prematurely turn away from learning C++ in favor of some other language. I think it is a pity.
Fortunately with C++11, the language gained a new way of declaring variables. The same code looks like this when using the auto
keyword and Braced Initialization:
auto i = 1;
auto f = 3.3f;
auto d = 4.4;
auto c = char{i + 2};
class A {
A() : s{}, i{} {}
string s;
int i;
};
This looks much more consistent and also safer (see the guidelines at the end of this post).
Here is a more elaborate example about the basic usage:
#include <iostream>
#include <string>
#include <typeinfo>
#include <vector>
#include <memory>
using namespace std;
template<typename T>
void print(const T& a) { cout << typeid(T).name() << "=(" << a.i << "," << a.d << "," << a.s << ") " << endl; }
struct A {
int i; double d; string s;
};
struct B {
explicit B(int ai, string as) : i{ai}, s{as} {}
virtual ~B() {}
int i; double d; string s;
};
struct C : public B {
explicit C() : B{1, "C"} {}
};
int main() { auto i1 = 11; auto i2 = 12u; auto i3 = 13.0f; auto i4 = 13.0; auto i5 = "hello"; auto i6 = string{"hello"};
auto v1 = vector<int>{}; auto v2 = vector<int>{1, 2, 3, 4};
auto p1 = unique_ptr<B>{new C{}}; auto p2 = shared_ptr<B>{new C{}}; auto p3 = new A{}; delete p3;
auto p4 = static_cast<B*>(new C{}); delete p4;
auto a1 = A{}; auto a2 = A{11, 3.3, "A"};
auto b2 = B{22, "B"};
print(a1); print(a2);
print(b2);
return 0;
}
Here is the output of the above program:
1A=(0,0,)
1A=(11,3.3,A)
1B=(22,-0.553252,B)
There are some situations when determining the resulting type of the declaration may look not so obvious, but it all makes sense and remembering the following helps you out:
- “auto a = expression;” in general:
- Creates a new object called
a
. - It has the same type as expression.
- Initialized with the value of expression.
- “auto a = expression;”
a
is a new object. The value of expression is copied into a
. So, the CV qualifier of the expression is dropped.
- “auto& a = referencable_expression;” and
- “auto* a = pointer_expression;” same as
“auto a = pointer_expression;”
- This creates a new reference (a reference or a pointer) to the object returned by the expression. Because it is a reference, the CV qualifier of the
expression is “inherited” to a
. For example, this will create a const
reference if the expression itself is const
. Makes sense!
Here is an example of some “not so obvious” declarations denoting the source type and the resulting type. Pay attention that the first column is the resulting type and the second column is the source type (the “a <- b" notation indicates that we get type a
from type b
).
#include <iostream>
#include <typeinfo>
using namespace std;
int main() {
auto i = int{3};
auto v1 = i; const auto v2 = i; auto& v3 = i; const auto& v4 = i; auto v5 = static_cast<const int>(i); auto v6 = static_cast<const int&>(v3);
auto& t4 = v4; const auto& t5 = v4; auto& t6 = v3;
auto w1 = v3; auto w2 = v4;
auto p1 = &i; auto p2 = p1; const auto p3 = p1; auto& p4 = p1;
auto q1 = static_cast<const int*>(&i); auto q2 = q1; auto q3 = *q1;
auto* r1 = static_cast<const int*>(&i); auto* r2 = r1; auto* r3 = *r1;
auto z1 = {1, 2, 3}; }
One other important rule to remember is that the Initializer-list Constructor wins over normal constructors. In other words, when in doubt, the compiler picks the Initializer-list Constructor. Here is an example that demonstrates this:
#include <iostream>
#include <string>
#include <typeinfo>
#include <initializer_list>
#include <vector>
using namespace std;
template<typename T>
void print(const T& a) {
cout << typeid(T).name() << "=(" << a.i << "," << a.d << "," << a.s << ",[";
for (const auto i : a.v) { cout << i << " "; }
cout << "])" << endl;
}
struct D {
explicit D() {}
explicit D(initializer_list<int> il) : v{il} {}
int i; double d; string s; vector<int> v;
};
struct C {
explicit C(int ai, string as) : i{ai}, s{as} {}
explicit C(int ai1, int ai2) : i{ai1 + ai2} {}
explicit C(initializer_list<int> il) : v{il} {}
int i; double d; string s; vector<int> v;
};
int main() {
auto v1 = vector<int>{}; auto v2 = vector<int>{1, 5}; auto v3 = vector<int>(1, 5);
auto d1 = D{}; auto d2 = D{1, 2, 3, 4 ,5, 6};
auto c1 = C{}; auto c3 = C{10, 20}; auto c4 = C{33, "C"}; auto c5 = C{1, 2, 3, 4, 5, 6};
print(d1); print(d2);
print(c1); print(c3); print(c4); print(c5);
return 0;
}
Here is the output of this example program:
1D=(-1217180992,3.64111e-314,,[])
1D=(-1217352208,-5.02222e-42,,[1 2 3 4 5 6 ])
1C=(-1218706460,-0.318174,,[])
1C=(-1220249883,4.88997e-270,,[10 20 ])
1C=(33,4.85918e-270,C,[])
1C=(-1217346088,4.8697e-270,,[1 2 3 4 5 6 ])
Guidelines
- Almost Always Auto: Use auto for local variable declaration whenever you can.
- Safety: Discourages uninitialized variables (Default Initialization).
- Safety: Discourages using naked pointers (the polymorphic case).
- Correctness: No automatic narrowing! You get the exact type.
- Convenience: With auto, the compiler “figures out” the type for you.
- Convenience: Use standard literal suffixes and prefixes for less typing.
- Integer suffixes: u (unsigned int), l (long int), ul (unsigned long int).
- Floating point suffixes: L (long double), f (float).
- Character prefixes: L’’ (wchar_t), L”” (const wchar_t[]).
- Almost Always Brace Initializers: Use brace initializers whenever you can.
- Consistency: Provides one syntax for initialization.
- Initialization != Function call != Type cast
- “auto w = Widget(12)”: This looks like a function call, but it’s not. It is a type cast of int to Widget (Explicit Type Conversion).
- “auto w = Widget{12}”: This looks like a brace initializer and it is a brace initializer.
- Almost: The compiler will tell you when you cannot use auto.
References
END OF POST