为什么我似乎可以为函数模板定义部分特化?

119
我知道下面的代码是一个类的部分特化。
template <typename T1, typename T2> 
class MyClass { 
  … 
}; 


// partial specialization: both template parameters have same type 
template <typename T> 
class MyClass<T,T> { 
  … 
}; 

我知道C++不允许函数模板的部分特化(只允许完全特化)。但是我的代码是否意味着我对同一类型的参数进行了部分特化?因为它在Microsoft Visual Studio 2010 Express中可以工作!如果不是这样,请您解释一下部分特化的概念。
#include <iostream>
using std::cin;
using std::cout;
using std::endl;

template <typename T1, typename T2> 
inline T1 max (T1 const& a, T2 const& b) 
{ 
    return a < b ? b : a; 
} 

template <typename T> 
inline T const& max (T const& a, T const& b)
{
    return 10;
}


int main ()
{
    cout << max(4,4.2) << endl;
    cout << max(5,5) << endl;
    int z;
    cin>>z;
}

寻找类专门化的类比。如果它被称为类专门化,那么为什么我应该将函数的同一概念视为重载呢? - Narek
1
不,专业化语法是不同的。请查看下面我回答中所述的(假定的)函数专业化语法。 - iammilind
2
为什么这不会抛出“调用max是模棱两可的”错误?max(5,5)如何解析为max(T const&,T const&) [with T = int]而不是max(T1 const&,T2 const&) [with T1 = int and T2 = int] - NHDaly
7个回答

104

根据标准,目前不允许函数偏特化。在这个例子中,你实际上是重载而不是特化max<T1,T2>函数。
如果允许的话,它的语法应该像下面这样:

// Partial specialization is not allowed by the spec, though!
template <typename T> 
inline T const& max<T,T> (T const& a, T const& b)
{            //    ^^^^^ <--- supposed specializing here as an example
  return a; // can be anything of type T
}

在函数模板的情况下,C++标准仅允许进行完全特化。
虽然有一些编译器扩展可以允许部分特化,但在这种情况下,代码将失去可移植性!

1
@Narek,部分函数特化不是标准的一部分(出于任何原因)。我认为MSVC支持它作为扩展。也许过段时间,其他编译器也会允许它。 - iammilind
1
@iammilind:没问题。他似乎已经知道了。这就是为什么他也在尝试函数模板的原因。所以我再次进行了编辑,现在更加清晰明了。 - Nawaz
47
谁能解释为什么不允许部分特化? - HelloGoodbye
2
@NHDaly,它不会产生歧义错误,因为一个函数比另一个函数更匹配。为什么选择(T, T)而不是(T1, T2)对于(int, int),是因为前者保证有两个参数且类型相同;后者只保证有两个参数。编译器总是选择准确的描述。例如,如果你必须在“河流”的两个描述之间做出选择,你会选择哪一个?“水的集合”还是“流动的水的集合”? - iammilind
2
@kfsone,我认为这个功能正在审核中,因此可以进行解释。您可以参考这个开放的标准部分,我在为什么C++标准不允许函数模板部分特化?中看到了它。 - iammilind
显示剩余4条评论

57

由于不允许部分特化 - 如其他答案所指出 -,您可以使用以下方法绕过此问题:std::is_samestd::enable_if

template <typename T, class F>
inline typename std::enable_if<std::is_same<T, int>::value, void>::type
typed_foo(const F& f) {
    std::cout << ">>> messing with ints! " << f << std::endl;
}

template <typename T, class F>
inline typename std::enable_if<std::is_same<T, float>::value, void>::type
typed_foo(const F& f) {
    std::cout << ">>> messing with floats! " << f << std::endl;
}

int main(int argc, char *argv[]) {
    typed_foo<int>("works");
    typed_foo<float>(2);
}

输出:

$ ./a.out 
>>> messing with ints! works
>>> messing with floats! 2
编辑:如果您需要能够处理所有剩余的情况,您可以添加一个定义,指出已经处理过的情况不应该匹配 -- 否则您会陷入模糊的定义。该定义可以是:
template <typename T, class F>
inline typename std::enable_if<(not std::is_same<T, int>::value)
    and (not std::is_same<T, float>::value), void>::type
typed_foo(const F& f) {
    std::cout << ">>> messing with unknown stuff! " << f << std::endl;
}

int main(int argc, char *argv[]) {
    typed_foo<int>("works");
    typed_foo<float>(2);
    typed_foo<std::string>("either");
}

这将产生:

$ ./a.out 
>>> messing with ints! works
>>> messing with floats! 2
>>> messing with unknown stuff! either

尽管这个“全案例”看起来有点乏味,因为你必须告诉编译器你已经做了什么,但是处理多达5个或更多专业化还是可行的。

