Fluent C++:Mixin类——CRTP的阳面

原文

既然我们已经清楚了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
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
RepeatPrint repeatPrint(Printable const& printable)
{
    return RepeatPrint(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
struct RepeatPrint : Printable
{
    ...
};

repeatPrint(ned).repeat(10);

而相应的CRTP则看起来像这样:

template
struct RepeatPrint
{
   ...
};

class Name : public RepeatPrint
{
    ...
};

ned.repeat(10);

实际上,这是使用CRTP的解决方案的完整实现:

template
struct RepeatPrint
{
    void repeat(unsigned int n) const
    {
        while (n-- > 0)
        {
            static_cast(*this).print();
        }
    }
};

class Name : public RepeatPrint
{
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撰写了一个系列,该系列已变得很火。

你可能感兴趣的:(Fluent C++:Mixin类——CRTP的阳面)