Item 3: Understand decltype

decltype rules are sometimes preferred in type deduction. Primary use case of decltype: When declaring function templates, make function’s return type depends on its parameter type.

Let’s examine decltype with a series of get() function.

1. Use the trailing return type syntax

C++11’s trailing return type syntax has nothing to do with auto type deduction. Advantage: You may use parameter types in specifying return type.

// Trailing return type syntax
template<typename C, typename I>
auto get(C& c, I i) -> decltype(c[i]) {
  return c[i];
}

2. Use the trailing return type syntax with type deduction

C++11 permits return type of single-statement lambdas to be deduced (again, nothing to do with auto type deduction though it reuses auto keyword). C++14 extends this feature to all lambdas and all functions.

// C++14
// Trailing return type syntax with return type deduced
template<typename C, typename I>
auto get(C& c, I i) {
  // New problem: Deduced type is not a reference type
  return c[i];
}

The problem is that, with normal type deduction rules, the reference-ness is ignored, and the deduced return type is not a reference type. So this won’t compile: get(container, 5) = 10.

3. Use decltype rules in type deduction

To fix this, C++14 introduces the decltype(auto) specifier. auto means using type deduction, and decltype means using decltype rules in type deduction, not the normal rules.

// C++14
template<typename C, typename I>
decltype(auto) get(C& c, I i) {
  return c[i];
}

// Now assignments work
get(container, 5) = 10;

4. Use universal reference

To support passing rvalue to get, we would use universal reference. (This is not strictly decltype related, but it’s nice to know.) (Check Item 25 for more about universal reference and std::forward.)

// C++14
template<typename C, typename I>
decltype(auto) get(C&& c, I i) {
  return std::forward<C>(c)[i];
}

// C++11
template<typename C, typename I>
auto get(C&& c, I i) -> decltype(std::forward<C>(c)[i]) {
  return std::forward<C>(c)[i];
}

// Now rvalues work
auto x = get(makeContainer(), 7);

decltype pitfall

  • When decltype is applied to names, it yields the type of that name
  • When decltype is applied to lvalue expressions, it generally ensures that the type reported is an lvalue reference
  • Now the surprise: x is a name, but (x) is lvalue expression:
decltype(auto) f1() {
  int x = 0;
  return x;  // decltype(x) is int, so f1 returns int
}

// Surprise! Return reference to a local variable!
decltype(auto) f2() {
  int x = 0;
  return (x);  // decltype((x)) is int&, so f2 returns int&
}
Creative Commons License
This blog by Che-Liang Chiou is licensed under a Creative Commons Attribution 4.0 International License.