自由函数的部分模板特化 - 最佳实践

13
作为大多数C++程序员应该知道的,自由函数的部分模板特化是不允许的。例如,以下代码是非法的C++代码:
template <class T, int N>
T mul(const T& x) { return x * N; }

template <class T>
T mul<T, 0>(const T& x) { return T(0); }

// error: function template partial specialization ‘mul<T, 0>’ is not allowed

然而,类/结构体的部分模板特化是被允许的,并且可以被利用来模仿自由函数的部分模板特化功能。例如,可以通过使用以下方式实现上一个示例中的目标:

template <class T, int N>
struct mul_impl
{
    static T fun(const T& x) { return x * N; }
};

template <class T>
struct mul_impl<T, 0>
{
    static T fun(const T& x) { return T(0); }
};

template <class T, int N>
T mul(const T& x)
{
    return mul_impl<T, N>::fun(x);
}

虽然更加臃肿,不够简洁,但它完成了工作——对于使用mul的用户而言,他们得到了期望的部分特化。


我的问题是:在编写模板化自由函数(旨在供他人使用)时,您是否应自动将实现委托给类的静态方法函数,以便您库的用户可以随意实现部分特化,还是按照常规方式编写模板化函数,并接受人们无法进行特化的事实?


2
我认为这取决于情况。在你的情况下,你像这样调用它 fun<U, N>(u),所以你不能重载(N 在参数中没有出现)。但是我认为如果“重载”是可能的,那就是首选的方式。很好的例子是 std::swap 或者 std::begin 或者 std::end(后两个是 C++0x 的函数)。请注意,Sutter 的文章是9年前写的。不确定他是否仍然推荐使用“委托给类”的方式。而且我认为它不太适用:无法与 ADL 兼容 - 你将不得不处理各种命名空间并特化它们的模板。没有很好的解决方法。 - Johannes Schaub - litb
2个回答

3
正如litb所说,只要模板参数可以从调用参数中推导出来,ADL就是优秀的。
#include <iostream>

namespace arithmetic {
    template <class T, class S>
    T mul(const T& x, const S& y) { return x * y; }
}

namespace ns {
    class Identity {};

    // this is how we write a special mul
    template <class T>
    T mul(const T& x, const Identity&) {
        std::cout << "ADL works!\n";
        return x;
    }

    // this is just for illustration, so that the default mul compiles
    int operator*(int x, const Identity&) {
        std::cout << "No ADL!\n";
        return x;
    }
}

int main() {
    using arithmetic::mul;
    std::cout << mul(3, ns::Identity()) << "\n";
    std::cout << arithmetic::mul(5, ns::Identity());
}

输出:

ADL works!
3
No ADL!
5

重载和ADL实现了部分特化函数模板 arithmetic::mul 的效果,使其适用于 S = ns :: Identity 。但是它确实依赖于调用者以一种允许ADL的方式来调用它,这就是为什么你从未显式调用 std :: swap 的原因。
那么问题来了,您希望库用户为何需要部分特化函数模板?如果他们要为类型进行特化(通常是算法模板的情况),请使用ADL。如果他们要为整数模板参数进行特化,就像您的示例一样,那么我想您必须委托给一个类。但是我通常不希望第三方定义3的乘法应该做什么 - 我的库将处理所有整数。我可以合理地期望第三方定义八元数的乘法应该做什么。
想想看,幂运算可能是我使用的更好的例子,因为我的 arithmetic :: mul operator * 非常相似,所以在我的示例中实际上没有必要特化 mul 。然后我会为第一个参数进行特化/ ADL重载,因为“任何东西的幂等于Identity”。希望您能理解这个想法。
我认为ADL有一个缺点-它有效地扁平化了命名空间。如果我想使用ADL为我的类“实现” arithmetic :: sub sandwich :: sub ,那么我可能会遇到麻烦。我不知道专家对此有何看法。
我的意思是:
namespace arithmetic {
    // subtraction, returns the difference of lhs and rhs
    template<typename T>
    const T sub(const T&lhs, const T&rhs) { return lhs - rhs; }
}

