繁体   English   中英

为什么模板函数只基于返回类型才能在C ++上运行?

[英]Why template function only base the return type works on C++?

据我所知,重载函数必须包含不同的参数(类型或计数)。 所以我认为模板函数不应该只基于返回类型。 但是,以下代码适用于GCC 6.3.0

#include <iostream>
using namespace std;

template<typename T>
T add(double a, double b)
{
    return static_cast<T>(a + b); 
}

int main()
{
    cout << add<int>(1.1, 1) << endl;
    cout << add<double>(1.1, 1) << endl;
    return 0;
}

构建并运行:

g++ -g -o test test.cpp
./test
2
2.1

剂量C ++标准澄清了这一点? 谢谢!

不能仅基于返回类型重载的原因是返回类型不是函数签名的一部分,与参数类型不同。 不要相信我的话,C ++标准同样说:

[defns.signature]

⟨function⟩name,parameter-type-list和封闭命名空间(如果有的话)

[注意:签名用作名称修改和链接的基础。 - 结束说明]

但对于函数模板 ,被他们产生或明或暗地,签名包含参数(S):

[defns.signature.spec]

⟨functiontemplatespecialization⟩模板的签名,它是一个特化及其模板参数(无论是明确指定还是推导)

因此,对于add<int>int成为签名的一部分。 不是因为它是返回类型,而是因为它是模板参数。 add<double> 只要签名不同,那些可以被识别为不同的功能,因此可能在同一名称上重载。

用户讲故事的人给出了最好的直线上升回答未来的standard 我想通过给出一个分解编译器如何处理这个的细分示例来详细说明这个:


我们来看看你当前的代码:

 #include <iostream> using namespace std; template<typename T> T add(double a, double b) { return static_cast<T>(a + b); } int main() { cout << add<int>(1.1, 1) << endl; cout << add<double>(1.1, 1) << endl; return 0; } 

让我们看看编译器将如何处理这个问题。 在我们这样做之前,请记住这一点: templates必须在编译时知道,类似于C ++如何用宏替换文本,并定义它在templates实例化时也为templates做了一些本质的事情。

您的函数模板具有此签名:这将生成满足T所需的任何函数。

 template<typename T> T add(double a, double b) { return static_cast<T>(a + b); } 

但是在这种情况下, T不是签名的一部分。 该函数的签名如下所示:

::add<T>(double, double)

并且由于templates argument引用其return类型而不是其parameters之一,因此它在此处无效。


让我们看看这就像我们没有使用模板一样。 仅用于演示目的:忽略以下事实将创建不明确的函数:

int add( double, double );
float add( double, double );
double add( double, double );

现在让我们在没有模板版本的情况下在main中应用函数调用:

#include <iostream>

int main() {
    std::cout << add( 1.1, 1 ) << '\n';  // <int> - reminder of original
    std::cout << add( 1.1, 1 ) << '\n';  // <double> -     ""
    return 0;
}

现在查看上面的代码,您可以使用相同的函数调用。 那么在这种情况下添加哪个重载调用? 这很简单; 如果不使用template并忽略ambiguity ,上面的函数会调用double add( double, double )

由于上面会因为模糊不清而产生编译器错误,让我们回过头来应用template来研究为什么template版本不会出现这种歧义。


- 原始代码 -

#include <iostream>

template<typename T>
T add( double a, double b ) {
    return static_cast<T>( a + b );
}

int main() {
    std::cout << add<int>(1.1, 1) << '\n';
    std::cout << add<double>(1.1,1) << '\n';
    return 0;
}

让我们看看编译器如何逐步处理它:


- 步骤1: - 名称解析,获取功能签名。

int main() {
    std::cout << ::add<int>( 1.1, 1 ) << '\n';
    std::cout << ::add<double>( 1.1, 1 ) << '\n';
    return 0;
}

- 第2步: - 调用函数,并创建函数的调用堆栈

int main() {
    std::cout << 
        ::add<int>( 1.1, 1 ) {
           return static_cast<int>( 1.1 + 1 );
        }
              << '\n';

    std::cout <<
        ::add<double>( 1.1, 1 ) {
            return static_cast<double>( 1.1 + 1 );
        }
              << '\n';

    return 0;
}

- 步骤3: - 执行功能中的所有指令

int main() {
    std::cout << 
        /*::add<int>( 1.1, 1 ) {
           return static_cast<int>( 1.1 + 1 );
        }*/
           return static_cast<int>( 2.1 ); 
              << '\n';

    std::cout <<
        /*::add<double>( 1.1, 1 ) {
            return static_cast<double>( 1.1 + 1 );
        }*/
            return static_cast<double>( 2.1 );
              << '\n';
    return 0;
}

