Reason for decltype in trailing return type

1.4k Views Asked by At

I was reading a text about the "new" C++ features and came across decltype and usage of that. I understand the reasoning behind the decltype in the trailing return type of something like

template <typename lhsT, typename rhsT>
auto add(lhsT& lhs, rhsT& rhs) -> decltype(lhs + rhs) {
    return lhs +rhs;
}

Without it, the compiler would not be able to derive the return type of the template function. But why is the syntax the way it is?

Why not use something like

template <typename lhsT, typename rhsT>
decltype(lhs + rhs) add(lhsT& lhs, rhsT& rhs) {
    return lhs +rhs;
}

Would feel more "natural" since the return type is declared where it normally is, although as a result of the two arguments. Is it that this clash with something else or does it cause extra work for the compiler if the syntax was this way that's not worth it?

2

There are 2 best solutions below

0
On BEST ANSWER

Note

Without it, the compiler would not be able to derive the return type of the template function.

This was later fixed in C++14 and the compiler doesn't need the trailing return type as it can infer the return type from the returned expression. The following works fine in C++14.

template <typename lhsT, typename rhsT>
auto add(const lhsT& lhs, const rhsT& rhs) {
    return lhs + rhs;
}

But why is the syntax the way it is?

The use of trailing return type is useful when the return type is deduced from the arguments, which always are declared after the return type (one can't refer to something that hasn't been declared yet).

Also, a trailing return type does appear to be more natural in the sense that functional and mathematical notation uses this kind of declaration. I think most types of notation outside of C-style declarations commonly use trailing return type. E.g.:

f : X → Y

In above mathematical notation, f is the function name, X the argument, and Y the returned value.

Just because the flock of pink sheep reject the gray sheep doesn't make it unnatural.

In your second example it is not possible for the compiler to infer the return types as of above reason, i.e., that the arguments have not yet been declared. However, inference is possible if using the template parameters and std::declval, e.g.:

template <typename T, typename U>
decltype(std::declval<T>() + std::declval<U>()) add(const T& lhs, const U& rhs) {
    return lhs + rhs;
}

std::declval acts as a lazy instantiation of a type and never evaluates, e.g., calls the constructor etc. It is mostly used when inferring types.

2
On

It is difficult for the compiler to introduce symbols prior to them being introduced.

template <typename lhsT, typename rhsT>
decltype(lhs + rhs) add(lhsT& lhs, rhsT& rhs) {
  return lhs +rhs;
}

Here, lhs and rhs are used before they are declared.

This is hard to get right.

template <typename lhsT, typename rhsT>
auto add(lhsT& lhs, rhsT& rhs) -> decltype(lhs + rhs) {
  return lhs +rhs;
}

here they are used after they are declared. That is easy to get right.

C++ since C++11 has made a conscious choice to pay attention to what is hard and what it easy to implement in existing C++ compilers. There where a number of C++98/03 features that where so hard to implement that nobody really did, and in some cases impossible (publishing templates from a .cpp file, and some of the requirements of std::string, are two of them off the top of my head).