带有模板参数的模板专业化

Template Specialisation with Template Argument

假设有一个 templateclass Foo:

template <typename T>
class Foo {
  void foo();
};

我还有一个templateclassBar(独立于第一个):

template <int N>
class Bar {};

比方说,我想针对任何 Bar class 专门化 foo() 方法。 我会错误地写:

template <>
template <int N>
void Foo<Bar<N> >::foo() { /* ... */ }

编译器指责我类型不完整:

error: invalid use of incomplete type 'class Foo<Bar<N> >'
 void Foo<Bar<N> >::foo() { }

Code

我正在使用 C++98,但我想知道 C++11 中是否存在不同的解决方案.


备注

我可以解决将整个 class Foo 专门化为泛型 Bar 的问题,但之后我必须定义所有方法。

Example Code

这不是我想要的,我正在寻找(如果存在)更优雅的解决方案(C++98 和 C++11),它允许我专门化并仅实现一个 class 方法.


编辑:

没有解释如何专门化模板参数。事实上,我的问题显示了编译器是如何抱怨的。

对于 C++11,您可以 SFINAE enable/disable(使用 std::enable_if)两个不同版本的 foo() 在一个未专门化的 Foo class 中。

在 C++98 中你没有 std::enable_if 但你可以模拟它(给我一些时间,我尝试提出一个例子)。 抱歉: 我的想法行不通,因为此方法需要为 C++11 创新的方法使用默认模板参数。

另一种方法是为Foo()定义一个模板基础class,比如FooBase,在[=16中插入foo()(并且只有foo()) =] 并专注于 FooBase.

另一种方法,也适用于 C++98,可以是标签分派:您可以定义一个唯一的 foo(),参数为零,调用另一个 foo(),参数为由 T.

决定

以下是一个完整的(C++98 可编译)示例

#include <iostream>

struct barWay   {};
struct noBarWay {};

template <int>
struct Bar
 { };

template <typename>
struct selectType
 { typedef noBarWay type; };

template <int N>
struct selectType< Bar<N> >
 { typedef barWay type; };

template <typename T>
struct Foo
 {
   void foo (noBarWay const &)
    { std::cout << "not Bar version" << std::endl; }

   void foo (barWay const &)
    { std::cout << "Bar version" << std::endl; }

   void foo ()
    { foo(typename selectType<T>::type()); }
 };


int main ()
 {
   Foo<int>        fi;
   Foo< Bar<42> >  fb;

   fi.foo();
   fb.foo(); 
 }

如果一个共同的基础是不可取的,还有另一种方法可以给 foo() 一个定制点,比如一个特征:

template <typename T>
struct foo_traits;

template <typename T>
struct Foo {
  void foo(){ foo_traits<T>::foo_cp(*this); }
};

template <typename T>
struct foo_traits{ static void foo_cp(T&){/*default*/} };

template <int N>
class Bar {};

template <int N>
struct foo_traits<Bar<N>>{ static void foo_cp(Foo<Bar<N>>&){/*spec*/} };

如果它的唯一目的是在内部为 Bar 提供 foo() 专业化,这样的特性也可以成为实现细节的朋友。

如果您不能专门化 foo,请对其进行定义,以便将调用委托给内部 foo-实现 class。然后专门化 class.
像这样的东西应该在 C++98 中编译,它与你的原始代码没有太大区别:

template <typename T>
class Foo {
    template<typename>
    struct FooImpl;

public:
    void foo() { FooImpl<T>()(); }
};

template <int N>
class Bar {};

template <typename T>
template <int N>
struct Foo<T>::FooImpl< Bar<N> > {
    void operator()() { /* ... */ }
};

int main() {
    Foo< Bar<0> > fb;
    fb.foo();

    Foo<int> fi;
    //fi.foo();
}

最后一行没有按预期编译(至少我得到了预期的结果,否则只需为 FooImpl 定义函数调用运算符)。

通过这种方式,您可以有选择地定义您希望 foo 工作的专业化。在所有其他情况下,尝试使用 foo 将导致编译错误。

I'd like to know if there exist different solutions in C++11.

这是标记调度的经典用例,其中 已被推荐。 C++98 和 C++11 中的方法,甚至语法基本相同。

我认为这里有一些比 max66 更简洁的实现 (running on godbolt):

template <class T>
class Foo {
    template <class>
    struct tag{};
    template<class U>
    void foo_helper(tag<U>){std::cout << "default\n";}
    void foo_helper(tag<Bar<3> >){std::cout << "specialization for Bar<3>\n";}
public:
    void foo(){return foo_helper(tag<T>());}
};

原理是一样的;不接受任何参数的客户端函数调用一个辅助函数,该函数根据 T 参数构造一个空类型。然后正常的重载会处理剩下的事情。

只有在这里我使用了模板化的包罗万象的方法。


在 C++11 中,语法只会有轻微的变化;我们可以说 tag<Bar<3>> 而不是 tag<Bar<3> > 因为新的解析规则允许嵌套模板使用人字形。

我们还可以将标签和模板化的 foo_helper 包罗万象 variadic templates 变得更加通用:

template <class T>
class Foo {
    template <class...>
    struct tag{};
    template<class... U>
    void foo_helper(tag<U...>){std::cout << "default\n";}
    void foo_helper(tag<Bar<3>>){std::cout << "specialization for Bar<3>\n";}
public:
    void foo(){return foo_helper(tag<T>{});}
};

随着 constexpr if that allows us to write what looks like normal branching logic based on T (Live Demo 的引入,C++17 中的事情实际上开始变得非常有趣:

template <class T>
class Foo {
public:
    void foo(){
        if constexpr (std::is_same_v<T, Bar<3>>){std::cout << "Specialization for Bar<3>\n";}
        else std::cout << "default\n";
    }
};

如您所见,所有标签内容都消失了,取而代之的是使用简单的 if 语句。

我们利用 C++11 中引入的 type_traits 来根据我们想要的类型检查 T 的类型。像这样的东西以前不一定有效,因为所有分支都需要编译。在 C++17 中,仅编译选定的分支(在编译时)。

请注意,早在 C++98 中,您就可以通过使用 typeid (godbolt demo):

来模拟此行为
void foo(){
    if (typeid(T) == typeid(Bar<3>)){std::cout << "Specialization for Bar<3>\n";}
    else std::cout << "default\n";
}

但是,typeid 方法是一个糟糕的选择,原因有两个:

  1. 这是一个运行时间检查(慢)我们在编译时知道的信息
  2. 它很脆弱,因为所有分支都必须针对所有模板实例进行编译,而在 C++17 中 if constexpr 仅编译选定的分支。