【问题标题】:Why template function only base the return type works on C++?为什么模板函数仅基于返回类型适用于 C++?
【发布时间】:2019-01-15 08:34:48
【问题描述】:

据我所知,重载函数必须包含不同的参数(类型或计数)。所以我认为模板函数不应该只基于返回类型。但是,以下代码适用于 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++ 标准是否澄清了这一点?谢谢!

【问题讨论】:

  • 这是简单的重载,但是当你指定模板参数时,就没有更多的重载决议了,因为只有 1 个候选函数。
  • add&lt;int&gt;add&lt;double&gt; 是两个完全独立的函数,它们彼此无关(它们是同一个模板的特化这一事实在这里并不重要)。所以这里没有重载。将它们视为他们调用add_intadd_double

标签: c++ templates


【解决方案1】:

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

[defns.signature]

⟨function⟩名称、参数类型列表和封闭命名空间(如果有)

[ 注意:签名用作名称修改和 链接。 — 尾注 ]

但是对于函数模板specializations,无论是隐式生成还是显式生成,签名都包含参数:

[defns.signature.spec]

⟨函数模板特化⟩其中模板的签名 它是一个特化及其模板参数(无论是显式 指定或推断)

所以对于add&lt;int&gt;int 成为签名的一部分。不是因为它是返回类型,而是因为它是模板参数。 add&lt;double&gt; 也一样。只要签名不同,它们就可以被识别为不同的函数,因此可能会在相同的名称上重载。

【讨论】:

    【解决方案2】:

    用户StoryTeller 给出了来自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 以及在它们被实例化时执行类似性质的操作。

    你的函数模板有这个签名:这将生成它需要满足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;
    }
    

    现在查看上面的代码,您有完全相同的函数调用。那么在这种情况下 add 调用的是哪个重载?这很简单;如果不使用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;
    }
    

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

    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;
    }
    

    这与您的输出完全匹配!

    -输出-

    2
    2.1
    

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

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

    【讨论】:

      【解决方案3】:

      考虑这段代码:

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

      然后(假设)当您调用foo() 时,编译器将看到两个候选重载决议,并且无法判断您想要哪个,也无法明确您想要哪个函数,所以这是在定义时被禁止的。

      但是在您的代码中,当您调用add&lt;int&gt;(1.1, 1) 时,编译器会看到只有一个 候选,因为您已明确指定模板参数,即::add&lt;int&gt;(double, double),因此这里没有重载并且因此没有任何问题。

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

      template int add<int>(double, double);
      template double add<double>(double, double);
      
      cout << add(1.1, 1);
      

      上面 sn-p 的前两行显式实例化了两个模板参数的模板函数,最后一行提出了重载决议,因为无法区分两个实例,所以失败了。但是您还有另一个选项可以消除此函数调用的歧义(指定模板参数),这就是前两行可以编译的原因。

      【讨论】:

      • Define(Declare) int foo(void) { return 1; } double foo(void) { return 1.0; } 导致编译错误:ambiguating declaration 甚至不调用其中之一。但是定义template&lt;typename T&gt; T add(double a, double b) { return static_cast&lt;T&gt;(a + b); }就可以了。
      • 您的foo 示例在第二个foo 声明中立即格式错误,而不是在cout &lt;&lt; foo(),因此您的解释不正确。
      • @sfz 这就是重点。你无法告诉编译器你使用哪个函数,所以直接禁止这样定义,但是对于第二种情况,你有机会说清楚,所以它不会在定义点出错。
      • “编译器看到两个候选重载决议” - 我不认为重载决议是正确的术语。
      • @Bathsheba 我不知道,还有什么可能?
      【解决方案4】:

      我已经尝试使用method from here

      首先,给出测试代码:

      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;
      }
      

      然后输入command:

      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)
      

      【讨论】:

        【解决方案5】:

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

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

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

        请注意一个需要揭穿的神话:在您的情况下,add&lt;double&gt;(double, double)add&lt;int&gt;(double, double)不是 函数重载(它们怎么可能?- 名称、通风和参数类型是相同的),而是模板函数的不同实例化。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2012-07-02
          • 2022-01-16
          • 1970-01-01
          • 2018-04-06
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多