Sunday, July 7, 2013

When decltype meets auto

C++11 has three sets of type deduction rules:
  • Those used in template type deduction.
  • Those used in auto type deduction.
  • Those used by decltype.
The rules for auto type deduction are the same as the rules for template type deduction, except that given a braced initializer such as { 1, 2, 3, 4 }, auto will deduce a std::initializer list type (in the case of { 1, 2, 3, 4 }, it will be std::initializer_list<int>), while template type deduction will fail. (I have no idea why type deduction for auto and for templates is not identical. If you know, please tell me!) The rules for decltype are more complicated, because they don't just distinguish between lvalues and rvalues, they also distinguish between id-expressions (i.e., expressions consisting only of identifiers, e.g., variable or parameter names) and non-id-expressions. For details on all these rules, consult this article by Thomas Becker, this article by me, or this article by Herb Sutter (for auto) and this one by Andrew Koenig (for decltype).

But that's for C++11, which, among the C++-obsessed, is rapidly approaching yawnworthiness. Fortunately, C++14 is on the horizon, and one of the new features sure to stifle even the strongest of  yawns is the ability to declare types using decltype(auto). This feature leads to two questions, only the first of which is rhetorical:
  1. You can declare what?
  2. During type deduction for decltype(auto), which type deduction rules are to be followed: those for auto or those for decltype?  Or does decltype(auto) have its own set of type deduction rules?
The answer is that decltype(auto)uses the decltype type deduction rules. The reason is that the type deduced by auto for an initializing expression strips the ref-qualifiers (i.e., lvalue references and rvalue references) and top-level cv-qualifiers (i.e., consts and volatiles) from the expression, but decltype does not. As a result, if you want the ref- and cv-qualifier stripping behavior, you can just write auto. If you don't, C++14 gives you the option of writing decltype(auto).

For variable declarations, this saves you the trouble of typing the initializing expression twice,
decltype(longAndComplexInitializingExpression) var =
  longAndComplexInitializingExpression;                     // C++11

decltype(auto) var = longAndComplexInitializingExpression;  // C++14
For auto function return types (another new C++14 feature), it's even more convenient. Consider a function template, grab, that authenticates a user and, assuming authentication doesn't throw, returns the result of indexing into some container-like object. Bearing in mind that some standard containers return lvalue references from their operator[] operations (e.g., std::vector, std::deque), while others return proxy objects (e.g., std::vector<bool>), and I believe it would be valid to define a container such that invoking operator[] on an rvalue would yield an rvalue reference, the proper "generic" way to declare this function in C++11 would be (I think):
template<typename ContainerType, typename IndexType>                //C++11
auto grab(ContainerType&& container, IndexType&& index) -> 
  decltype(std::forward<ContainerType>(container)[std::forward<IndexType>(index)]);
{
  authenticateUser();
  return std::forward<ContainerType>(container)[std::forward<IndexType>(index)];
}

In C++14, I believe this can be simplified to the following, thanks to function return type deduction and decltype(auto):
template<typename ContainerType, typename IndexType>                // C++14
decltype(auto) grab(ContainerType&& container, IndexType&& index)
{
  authenticateUser();
  return std::forward<ContainerType>(container)[std::forward<IndexType>(index)];
}

Scott