- 步骤4: - 从函数返回结果并清理函数调用堆栈

int main() {
    std::cout << 
            return 2; 
              << '\n';

    std::cout <<
            return 2.1;
              << '\n';
    return 0;
}

- 第5步: - 主要功能是将返回的结果传递给流操作符到标准屏幕输出。

int main() {
    std::cout << 2 << '\n';
    std::cout << 2.1 << '\n';
    return 0;
}

这完全符合您的输出!

-Output-

2
2.1

我希望这个分解可以帮助你更好地理解templates并且看看为什么没有歧义,就像你没有使用它们一样。 这里的底线是,由于您explicitly实例化了函数模板,因此没有歧义。

现在尝试再次运行程序,但这次没有指定类型,让编译器implicitly实例化函数模板。 我相信你会得到一个编译错误!

考虑以下代码:

int    foo(void) { return 1; }
double foo(void) { return 1.0; }

然后(假设)当你调用foo() ,编译器会看到两个重载决策候选者,并且没有办法告诉你想要哪一个,也没有办法澄清你想要的功能,所以这是禁止的在定义点。

但是在你的代码中,当你调用add<int>(1.1, 1) ,编译器只能看到一个候选项,因为你已经明确指定了模板参数,即::add<int>(double, double) ,所以这里有这里没有超载因此没有出错。

另一方面,下面的代码会引起与答案第一部分相同的混淆:

template int add<int>(double, double);
template double add<double>(double, double);

cout << add(1.1, 1);

上面代码段的前两行显式地为两个模板参数实例化了模板函数,最后一行显示了重载决策,由于没有办法区分这两个实例,因此失败了。 但是你有另一个选项可以去除这个函数调用(指定模板参数),这就是前两行可以编译的原因。

我试过用这里方法

首先,给出测试代码:

template < class T> T add(T a, T b){
            return a+b;
}

void tmp(){
    add<int>(10, 2);
}

int add(int a, int b)
{
    return a + b;
}

然后输入commond:

gcc -S -O1 test.cpp

最后,我将获得以下内容:

    .file   "compile2.cpp"
    .text
    .globl  _Z3tmpv
    .type   _Z3tmpv, @function
_Z3tmpv:
.LFB1:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    $2, %esi
    movl    $10, %edi
    call    _Z3addIiET_S0_S0_
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE1:
    .size   _Z3tmpv, .-_Z3tmpv
    .globl  _Z3addii
    .type   _Z3addii, @function
_Z3addii:
.LFB2:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    %edi, -4(%rbp)
    movl    %esi, -8(%rbp)
    movl    -8(%rbp), %eax
    movl    -4(%rbp), %edx
    addl    %edx, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE2:
    .size   _Z3addii, .-_Z3addii
    .section    .text._Z3addIiET_S0_S0_,"axG",@progbits,_Z3addIiET_S0_S0_,comdat
    .weak   _Z3addIiET_S0_S0_
    .type   _Z3addIiET_S0_S0_, @function
_Z3addIiET_S0_S0_:
.LFB3:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    %edi, -4(%rbp)
    movl    %esi, -8(%rbp)
    movl    -8(%rbp), %eax
    movl    -4(%rbp), %edx
    addl    %edx, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE3:
    .size   _Z3addIiET_S0_S0_, .-_Z3addIiET_S0_S0_
    .ident  "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-36)"
    .section    .note.GNU-stack,"",@progbits

并且,我们可以找到两个不同的函数签名_Z3addii_Z3addIiET_S0_S0_

[root@localhost template]# c++filt _Z3addIiET_S0_S0_
int add<int>(int, int)
[root@localhost template]# c++filt _Z3addii
add(int, int)

C ++中的模板功能自第一次开始以来就以随意的方式发展(基本上只允许我们编写通用容器类)。 在此之后,C ++编程社区很快将它们用于其他用法(例如元编程技术)。

由于C ++标准委员会(实际上是Bjarne本人在移交语言控制之前)认为它有用,因此仅允许基于返回类型实例化不同函数的能力。 它是:如果只有std::accumulate这种方式工作,而不是从提供初始值的变量类型派生返回类型!

非常有用的是,从C ++ 11开始,我们甚至可以使用尾随返回类型语法来允许编译器在只能通过检查函数参数列表以及在后面的标准中检查函数内容时才能发现返回类型。

注意一个需要被揭穿的神话:在你的情况下add<double>(double, double)add<int>(double, double) 不是函数重载(它们怎么可能? - 名称,空格和参数类型是相同的),而是模板功能的不同实例。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM