Modern C++ Features – decltype and std::declval

decltype and std::declval are two features that go hand in hand and are most useful in template metaprogramming and in conjunction with the use of type deduction via auto, e.g. in generic lambdas.

As with many template functionalities (generic lambdas loosely fit into that category), the two are mostly used in library development. That does not mean that they are of no interest or use for application development. After all, from time to time everyone has to write their own utility classes that go into that direction.

decltype

The decltype specifier introduced in C++11 does, roughly speaking, give us the type of an expression or entity. To be consistent with other standard namings, the type probably should have been typeof, but due to existing, incompatible extensions of that name in many compilers, decltype was chosen instead.

So, decltype “returns” a type. It can basically be used wherever a type is needed:

 struct X {
   int i;
   double bar(short);
 };

 X x;
 decltype(x) y; //y has type X;
 std::vector<decltype(x.i)> vi; //vector<int>
 using memberFunctionPointer = decltype(&X::bar); //double X::(*)(short)

 auto lam = [&]() -> decltype(y) { return y; }; //decltype(y) is const X&

decltype returns

But what type exactly does decltype return? I’ll simplify the wording just a bit, if you want a precise definition, refer to the cppreference site on decltype.

If what we pass to decltype is the name of a variable (e.g. decltype(x) above) or function or denotes a member of an object (decltype x.i), then the result is the type of whatever this refers to. As the example of decltype(y) above shows, this includes reference, const and volatile specifiers.

An exception to this last rule is the use of C++17’s structured binding: If the name we pass to decltype is one of a variable defined in structured binding, then the result is the type of the bound-to element. Example:

std::pair<int volatile &&, double&> f(int);
auto const& [a, b] = f(22);

While the type of a is int const volatile&, decltype(a) will give int volatile&&, as that is the type of the first element of f‘s return value. Similarly, decltype(b) will result in double&, not double const&.

If the expression passed to decltype is not just a name or member access expression, the resulting type depends on the value category of the expression. Given the type of the expression e is E, then decltype(e) is

  • E, if e is an prvalue,
  • E&, if e is an lvalue, and
  • E&&, if e is a xvalue

As an example, the above decltype(&X::bar) is just a member function pointer and not a reference to one, because the built-in address-of operator returns a prvalue.

These rules may look complicated, but they mostly do what you’d naturally expect, with the exceptions of the mentioned results of structured binding and the fact that a name expression in parentheses makes it an lvalue. That means, that when x is a variable of type X, then decltype((x)) will give X& as opposed to decltype(x) giving x.

Use cases

One of the standard examples for decltype in C++11 was determining the return type of a function template that returns an expression dependent on the template parameters. A usual suspect is a simple addition: Adding two values of possibly different types can give a result of any type, especially when operator overloading is involved.

As an example, the addition of an int to a char const* results in a char const*. Adding a std::string to a char const* on the other hand results in a std::string. Adding a SugarCube to a FuelTank probably results in volatile Engine.

template <class T, class U>
auto add(T const& t, U const& u) -> decltype(t+u) {
  return t+u;
}

Luckily, in C++14 we got return type deduction for functions, so we can leave it to the compiler and remove this use of decltype.

But, also with C++14, we got generic lambdas. Those are basically lambdas with a templated function call operator, but we don’t get to declare any template parameters. Actually working with the type of whatever was passed to the lambda requires decltype:

auto make_multiples = [](auto const& x, std::size_t n) { 
  return std::vector<std::decay_t<decltype(x)>>(n, x); 
};

Here, std::decay_t will strip the const& from the type given by decltype, because decltype(x) will not result in what would have been T in a template, but in what would have been T const&.

decltype does not execute anything

Whatever expression we pass to decltype does not get executed. That means, that we don’t pay any runtime overhead and don’t see any side effects. For example, decltype(std::cout << "Hello world!\n") will result in std::ostream&, but not a single character will be printed to our console.

When we call functions, usually the involved types, especially the return types, must be defined. It is, however, possible to declare a function with an incomplete type as return parameter, by using forward declarations. decltype is consistent in that manner as it can be used on such functions without having to define the return type. After all, we know that there is such a type and that’s all we and the compiler care about.

class Foo; //forward declaration
Foo f(int); //ok. Foo is still incomplete
using f_result = decltype(f(11)); //f_result is Foo

std::declval

In some contexts, we don’t have the objects available that we need to pass to an expression to evaluate it in decltype and similar functionalities. We might even not be able to create those objects at all, e.g. because their classes have only private or protected constructors.

Consider for example the last example. decltype(f(11)) says “what type will I get when I call f with 11?”. What we actually mean is “what type will I get when I call f with some int?”. In the case of int, we just could use a default initialized int. But the default constructor is not always available.

For those cases, std::declval comes in handy. It is just a declared function template that returns an rvalue reference to whatever you pass to it. That way we don’t need to artificially declare a poorly named function to have something that we can use in our decltype argument: decltype(f(std::declval<int>()))

It comes especially handy if you are in a templated context and the value you want to obtain depends on a template parameter. Consider this little type alias for whatever gives the addition of two types:

template<typename T, typename U>
using sum_t = decltype(std::declval<T>() + std::declval<U>());

Read this as “sum_t is the type I get when I add some T to some U.” Note also that neither T nor U needs to be fully defined when we instantiate the template because the expression inside decltype never actually gets evaluated.

Conclusion

This was a fairly technical topic, and if you are not in the business of writing generic libraries or other template-heavy code, you are not likely to use it much. It is, however, likely that you come across it once in a while, and for template magicians, these two features are among the bread and butter tools.

Previous Post
Next Post

11 Comments



  1. Can you please elaborate what this line does and why it uses brackets?

    auto const& [a, b] = f(22);

    How is this type of initialization called?

    Reply

    1. It’s called structured binding and will be introduced in C++17. Think of it as aliases to the members of an object:

      auto const& E = f(22);

      now a is an alias to the first member (i.e. E.first in this case, since E is a std::pair) and b an alias to the second.

      Reply

  2. Arne,

    Great Article!

    “std::declval comes in handy. It is just a declared function template that returns an rvalue reference to whatever you pass to it.”

    Though we pass the lvalue type? wouldn’t the return type is based on reference collapsing rule?

    Reply

    1. Hi Rajeev,
      thanks for the input. Yes, reference collapsing rules apply, as was pointed out by Aaron in another comment.

      Reply

      1. Ahh…I should have started to read all comments 🙂

        Reply

  3. Nice article. I have found myself using std::declval<T&>() somwtimes but never saw a good documentation about these cases.

    Reply

    1. I think it’s definitely worth mentioning that to get an lvalue reference from declval you use std::declval<T&>() because it would then return T&&& which will collapse to T&.

      This is useful if passing the value to a function that takes a non-const reference because passing an rvalue reference would be a compile error.

      Reply

  4. Slight typo:
    “then decltype((x)) will give X& as opposed to decltype(x) giving x”

    that last x should be X, I think?

    Reply

  5. Hi Arne,
    Thank you for the article.

    Guess there is a small mistake in the make_multiples example.
    The arguments to the vector should be swapped: (x, n) -> (n, x)
    and you need to decay the type you pass to the vector, something like: std::decay_t<decltype(x)>

    Resulting in:

    return std::vector<std::decay_t<decltype(x)>>(n, x);

    What do you think?

    Reply

    1. Hi Grigoriy, thanks for pointing out that error, fixed it!

      Reply

Leave a Reply

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