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)); }