为什么具有推导的 return 类型的模板不能被其他版本重载?

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

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

#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' 但是,如果我离开尾随 return 类型。

(请注意,我不是在寻找定义此行为的标准中的位置 - 如果您愿意,您也可以将其包含在您的答案中 - 而是为了解释为什么会出现此行为理想或现状)。

我认为这可能是委员会失误,但我认为背景故事如下:

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

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

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

  2. 时,它不会在所有其他情况下存在
  3. 生成重载时,编译器将检查两个函数签名是否完全等价,同时考虑所有可能的替换。

    在第一种情况下,编译器会得到类似于

    [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 时可以

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

[tej@archivbox ~]$ cat test1.cc

#include <vector>

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

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

int do_something() {
       JSchaubWhosebug(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 _Z20JSchaubWhosebugISt6vectorIiSaIiEEEDaT_
[tej@archivbox ~]$ nm -C test1.o | grep JScha
0000000000000000 W auto JSchaubWhosebug<std::vector<int, std::allocator<int> > >(std::vector<int, std::allocator<int> >)
[tej@archivbox ~]$ cat test2.cc

#include <vector>

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

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

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


int do_something() {
       JSchaubWhosebug(std::vector<int>());
       JSchaubWhosebug(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 _Z20JSchaubWhosebugI9MetallicaEDTcldtfp_6foobarEET_
0000000000000000 W _Z20JSchaubWhosebugISt6vectorIiSaIiEEEDTcldtfp_4sizeEET_
[tej@archivbox ~]$ nm -C test2.o | grep JScha
0000000000000000 W decltype (({parm#1}.foobar)()) JSchaubWhosebug<Metallica>(Metallica)
0000000000000000 W decltype (({parm#1}.size)()) JSchaubWhosebug<std::vector<int, std::allocator<int> > >(std::vector<int, std::allocator<int> >)

从这里可以看出,decltype(whatever)可以帮助我们区分符号,它是签名的一部分。但是 "auto" 对我们没有帮助...... 因此,如果 vector 同时具有 foobar 和 size 方法,则 JSchaubWhosebug 的两个重载都会被破坏为 Z20JSchaubWhosebugISt6vectorIiSaIiEEEDaT 现在我将留给其他人在 ISO 中查找有关模板函数签名的相关部分。

--编辑-- 我知道它已经有一个可接受的答案,但只是为了记录,这里有一个技术难题——没有定义的声明:

[tej@archivbox ~]$ cat test2.cc

#include <vector>

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

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

struct Metallica
{

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


int do_something() {
       JSchaubWhosebug(std::vector<int>());
       JSchaubWhosebug(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)()) JSchaubWhosebug<Metallica>(Metallica)
                 U decltype (({parm#1}.size)()) JSchaubWhosebug<std::vector<int, std::allocator<int> > >(std::vector<int, std::allocator<int> >)

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

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

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

  2. 此功能的一个常见用例是函数模板 returning lambdas。然而,我们很难让 lambda 成为签名的一部分——之前已经详细阐述了可能出现的复杂情况。光是重整就需要英勇的努力。因此我们必须排除使用 lambda 的函数模板。

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

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

来自cppreference.com

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.

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