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

3 Responses to “Using C++11 auto and decltype

  1. Jesse Says:

    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&

  2. Boris Kolpackov Says:

    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:

    template <typename T>
    void g1 (T x);
     
    template <typename T>
    void g2 (T& x);
     
    g1 (f ()); // T is int (not const int), x is int
    g2 (f ()); // T is const int, x is const int&
    
  3. Dmitri Says:

    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.