Why is a template with deduced return type not ove

2019-03-25 07:14发布

问题:

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).

回答1:

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.



回答2:

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.



回答3:

Looking at the symbols created by my compiler:

[tej@archivbox ~]$ cat test1.cc

#include <vector>

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

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

int do_something() {
       JSchaubStackOverflow(std::vector<int>());
       return 4;
}
[tej@archivbox ~]$ c++ -std=c++14 -pedantic test1.cc -c -o test1.o
[tej@archivbox ~]$ nm test1.o | grep JScha
0000000000000000 W _Z20JSchaubStackOverflowISt6vectorIiSaIiEEEDaT_
[tej@archivbox ~]$ nm -C test1.o | grep JScha
0000000000000000 W auto JSchaubStackOverflow<std::vector<int, std::allocator<int> > >(std::vector<int, std::allocator<int> >)
[tej@archivbox ~]$ cat test2.cc

#include <vector>

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

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

    Metallica* foobar() const
    {
        return nullptr;
    }
};


int do_something() {
       JSchaubStackOverflow(std::vector<int>());
       JSchaubStackOverflow(Metallica());
       return 4;
}
[tej@archivbox ~]$ c++ -std=c++14 -pedantic test2.cc -c -o test2.o
[tej@archivbox ~]$ nm test2.o | grep JScha
0000000000000000 W _Z20JSchaubStackOverflowI9MetallicaEDTcldtfp_6foobarEET_
0000000000000000 W _Z20JSchaubStackOverflowISt6vectorIiSaIiEEEDTcldtfp_4sizeEET_
[tej@archivbox ~]$ nm -C test2.o | grep JScha
0000000000000000 W decltype (({parm#1}.foobar)()) JSchaubStackOverflow<Metallica>(Metallica)
0000000000000000 W decltype (({parm#1}.size)()) JSchaubStackOverflow<std::vector<int, std::allocator<int> > >(std::vector<int, std::allocator<int> >)

What you can see from this, is the decltype(whatever) can help us differentiate between symbols, it is part of the signature. But "auto" doesn't help us... So if vector had both a foobar, and size method, both overloads of JSchaubStackOverflow would be mangled as Z20JSchaubStackOverflowISt6vectorIiSaIiEEEDaT Now I'll leave to someone else to find to related section in ISO about signatures of template functions.

--EDIT-- I know it already has an accepted answer, but just for the record, here is a technical difficulty -- declarations without definitions:

[tej@archivbox ~]$ cat test2.cc

#include <vector>

template<typename T>
auto JSchaubStackOverflow(T t) -> decltype(t.size());

template<typename T>
auto JSchaubStackOverflow(T t) -> decltype(t.foobar());

struct Metallica
{

    Metallica* foobar() const
    {
        return nullptr;
    }
};


int do_something() {
       JSchaubStackOverflow(std::vector<int>());
       JSchaubStackOverflow(Metallica());
       return 4;
}
[tej@archivbox ~]$ c++ -std=c++14 -pedantic test2.cc -c -o test2.o
[tej@archivbox ~]$ nm -C test2.o | grep JScha
                 U decltype (({parm#1}.foobar)()) JSchaubStackOverflow<Metallica>(Metallica)
                 U decltype (({parm#1}.size)()) JSchaubStackOverflow<std::vector<int, std::allocator<int> > >(std::vector<int, std::allocator<int> >)

This means one can do the whole thing without function bodies. The template specializations would be given in another translation unit, but for this, the linker needs to find them... thus one can't overload on function body.



回答4:

"Only the failures in the types and expressions in the immediate context of the function type or its template parameter types are SFINAE errors.

If the evaluation of a substituted type/expression causes a side-effect such as instantiation of some template specialization, generation of an implicitly-defined member function, etc, errors in those side-effects are treated as hard errors. " source

Your first declaration causes implicit substitution of the return type, and therefore does not adhere to SFINAE