既然我们已经清楚了CRTP的工作原理,那么让我与你分享另一种涉及模板的技术,该模板是CRTP的补充:Mixin类。
我发现Mixin类很有趣,因为它们为CRTP提供了另一种实现等效的方法,因此提供了不同的权衡。
CRTP的主要用途是为特定类添加通用功能。 Mixin类也这样做。
Mixin类是定义通用行为的模板类,通过继承你希望扩展其功能的类型来实现。
这儿有一个例子。 让我们上一个代表一个人的名字的类。 它具有名字和姓氏,并且可以使用特定格式打印出该名字:
class Name
{
public:
Name(std::string firstName, std::string lastName)
: firstName_(std::move(firstName))
, lastName_(std::move(lastName)) {}
void print() const
{
std::cout << lastName_ << ", " << firstName_ << '\n';
}
private:
std::string firstName_;
std::string lastName_;
};
这儿是使用它的代码段:
Name ned("Eddard", "Stark");
ned.print();
这会输出:
Stark, Eddard
到目前为止,还没有什么特别的,但是这儿有一个新的需求:我们需要能够连续多次打印此名称。
我们可以向Name类添加一个repeat方法。 但是,重复调用print方法的概念也可以应用于其他类,例如PhoneNumber类,也可以具有print()方法。
mixin类的想法是将通用功能隔离到其自己的类中,使用要增加该功能的类型对该类进行模板化,并从该类型派生:
template<typename Printable>
struct RepeatPrint : Printable
{
explicit RepeatPrint(Printable const& printable) : Printable(printable) {}
void repeat(unsigned int n) const
{
while (n-- > 0)
{
this->print();
}
}
};
在我们的示例中,Name类将扮演Printable的角色。
注意repeat方法的实现中的this->。 没有它,代码将无法编译。 确实,编译器不确定在哪里声明的print:即使在模板类Printable中声明了它,从理论上讲,也无法保证该模板类不会被特化并针对特定类型进行重写,从而不会公开print方法 。 因此,C ++中会忽略模板基类中的名称。
使用this->是将它们重新包含在调用它们的函数范围内的一种方法。 还有其他方法也可以做到,尽管它们可能并不适合这种情况。 无论如何,你都可以在Effective C++ 的第43条中阅读有关此主题的所有信息。
为了避免显式指定模板参数,我们使用一个推导它们的函数:
template<typename Printable>
RepeatPrint<Printable> repeatPrint(Printable const& printable)
{
return RepeatPrint<Printable>(printable);
}
然后这儿是我们的客户端代码:
Name ned("Eddard", "Stark");
repeatPrint(ned).repeat(10);
输出就变成了:
Stark, Eddard
Stark, Eddard
Stark, Eddard
Stark, Eddard
Stark, Eddard
Stark, Eddard
Stark, Eddard
Stark, Eddard
Stark, Eddard
Stark, Eddard
我们甚至可以改个名字来让代码更有表现力:
Name ned("Eddard", "Stark");
repeatedlyPrint(ned).times(10);
(我在这里改名称,只是为了跟之前的CRTP代码进行比较,在那里这些新名称并不适用。)
CRTP的反面
Mixin类涉及模板和继承的混合,以便将通用功能插入现有类。 这种感觉就像CRTP,不是吗?
Mixin类类似于CRTP,但是是反过来的。 实际上,我们的mixin类如下所示:
class Name
{
...
};
template<typename Printable>
struct RepeatPrint : Printable
{
...
};
repeatPrint(ned).repeat(10);
而相应的CRTP则看起来像这样:
template<typename Printable>
struct RepeatPrint
{
...
};
class Name : public RepeatPrint<Name>
{
...
};
ned.repeat(10);
实际上,这是使用CRTP的解决方案的完整实现:
template<typename Printable>
struct RepeatPrint
{
void repeat(unsigned int n) const
{
while (n-- > 0)
{
static_cast<Printable const&>(*this).print();
}
}
};
class Name : public RepeatPrint<Name>
{
public:
Name(std::string firstName, std::string lastName)
: firstName_(std::move(firstName))
, lastName_(std::move(lastName)) {}
void print() const
{
std::cout << lastName_ << ", " << firstName_ << '\n';
}
private:
std::string firstName_;
std::string lastName_;
};
int main()
{
Name ned("Eddard", "Stark");
ned.repeat(10);
}
那么,CRTP还是mixin类?
CRTP和mixin类提供了解决同一问题的两种方法:向现有类添加通用功能,但要权衡取舍。
以下是它们之间的不同点:
CRTP:
- 影响现有类的定义,因为它必须继承自CRTP,
- 客户代码直接使用原始类,并从其扩展的功能中受益。
mixin类:
- 保持原始类不变,
- 客户代码不会直接使用原始类,而是需要将其包装到mixin中才能使用扩展功能,
- 即使没有虚析构函数,它也会从原始类继承。你可以这么干,除非要通过指向原始类的指针多态删除mixin类。
了解这些权衡之后,你可以选择最适合给定情况的解决方案。
CRTP不仅限于此。如果你想了解更多信息,我已经为CRTP撰写了一个系列,该系列已变得很火。