12

为什么下面两个模板不兼容不能重载?

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

main.cpp:6:16: error: redefinition of 'f'但是,如果我不使用尾随返回类型,Clang 和 GCC 会抱怨。

(请注意,这是一个rationale问题。我不是在寻找标准中定义这种行为的位置——如果你愿意,你也可以在你的答案中包括它——而是为了解释为什么这种行为是可取的或现状)。

4

4 回答 4

6

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

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

  2. 此功能的一个流行用例是返回lambdas的函数模板。但是,我们很难将 lambda 作为签名的一部分——之前已经详细阐述过会出现的复杂情况。单是弄脏就需要英勇的努力。因此,我们必须使用 lambdas 排除函数模板。

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

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

于 2015-10-25T14:24:54.363 回答
1

我认为这可能是委员会的遗漏,但我相信的背景故事如下:

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

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

    实际上,在函数实例化之前,编译器的值auto并不感兴趣。显然,编译器不会向函数体添加一些 SFINAE 检查以检查是否存在,因为在函数体内使用T::size时在所有其他情况下都不会T

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

    在第一种情况下,编译器会像

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

于 2015-10-25T14:17:50.830 回答
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(whatever) 可以帮助我们区分符号,它是签名的一部分。但是“auto”对我们没有帮助......因此,如果 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> >)

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

于 2015-10-25T14:18:39.250 回答
1

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

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

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

于 2015-10-25T15:08:36.270 回答