经常会碰到这样的需求,有一个类A,A的实例调用A的大部分函数都是一样的,只有小部分需要根据实例的具体情况展现出不同的表现。比如下面例子
class A
{
void foo(){}; // do sth
void foo(){}; // do sth else
//... A类的一些其他成员方法
}
A a1;
A a2;
现在我希望a1和a2对A的所有其他函数调用表现相同,而只有调用foo()时有不同的表现。我们当然可以直接把所有代码复制粘贴写第二个类,但是那样两个类将有大量相同的代码。
方法一
一种比较low的方法可能是给类搞一个类成员的bool,来控制代码执行区域
class A
{
public:
A(bool flag) : _flag(flag){};
void foo(){
if(_flag){
// do sth ...
}
else{
// do sth else...
}
};
//... A类的一些其他成员方法
privare:
bool _flag;
}
这么做可以实现我们的功能,但是代码在运行期才会确定要跑哪块,而且一看就很丑,不是production code的质量。
方法二
上面的写法实在太丑,有没有什么让它变好看一点的办法呢。也许你已经想到了用模板和函数重载来解决
template<bool B>
class A
{
public:
void foo(){ foo(std::bool_constant<B>()) }; // function selection
//... A类的一些其他成员方法
private:
void foo(std::true_type){ }; // do sth
void foo(std::false_type){ }; // do sth else
}
这里相当于留一个对外的接口来调用函数,然后利用函数重载,根据std::bool_constant<>()的值来决定执行的对象。但是这样要求我们一个函数写三次。
方法三
有没有什么更好的方法呢?答案是用模板元编程。具体来讲是用std::enable_if。
std::enable_if是C++11里的东西,简单来说它可以对模板参数进行判别,根据判别结果来决定函数的选择。
std::enable_if的源码非常简单,就四行。它本质上就是一个struct,其中第一个就是普通的模板结构体定义,而第二个定义是对第一个结构体定义的偏特例化重载。std::enable_if接受两个模板参数(其实一个也可以,第二个模板参数默认是void,可以看源码),其中第一个是一个boolean。构造时当第一个模板参数为false时没有特例化,实例化为第一个结构体,而为true时则特例化为第二个结构体。两个结构体相差了一个type的定义。enable_if<true, T>::type成立,且为类型T,而enable_if<flase, T>::type未定义,因此不存在。
template<bool B, class T>
struct enable_if {};
template<class T>
struct enable_if<true, T> { using type = T; };
更多关于enable_if和偏特例化的信息,有一篇很好的帖子,可以自行查看: https://medium.com/@sidbhasin82/c-templates-what-is-std-enable-if-and-how-to-use-it-fd76d3abbabe
再回到最初的问题上来。有了std::enable_if,事情就可以放到编译期去完成了。
template<bool _B>
class A
{
template<bool B = _B>
typename std::enable_if<B, void>::type foo(){}; // do sth
template<bool B = _B>
typename std::enable_if<!B, void>::type foo(){}; // do sth else
//... A类的一些其他成员方法
}
A<true> a1;
A<false> a2;
在编译实例的foo()方法的时候,enable_if将根据模板参数B是true还是false来确定是否有enable_if::type的定义,如果有就可以正常执行函数,如果没有就会把根据SFINAE机制把这块内容抛弃掉。
下面是一个mini demo
template <bool _B>
class A
{
public:
template <bool B = _B>
A(typename enable_if<B>::type* = 0)
{
cout << "first constructor" << endl;
};
template <bool B = _B>
A(typename enable_if<!B>::type* = 0)
{
cout << "second constructor" << endl;
};
template <bool B = _B>
typename std::enable_if<B, void>::type foo()
{
cout << "first foo()" << endl;
}
template <bool B = _B>
typename std::enable_if<!B, void>::type foo()
{
cout << "second foo()" << endl;
}
};
int main()
{
A<true> a1{}; // first constructor
a1.foo(); // first foo()
A<false> a2{}; // second constructor
a2.foo(); // second foo()
return 0;
}