实际上没有必要这样做,因为函数重载可以更简单、更清晰地处理这个问题。 - Adrian
3
@Adrian,我真的想不出其他的函数重载方法来解决这个问题了。你注意到部分重载是不允许的,对吧?如果你认为你有更清晰的解决方案,请与我们分享。 - Rubens
1
有没有其他更容易的方法来捕获所有模板函数? - Nick
1
@Adrian 在某些情况下,确实可以重新定义 typed_foo,使其只需要一个模板参数而不是两个,然后像你说的那样使用重载。但是,这并不是 OP 所问的问题。此外,我不确定您是否可以仅使用重载来创建 catch-all 函数。另外,您可能希望您的 catch-all 实现在使用时会导致编译错误,这只有在模板函数中才可能,其中模板相关行将导致编译器发出错误。 - adentinger

18

什么是特化?

如果你真的想了解模板,你应该看一下函数式语言。C++中的模板世界是一个纯粹的自己的函数子语言。

在函数式语言中,选择使用模式匹配(Pattern Matching)

-- An instance of Maybe is either nothing (None) or something (Just a)
-- where a is any type
data Maybe a = None | Just a

-- declare function isJust, which takes a Maybe
-- and checks whether it's None or Just
isJust :: Maybe a -> Bool

-- definition: two cases (_ is a wildcard)
isJust None = False
isJust Just _ = True

正如你所看到的,我们重载isJust的定义。

好吧,C++类模板的工作方式完全相同。您提供一个主要声明,指定参数的数量和性质。它可以只是一个声明,也可以充当定义(由您决定),然后您可以(如果希望)提供模式的特殊化,并将其与不同的(否则将很愚蠢的)类版本相关联。

对于模板函数,特化有些棘手:它在某种程度上与重载分辨率冲突。因此,已经决定特化将与非特化版本相关联,而在重载分辨率期间不会考虑特化。因此,选择正确函数的算法变为:

  1. 在常规函数和非特化模板之间进行重载分辨
  2. 如果选择了一个非特化模板,请检查是否存在更好匹配的特化

(有关详细处理,请参见GotW#49

因此,函数模板的特化是“二等公民”(字面上)。就我而言,我们最好没有它们:我还没有遇到过使用函数模板特化无法通过重载解决的情况。

这是一个函数模板特化吗?

不,这只是一个重载,这很好。实际上,重载通常按我们期望的方式工作,而特化可能会令人惊讶(请记住我链接的GotW文章)。


因此,函数模板的特化是一个次要的公民(字面上)。就我而言,如果没有它们,我们会更好:我还没有遇到过使用函数模板特化无法用重载解决的情况。那么对于非类型模板参数呢? - Jules G.M.
@Julius:你仍然可以使用重载,只需引入一个虚拟参数,例如 boost::mpl::integral_c<unsigned, 3u>。另一种解决方案也可以是使用 enable_if/disable_if,但这是另外一个故事了。 - Matthieu M.

12

非类、非变量的部分特化是不允许的,但正如 David Wheeler 所说:

计算机科学中的所有问题都可以通过引入另一层间接性来解决。

添加一个转发函数调用的类可以解决这个问题,以下是一个例子:

template <class Tag, class R, class... Ts>
struct enable_fun_partial_spec;

struct fun_tag {};

template <class R, class... Ts>
constexpr R fun(Ts&&... ts) {
  return enable_fun_partial_spec<fun_tag, R, Ts...>::call(
      std::forward<Ts>(ts)...);
}

template <class R, class... Ts>
struct enable_fun_partial_spec<fun_tag, R, Ts...> {
  constexpr static R call(Ts&&... ts) { return {0}; }
};

template <class R, class T>
struct enable_fun_partial_spec<fun_tag, R, T, T> {
  constexpr static R call(T, T) { return {1}; }
};

template <class R>
struct enable_fun_partial_spec<fun_tag, R, int, int> {
  constexpr static R call(int, int) { return {2}; }
};

template <class R>
struct enable_fun_partial_spec<fun_tag, R, int, char> {
  constexpr static R call(int, char) { return {3}; }
};

template <class R, class T2>
struct enable_fun_partial_spec<fun_tag, R, char, T2> {
  constexpr static R call(char, T2) { return {4}; }
};

static_assert(std::is_same_v<decltype(fun<int>(1, 1)), int>, "");
static_assert(fun<int>(1, 1) == 2, "");

static_assert(std::is_same_v<decltype(fun<char>(1, 1)), char>, "");
static_assert(fun<char>(1, 1) == 2, "");

static_assert(std::is_same_v<decltype(fun<long>(1L, 1L)), long>, "");
static_assert(fun<long>(1L, 1L) == 1, "");

static_assert(std::is_same_v<decltype(fun<double>(1L, 1L)), double>, "");
static_assert(fun<double>(1L, 1L) == 1, "");

static_assert(std::is_same_v<decltype(fun<int>(1u, 1)), int>, "");
static_assert(fun<int>(1u, 1) == 0, "");

static_assert(std::is_same_v<decltype(fun<char>(1, 'c')), char>, "");
static_assert(fun<char>(1, 'c') == 3, "");

static_assert(std::is_same_v<decltype(fun<unsigned>('c', 1)), unsigned>, "");
static_assert(fun<unsigned>('c', 1) == 4, "");

static_assert(std::is_same_v<decltype(fun<unsigned>(10.0, 1)), unsigned>, "");
static_assert(fun<unsigned>(10.0, 1) == 0, "");

static_assert(
    std::is_same_v<decltype(fun<double>(1, 2, 3, 'a', "bbb")), double>, "");
static_assert(fun<double>(1, 2, 3, 'a', "bbb") == 0, "");

static_assert(std::is_same_v<decltype(fun<unsigned>()), unsigned>, "");
static_assert(fun<unsigned>() == 0, "");

我不想听到我的疯狂想法行不通,也不想听到标准如此糟糕之类的话。我希望听到的是只要付出足够的努力,它就会奏效。 - dEmigOd

5

非常抱歉回答晚了,但我找到了一个解决方案,其他答案中并没有直接解释。

函数无法进行部分特化,但类可以。

这里的解决方法是在类内部创建一个静态函数。我们可以通过将“模板部分特化”移动到类特化内部,并在其中创建标记为静态的函数,从而使其基本上可行。这将允许我们通过适当增加所需代码的行数来构建我们的部分特化函数。

考虑以下不可用的部分特化函数Printer根本无法编译的代码)。

