为什么推导出的返回类型的模板不能与其他版本一起过载?

时间:2015-10-25 13:27:00

标签: c++ overloading c++14 auto rationale

为什么以下两个模板不兼容且无法重载?

#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>());   
}

我认为它们(或多或少)等同于以下编译精细(因为我们不能做decltype auto(t.size())我不能给出一个没有噪音的精确等价物。)。

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和GCC会抱怨main.cpp:6:16: error: redefinition of 'f'

(请注意,这是一个理由问题。我不是在寻找定义此行为的标准中的位置 - 如果您愿意,也可以在答案中包含 - 但是要解释为什么这种行为是可取的或现状的。

4 个答案:

答案 0 :(得分:6)

推导的返回类型显然不能成为签名的一部分。 但是,从return语句推断确定返回类型(并参与SFINAE)的表达式存在一些问题。假设我们采用第一个return语句的表达式并将其粘贴到一些调整后的虚拟尾随返回类型中:

  1. 如果返回的表达式依赖于本地声明,该怎么办?这不一定会阻止我们,但它会极大地扼杀规则。不要忘记我们不能使用声明的实体的名称;这可能会使我们的尾随返回类型的天空变得复杂,可能完全没有任何好处。

  2. 此功能的一个常见用例是返回 lambdas 的函数模板。然而,我们很难做出签名的一部分 - 之前已经详细阐述了可能出现的复杂情况。单独管理就需要英勇的努力。因此,我们必须使用lambdas排除函数模板。

  3. 如果声明的签名也不是定义,则无法确定,引入了一整套其他问题。最简单的解决方案是完全禁止(非定义)此类函数模板的声明,这几乎是荒谬的。

  4. 幸运的是,N3386的作者努力保持规则(和实现!)简单。我无法想象如何在一些极端情况下不必自己写一个尾随返回类型,这需要保证如此细致的规则。

答案 1 :(得分:1)

我认为这可能是委内瑞拉小姐,但我认为是背景故事:

  1. 您不能重载函数返回类型。这意味着在声明中

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

    auto的值实际上对编译器来说并不重要,直到函数实例化为止。显然,compiller不会向函数体添加一些SFINAE检查以检查T::size是否存在,因为在函数体内使用T时并非在所有其他情况下

  2. 当生成重载时,编译器将检查两个函数签名是否完全等效,并考虑到所有可能的替换。

    在第一种情况下,编译器将获得类似

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

    这完全等效

    在第二种情况下,当明确指定decltype时,它将被添加到模板参数中,以便您获得

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

    这显然不是确切的等价物。

    因此,当替换真实类型而不是T时,编译器将拒绝第一种情况,而第二种可能确定。

答案 2 :(得分:1)

查看我的编译器创建的符号:

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

你可以从中看到,是decltype(无论如何)可以帮助我们区分符号,它是签名的一部分。但“汽车”并没有帮助我们...... 因此,如果vector同时具有foobar和size方法,那么JSchaubStackOverflow的两个重载都将被破坏为 Z20JSchaubStackOverflowISt6vectorIiSaIiEEEDaT 现在我将留给其他人在ISO中找到关于模板函数签名的相关部分。

- EDIT-- 我知道它已经有了一个公认的答案,但仅仅是为了记录,这是一个技术难题 - 没有定义的声明:

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

这意味着可以在没有函数体的情况下完成所有事情。模板特化将在另一个翻译单元中给出,但为此,链接器需要找到它们...因此,不能在函数体上重载。

答案 3 :(得分:1)

“只有函数类型或其模板参数类型的直接上下文中的类型和表达式中的失败才是SFINAE错误。

如果对替换类型/表达式的求值导致副作用,例如某些模板特化的实例化,隐式定义的成员函数的生成等,则这些副作用中的错误被视为硬错误。 “source

您的第一个声明导致隐式替换返回类型,因此不符合SFINAE