【问题标题】:Template Specialization VS Function Overloading模板专业化 VS 函数重载
【发布时间】:2011-10-29 19:09:36
【问题描述】:

一本教科书我注意到您可以通过模板特化或函数重载为标准库函数(如swap(x,y))提供自己的实现。这对于任何可以从赋值交换以外的东西中受益的类型都很有用,例如 STL 容器(我知道,它们已经编写了交换)。

我的问题如下:

  1. 更好的是:模板专业化,让您的专业化 交换实现,或函数重载提供确切的 您希望在没有模板的情况下使用参数?

  2. 为什么更好?或者如果他们是平等的,为什么会这样?

【问题讨论】:

    标签: c++ stl template-specialization standard-library overloading


    【解决方案1】:

    简短的故事:尽可能重载,在需要时专攻。

    长话短说:C++ 对待特化和重载的方式非常不同。这最好用一个例子来解释。

    template <typename T> void foo(T);
    template <typename T> void foo(T*); // overload of foo(T)
    template <>           void foo<int>(int*); // specialisation of foo(T*)
    
    foo(new int); // calls foo<int>(int*);
    

    现在让我们交换最后两个。

    template <typename T> void foo(T);
    template <>           void foo<int*>(int*); // specialisation of foo(T)
    template <typename T> void foo(T*); // overload of foo(T)
    
    foo(new int); // calls foo(T*) !!!
    

    编译器在查看特化之前就进行了重载解析。因此,在这两种情况下,重载决议都选择foo(T*)。但是,只有在第一种情况下,它才能找到 foo&lt;int*&gt;(int*),因为在第二种情况下,int* 特化是 foo(T) 的特化,而不是 foo(T*)


    你提到了std::swap。这让事情变得更加复杂。

    标准规定您可以向std 命名空间添加特化。太好了,所以你有一些 Foo 类型并且它有一个高性能交换,然后你只需在 std 命名空间中专门化 swap(Foo&amp;, Foo&amp;)。没问题。

    但是如果Foo 是一个模板类呢? C++ 没有函数的部分特化,所以你不能特化swap。您唯一的选择是重载,但标准规定您不得将重载添加到 std 命名空间中!

    此时您有两个选择:

    1. 在您自己的命名空间中创建一个swap(Foo&lt;T&gt;&amp;, Foo&lt;T&gt;&amp;) 函数,并希望通过ADL 找到它。我说“希望”是因为如果标准库调用像 std::swap(a, b); 这样的交换,那么 ADL 根本不起作用。

    2. 忽略标准中说不要添加重载的部分,无论如何都要这样做。老实说,即使在技术上不允许这样做,但在所有现实场景中它都会起作用。

    但要记住的一点是,标准库完全不能保证使用swap。大多数算法使用std::iter_swap,在我看过的一些实现中,它并不总是转发到std::swap

    【讨论】:

    • “我说希望是因为” ... 这一点在几年前的 WG21 中就已经提到过。所有标准库的实现者都知道不这样做。
    • 简单明了的解释:两种情况下编译器都会首先找到重载 F(T*) 的最佳匹配。但在案例 1 中,它可以找到 F(T*) - foo(int*) 的特化,所以它选择了它。在情况 2 中,没有 F(T*) 的特化,所以它选择 F(T*)。请注意,在案例 2 中,唯一的特化是 F(T),而不是 F(T*)。
    • 为什么有这么多赞成票?我在 gcc 4.8 和 LLVM 5.0 中进行了测试,第一个示例选择了 foo(T*)。
    • “我说“希望”是因为如果标准库像 std::swap(a, b); 那样调用交换,那么 ADL 根本就无法工作。” 标准保证库不会那样做, 所以它很简单,所以选项 (1) 总是比 (2) 好。
    • “现在让我们交换最后两个”。最后两行不一样。一种是“void foo(int*);”另一个是“void foo(int*);”
    【解决方案2】:

    Peter Alexander 的回答没有什么可补充的。让我只提一下函数专业化中的一种使用可能比重载更可取:如果您必须在没有参数的函数中进行选择

    例如

    template<class T> T zero();
    template<> int zero() { return 0; }
    template<> long zero() { return 0L; }
    

    要使用函数重载做类似的事情,你必须在函数签名中添加一个参数:

    int zero(int) { return 0; }
    long zero(long) { return 0L; }
    

    【讨论】:

    • 您可以使用template &lt;typename&gt; struct Tag{}; 作为虚拟参数进行标签调度。处理无法构造或作为参数轻松传递的类型:foo(tag&lt;void&gt;{})foo(tag&lt;int[42]&gt;{})
    【解决方案3】:

    你不能重载 std 命名空间中的函数,但你可以专门化模板(我记得),所以这是一种选择。

    另一种选择是在调用非限定交换之前将 swap 函数与它正在操作的事物和 using std::swap; 放在同一命名空间中。

    【讨论】:

    • 比起using namespace std;,我更喜欢using std::swap;
    • 您可以特化一些模板:"只有当声明依赖于用户定义的类型并且特化符合标准时,程序才能将任何标准库模板的模板特化添加到命名空间 std原始模板的库要求,并未明确禁止。” [n3337; 17.6.4.2.1/1] 所以你可以将std::swap 专门用于不在std 中的类型。
    • 为什么不能重载标准函数?是坏习惯,但 std 命名空间并不特别。我不喜欢它,但是许多产品在 std 命名空间中为其类型添加了 std swap。
    猜你喜欢
    • 1970-01-01
    • 2021-12-11
    • 2013-12-27
    • 2016-09-12
    • 2020-07-25
    • 2011-02-19
    • 2017-12-10
    • 1970-01-01
    相关资源
    最近更新 更多