Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is a template with deduced return type not overloadable with other versions of it?

Why are the following two templates incompatible and can't be overloaded?

#include <vector>

template<typename T>
auto f(T t) { return t.size(); }
template<typename T>
auto f(T t) { return t.foobar(); }

int main() {
   f(std::vector<int>());   
}

I would think they are (more or less) equivalent with the following which compiles fine (as we cannot do decltype auto(t.size()) I can't give an exact equivalent without some noise..).

template<typename T>
auto f(T t) -> decltype(t.size() /* plus some decay */) { return t.size(); }

template<typename T>
auto f(T t) -> decltype(t.foobar() /* plus some decay */) { return t.foobar(); }

Clang and GCC complain main.cpp:6:16: error: redefinition of 'f' if I leave off the trailing return type, however.

(Note that this is a rationale question. I am not seeking for the place in the Standard which defines this behavior - which you may include in your answer too, if you wish - but for an explanation of why this behavior is desirable or status-quo).

like image 803
Johannes Schaub - litb Avatar asked Oct 25 '15 13:10

Johannes Schaub - litb


People also ask

Can you use auto as a return type in c++?

In C++14, you can just use auto as a return type.

What is c++ type deduction?

Type inference or deduction refers to the automatic detection of the data type of an expression in a programming language. It is a feature present in some strongly statically typed languages. In C++, the auto keyword(added in C++ 11) is used for automatic type deduction.

Can a function have auto return type?

C++: “auto” return type deduction The “auto” keyword used to say the compiler: “The return type of this function is declared at the end”. In C++14, the compiler deduces the return type of the methods that have “auto” as return type. Restrictions: All returned values must be of the same type.


2 Answers

The deduced return type can clearly not be part of the signature. However, inferring an expression that determines the return type (and participates in SFINAE) from return statements has some issues. Let's say we were to take the first return statement's expression and paste it into some adjusted, virtual trailing-return-type:

  1. What if the returned expression depends on local declarations? This isn't necessarily stopping us, but it snarls the rules tremendously. Don't forget that we can't use the names of the entities declared; This could potentially complex our trailing-return-type sky-high for potentially no benefit at all.

  2. A popular use case of this feature are function templates returning lambdas. However, we can hardly make a lambda part of the signature - the complications that would arise have been elaborated on in great detail before. Mangling alone would require heroic efforts. Hence we'd have to exclude function templates using lambdas.

  3. The signature of a declaration couldn't be determined if it wasn't a definition also, introducing a whole set of other problems. The easiest solution would be to disallow (non-defining) declarations of such function templates entirely, which is almost ridiculous.

Fortunately the author of N3386 strove to keep the rules (and implementation!) simple. I can't imagine how not having to write a trailing-return-type yourself in some corner cases warrants such meticulous rules.

like image 131
Columbo Avatar answered Oct 11 '22 14:10

Columbo


I think that may be commitee miss but backstory I believe is the following:

  1. You cannot overload over the function return type. This means that in declaration

    template<typename T>
    auto f(T t) { return t.size(); }
    

    Value of auto is not interesting to compiler in fact until function instantiation. Obviously compiller does not add some SFINAE check to the function body to check if T::size exist as it does not in all other cases when T is used inside function body

  2. When generating overloads compiler will check if two function signatures are exact equivalent taking in mind all possible substitutions.

    In the first case then compiler will get smth like

    [template typename T] f(T)
    [template typename T] f(T)
    

    That are exact equivalent

    In the second case however as decltype specified explicitly it will be added to the template arguments so you'll get

    [template typename T, typename = typeof(T::size())] f(T)
    [template typename T, typename = typeof(T::size())] f(T)
    

    That are not exact equivalents obviously.

    So compiler will refuse the first case while second could be OK when substituting real type instead of T.

like image 27
Lol4t0 Avatar answered Oct 11 '22 14:10

Lol4t0