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

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

本文关键字:不能 其他 重载 版本 返回类型 为什么      更新时间:2023-10-16

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

#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语句中推断一个决定返回类型(并参与SFINAE)的表达式存在一些问题。假设我们要取第一个return语句的表达式并将其粘贴到某个调整后的虚拟尾随返回类型中:

  1. 如果返回的表达式依赖于本地声明怎么办?这并不一定会阻止我们,但它极大地扰乱了规则。不要忘记,我们不能使用声明的实体的名称;

  2. 这可能会使我们的跟踪回报类型的天空变得复杂,而可能根本没有任何好处。
  3. 此功能的一个流行用例是返回lambdas的函数模板。然而,我们很难将lambda作为签名的一部分——可能出现的复杂性在前面已经详细阐述过了。光是撕碎就需要巨大的努力。因此,我们必须使用lambdas排除函数模板。

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

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

我认为这可能是委员会的错过,但我相信背景故事是这样的:

  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时。

查看编译器创建的符号:

[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(无论什么)可以帮助我们区分符号,它是签名的一部分。但是"auto"对我们没有帮助……因此,如果vector同时具有foobar和size方法,则JSchaubStackOverflow的两个重载将被修改为Z20JSchaubStackOverflowISt6vectorIiSaIiEEEDaT现在,我将留给其他人去查找ISO中关于模板函数签名的相关章节。

——编辑我知道它已经有了一个公认的答案,但是为了记录,这里有一个技术上的困难——没有定义的声明:

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

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

From cppreference.com:

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

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

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

相关文章: