Using C++11 auto
and decltype
I am sure by now you’ve heard of C++11 type deduction from initializer (aka auto
). Probably also seen a few examples or, maybe, even wrote some code that uses it. There is also another similar, yet different, new feature in C++11: an operator that returns the declaration type of an expression (aka decltype
). In this post I am going to discuss a few thoughts and guidelines on using these new features in C++ applications.
But first let’s quickly recap what auto
and decltype
are all about. C++11 auto
instructs the compiler to automatically deduce the type of a variable from its initializer. Here is an example:
auto x = f ();
When I first saw a code fragment like this, two thoughts immediately crossed my mind: I now have no idea what the type of x
is and This is a code readability and maintenance nightmare waiting to happen. But as I started learning more about auto
’s semantics, I began to realize that perhaps my scepticism was not justified. To see why, let’s consider what type we get for a few different declarations of f()
. First, if f()
returns by value, things are pretty straightforward:
int f (); auto x = f (); // x is of type int
Things get more interesting when f()
returns a reference. Does x
also become a reference or does it remain a value? The answer is, it remains a value:
int& f (); auto x = f (); // x is of type int, not int&
This is probably the most important point to keep in mind when using auto
: the type that it “substituted” for auto
always has its top-level reference removed. When I understood this point, again, my first thought was: This is bizarre. Now, besides not knowing what we get, we also don’t get exactly the same type. My mind promptly envisioned all these cases where a function returns by reference but an unnecessary copy is made because auto
stripped the reference. But, again, as I had more time to think about it, I realized that my fears are probably ungrounded. To understand why, think about the type of the local variable (x
in our example) as having two parts: the core type (int
in our case) and its const-ness/reference-ness. The core type can be naturally determined from the initializer expression. However, const/reference-ness is really determined by what we plan to do with the object further down within our code. Are we just accessing it? Then our variable should probably be a const
reference. Are we planning to modify it? If so, then do we want to modify a shared object or our own copy? If it is shared, then our variable should be a reference. Otherwise, it should be a value. Here are the signatures for each case:
const auto& x = f (); // x is not modified auto& x = f (); // x is modified, shared auto x = f (); // x is modified, private
In a sense, by choosing to strip the top-level reference, auto
forces us to specify our intentions. Plus, if we use the above signatures for each use-case, we get an additional safety net in case the type of an initializer changes. For example, if we are expecting to modify a shared reference and the signature of f()
changes to return, say, by-value instead of by-reference, we will get a compile error.
If you have to stop reading right now and need a single takeaway from this post, then it will be this: whenever you find yourself writing auto x
, stop and ask if you plan to modify x
? If the answer is No, then change that to const auto& x
.
Now that we understand auto
, it is easy to define decltype
. This operator evaluates to the exact declaration type of an expression, including references and all. Here is an example that contrasts the two:
int f1 (); int& f2 (); const int& f3 (); auto a1 = f1 (); // a1 is int auto a2 = f2 (); // a1 is int auto a3 = f3 (); // a1 is int decltype (f1 ()) d1 = f1 (); // d1 is int decltype (f2 ()) d2 = f2 (); // d2 is int& decltype (f3 ()) d3 = f3 (); // d3 is const int&
You may have noticed that the top-level const/reference stripping semantics of auto
mimics that of automatic template argument deduction. In fact, in the standard, auto
is defined in terms of template argument deduction. By now many people have developed a pretty good intuition about what the deduced template argument will be. We can easily extend this intuition to auto
by mentally re-writing a statement like this:
const int& f (); auto& x = f (); // auto -> const int, x is const int&
To something like this
template <typename auto> void g (auto& x); g (f ()); // auto -> const int, x is const int&
One interesting consequence of this equivalence is that auto
also uses the special perfect forwarding deduction rules when we have just auto&&
. Consider this example:
struct s {}; s f1 (); const s f2 (); s& f3 (); const s& f4 (); auto&& r1 (f1 ()); // s&& auto&& r2 (f2 ()); // const s&& auto&& r3 (f3 ()); // s& auto&& r4 (f4 ()); // const s&
While probably not very useful in ordinary code, this can be handy in generic code, if, for example, we need to forward an unknown return value to another function:
template <typename F1, typename F2, typename F3> void compose (F1 f1, F2 f2, F3 f3) { auto&& r = f1 (); f2 (); f3 (std::forward<decltype (f1 ())> (r)); }
August 14th, 2012 at 1:30 pm
You first say this:
const int& f3 ();
auto a3 = f3 (); // a1 is int
Then you say that
const int& f ();
auto& x = f (); // auto -> const int, x is const int&
Wouldn’t this imply that:
auto& x = f (); // auto -> int, x is int&
August 14th, 2012 at 2:24 pm
Jesse, only if auto is not used to form a reference the top-level const-qualifier is stripped. I know this is a lot of “if-this-then-that” special cases. That’s why I like the idea of mentally re-writing a statement with auto into a function template. Consider:
August 15th, 2012 at 11:39 am
In addition to what was said in this article, I think the great benefit of keyword `auto` is its use follow the DRY principle. It also allows to capture the intention of coder more accurately. Consider:
int f() { return 42; }
int x = f(); // In this line, what we really want is a variable x of type whatever f() returns, which we have to write explicitly as int, violating DRY
auto y = f(); // This does not violate DRY, and matches the intention of having variable x of type whatever f() returns
That said, all these auto and decltype keywords make it more mandatory to have a good IDE with type induction. While previously you could use notepad and Search to find the type of a given variable, now that process becomes more convoluted. Instead, your IDE should be able to tell you what the type of any variable is right away.