namespace sandwich {
    // sandwich factory, returns a baguette containing lhs and rhs
    template<typename SandwichFilling>
    const Baguette sub(const SandwichFilling&lhs, const SandwichFilling&rhs) { 
      // does something or other 
    }
}

现在,我有一个类型为ns::HeapOfHam。我想利用std::swap-style ADL来编写自己的arithmetic::sub实现:
namespace ns {
    HeapOfHam sub(const HeapOfHam &lhs, const HeapOfHam &rhs) {
        assert(lhs.size >= rhs.size && "No such thing as negative ham!");
        return HeapOfHam(lhs.size - rhs.size);
    }
}

我还想利用类似于std::swap的ADL来编写自己的sandwich::sub实现:
namespace ns {
    const sandwich::Baguette sub(const HeapOfHam &lhs, const HeapOfHam &rhs) {
        // create a baguette, and put *two* heaps of ham in it, more efficiently
        // than the default implementation could because of some special
        // property of heaps of ham.
    }
}

稍等一下。我不能这样做,对吧?两个不同命名空间中具有相同参数和不同返回类型的不同函数通常不是问题,这就是命名空间的作用。但我不能同时将它们都ADL化。可能我漏掉了什么非常明显的东西。
顺便说一下,在这种情况下,我可以完全专门化每个arithmetic :: sub和sandwich :: sub。调用者将使用其中一个,并获得正确的函数。原始问题涉及到部分专门化,所以我们能否假装专门化不是一种选择,而不必使HeapOfHam成为类模板?

@lit:完成了。如果你不知道答案,也许我会问一个问题。 - Steve Jessop
@litb:我会尝试提供一个更好的例子。假设在英语中,“swap”是有歧义的,除了表示“交换两个值”(std::swap)之外,它还表示“对两个对象执行卷积”(calculus::swap)。我将面临同样的问题,即无法在HeapOfHam上进行ADL重载。如果没有解决方案,那么除了std命名空间之外的其他命名空间不应该使用这种技巧来重载算法,以免名称冲突。这只能通过委托给类来实现,就像提问者的代码一样。 - Steve Jessop
或许工厂在使用 ADL(参考依赖查找)的技巧是通过传递一个 identity 对象,例如: sub(identity<KindOfSub>(), part1, part2),这样 ADL 将会在 KindOfSub 的命名空间和 part1、part2 的命名空间中搜索。因此对于工厂而言,第一个参数指定了要创建的类型。不同类型的 sub 可能有不同的三明治制作技巧,而第一个 identity 参数将使工厂与 part1 和 part2 的纯操作函数区分开来。 - Johannes Schaub - litb
思考了一下,这可能只是与成员函数名称的问题相同。如果Concept1定义了要求一个成员函数renuberate(void)的允许表达式,而Concept2也定义了一个做不同事情的成员函数renuberate(void),那么HeapOfHam就不可能同时实现这两个概念。如果HeapOfHam的作者从未听说过命名空间演算,那就更糟了。他实现了非成员的swap,意味着std::swap,但有人却使用了using calculus::swap; swap(heap,heap);,想要得到最坏的情况下calculus::swap的默认实现。错误的。 - Steve Jessop
所以,如果calculus定义了一个函数“swap”,并且期望使用这种技术的类型,那么显然是错误的,因为每个人都知道std::swap的存在并会发生冲突。但是,如果这个模糊的动词不在命名空间std中,而是在两个不相关的库中,这两个库都可以与我的类一起使用,那么我就陷入了困境。通过委托给一个类,我就不会陷入困境,我只需要实现两个类的部分特化即可。 - Steve Jessop
显示剩余3条评论

1
如果你正在编写一个库以供他人或其他人使用,请使用结构体/类的方式。这会增加一些代码量,但是你的库的用户(可能包括未来的你!)会感激你的。如果这只是一次性的代码,那么部分特化的损失对你来说并不会有太大影响。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接