template <class T, class Trait = void>
void Printer(const T&);

template <class T>
void Printer<T, std::enable_if_t<std::is_floating_point_v<T>>>(const T& v){
    std::cout << "I m partially specialized for any floating point type." << std::endl;
}
template <class T>
void Printer<T, std::enable_if_t<std::is_integral_v<T>>>(const T& v){
    std::cout << "I m partially specialized for any integral type." << std::endl;
}

我们可以使用静态类函数,并将部分特化移动到类上,像这样使其工作:
namespace detail{
    
    template<class T, class Trait = void>
    struct Specialized;

    template<class T>
    struct Specialized<T, std::enable_if_t<std::is_floating_point_v<T>>>
    {
        static void Printer(const T& v){
            std::cout << "I m specialized for any floating point type"<< std::endl;
        }
    };

    template<class T>
    struct Specialized<T, std::enable_if_t<std::is_integral_v<T>>>
    {
        static void Printer(const T& v){
            std::cout << "I m specialized for any integral type"<< std::endl;
        }
    };
}


template<class T>
void Printer(const T& v)
{
    detail::Specialized<T>::Printer(v);   
}

这样做会有点长,但可以以相对清晰的方式解决我们的问题。您可以在godbolt 这里 进行测试。

------ 编辑:感谢KROy 的提示

只需将两个静态函数包装在一个结构体中,保留它们的模板特化,就可以使代码变得更短:

namespace detail{
    struct Specialized{
        template<class T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
        static void Printer(const T& v){
            std::cout << "I'm specialized for integral types." << std::endl;
        }

        template<class T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
        static void Printer(const T& v){
            std::cout << "I'm specialized for floating point types." << std::endl;
        }
    };
}

template<class T>
void Printer(const T& v)
{
    detail::Specialized::Printer(v);   
}

可以在 Godbolt 这里 进行测试。


只需将函数包装在一个结构体中,它就可以工作了。查看std::Optional如何调用dtor。 - KRoy
@KRoy感谢您的提示,我已经调整了答案。 - IkarusDeveloper

4
不行。例如,您可以合法地专门化std::swap,但您不能合法地定义自己的重载。这意味着您无法使std::swap适用于自己的自定义类模板。
在某些情况下,重载和部分特化可能具有相同的效果,但远非全部情况。

4
这就是为什么你要将swap重载函数放在你的命名空间中。 - jpalecek

2
晚回答了,但是一些晚来的读者可能会发现这很有用:有时候,一个辅助函数 - 设计成可以被专门化 - 也可以解决问题。
所以让我们想象一下,这就是我们试图解决的问题:
template <typename R, typename X, typename Y>
void function(X x, Y y)
{
    R* r = new R(x);
    f(r, y); // another template function?
}

// for some reason, we NEED the specialization:
template <typename R, typename Y>
void function<R, int, Y>(int x, Y y) 
{
    // unfortunately, Wrapper has no constructor accepting int:
    Wrapper* w = new Wrapper();
    w->setValue(x);
    f(w, y);
}

好的,部分模板函数特化,我们无法做到这一点...因此让我们将需要特化的部分“导出”到一个辅助函数中,对其进行特化并使用它:

template <typename R, typename T>
R* create(T t)
{
    return new R(t);
}
template <>
Wrapper* create<Wrapper, int>(int n) // fully specialized now -> legal...
{
    Wrapper* w = new Wrapper();
    w->setValue(n);
    return w;
}

template <typename R, typename X, typename Y>
void function(X x, Y y)
{
    R* r = create<R>(x);
    f(r, y); // another template function?
}

可能会 很有趣,尤其是如果替代方案(而不是特化,例如普通重载,Rubens提出的解决方法,…这些并不是不好或我的更好,只是 另一种)将共享相当多的公共代码。


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