1. Overview

In this article, we will be looking at new in c++14 type-specifier decltype(auto). We will learn how it enhances bare auto usage, and when its use really makes sense.

2. History

Since C++11, you can use auto when declaring variables to deduce type instead of writing it explicitly:

auto val = 10; is equivalent to: int val = 10;

The wide use of auto is considered a good coding style as it promotes programming against interfaces instead of against types and implementations. The problem with auto is that it does not always behave as we would like it to. It strips references, constness, and decays arrays. The rules for auto type deduction for variables are almost the same as for function template type deduction1Effective Modern C++ by Scott Meyers (it only differs when auto variable is assigned initializer list using braces). On the other hand, decltype deduces exactly the type passed to it. To allow you to choose the type of deduction rules for auto,  C++14 allows you to use decltype(auto) in place of bare auto if you want decltype deduction rules.

3. How to view deduced types

Before seeing examples, we want to introduce a simple way to check the type of variable – without using any special libraries or compiler functionalities2Effective Modern C++ – Item 4. To do it, it suffices to force the compiler to issue a compilation error, which will show us the type of variable:

template<typename T>
struct TD;

TD is a declaration of a template but on purpose no definition is ever given, then:

int val;
TD<decltype(val)> td;

will cause compilation error:

error: implicit instantiation of undefined template 'TD<int>'

from which we see that the type of val is int. Of course, you can use your favorite IDE to show you the type of auto and other variables.

4. How decltype(auto) can be used

To understand when decltype(auto) can be used, let’s see when auto might give us unexpected types. Following code presents case when auto strips const and & :

std::string str = "123";
const std::string& str_ref = str;

auto aval = str_ref; // Here aval is of type std::string

TD<decltype(aval)> type_test; // gives an error informing that TD<std::string> cannot be deduced, which indicates that aval is of type std::string.

if your intention was to make aval follow the type of str to be of type const std::string& use decltype(auto):

decltype(auto) aval = str_ref; // Now aval is of type const std::string&

But to be honest, a better approach is to use const auto& aval = str_ref; as it explicitly specifies your intention.

Another example is when calling a function. Suppose you have some method returning a reference to const object:

struct MyData {
  private:
    std::string data;
  public:
    const std::string& GetData() {
      return data;
    }
};

int main() {
  MyData data;
  auto val = data.GetData(); // val is of std::string type
}

So val will contain a copy of what MyData::data contains, this might cause various problems, starting with inefficiency. To make sure val is const&, use decltype(auto):

decltype(auto) val = data.GetData(); // val is of const std::string& type

// ...

auto& val2 = data.GetData(); // same as for decltype(auto), val2 is of type const std::string&

As you see auto& gives the same result as decltype(auto), so it seems to be a good alternative. The problem with auto& might arise when rvalue references (&&) are used. auto& val = std::move(str); will issue an error as val is non const. decltype(auto) takes away from you all the headaches from such subtle problems which might arise in templates.

decltype(auto) can also be used as function return type to use decltype rules for deducing return type. For example: decltype(auto) foo() { return 123; }.

Place where decltype(auto) really shines is with perfect forwarding, it allows you to define a template function that will pass some arguments to some other function and return exactly the same type as this function:

struct MyData {
  private:
    std::string data;
  public:
    const std::string& GetData(int val) {
      data += std::to_string(val);
      return data;
    }
};

template<class Obj, class F, class... Args>
decltype(auto) super_call(Obj& obj, F function, Args&&... args) 
{ 
    return (obj.*function)(std::forward<Args>(args)...); 
}


int main() {
  MyData dd;
  decltype(auto) res = super_call(dd, &MyData::GetData, 1); 
  // Here super_call return type is const std:string&
  // exactly as return type of MyData::GetData
}

You can think of super_call as a function in the middle of some generic call, which might for example log or count its usage.

5. Conclusions

In this article, we were looking at the decltype(auto) type specifier and how it can be used in our code. We learned that compiler will substitute it with the exact same type (with same adornments – const and &) as what is placed in the function return or on the right side of the assignment operator.

  • 1
    Effective Modern C++ by Scott Meyers
  • 2
    Effective Modern C++ – Item 4

Leave a Reply

Your email address will not be published. Required fields are marked *