Fluent C++:CRTP可以为你的代码带来什么

原文

在系列第一节中定义了CRTP的基础知识之后,现在让我们考虑一下CRTP如何在日常代码中提供帮助。

我不知道对你来说怎么样,但是最初几次我理解了CRTP的工作方式后,很快就忘记了,最后我再也记不清CRTP到底是什么了。 发生这种情况的原因是,很多关于CRTP定义的讲述就此止步,而没有向你展示CRTP可以为你的代码带来什么价值。

但是CRTP其实有几种很有用的用法。 在这里,我将介绍我在代码中最常看到的一个功能,即“添加功能”,另一个很有趣但又不经常遇到的功能:创建静态接口。

为了使代码示例更简短,我省略了第一节中的私有构造函数和模板友元技巧。 但是在实践中,你会发现这对防止将错误的类传递给CRTP模板很有用。

增加功能

有些类提供了通用功能,让许多其他类可以复用。

为了说明这一点,让我们以代表敏感度的类为例。 灵敏度是一种度量,用于量化如果给定输入要进行一定量的计算,则给定输出会受到多少影响。 这个概念与导数有关。 无论如何,如果你不(或不再)熟悉数学,请不要担心:以下内容不依赖于数学方面,该示例唯一要关注的是灵敏度有一个值。

class Sensitivity
{
public:
    double getValue() const;
    void setValue(double value);
    // 其他接口...
};

现在,我们要为此灵敏度添加辅助操作,例如缩放(将其乘以常数值),另外还有平方或将其设置为相反的值(一元减)。 我们可以在接口中添加相应的成员方法。 我意识到在这种情况下,将这些功能实现为非成员非友元函数会比较好,但是请先等一下,让我们将它们作为成员方法来实现,以说明以后的观点。 我们稍后再回来说明这一点。

class Sensitivity
{
public:
    double getValue() const;
    void setValue(double value);

    void scale(double multiplicator)
    {
        setValue(getValue() * multiplicator);
    }
    void square()
    {
        setValue(getValue() * getValue());
    }
    void setToOpposite()
    {
        scale(-1);
    };

    // 其他接口...
};

到目前为止,一切都很好。 但是,现在想象一下,我们还有另一个类,也有一个值,并且也需要上面的3个数值功能。 我们应该将这三个实现复制并粘贴到新类中吗?

到现在为止,我几乎可以听到你们中的一些人大喊使用模板非成员函数,该函数可以接受任何类并完成处理。 请稍等一下,我保证,我们等会儿会到说到这个的。

这就是CRTP发挥作用的地方。 在这里,我们可以将这三个数值函数分解为一个单独的类:

template <typename T>
struct NumericalFunctions
{
    void scale(double multiplicator);
    void square();
    void setToOpposite();
};

然后使用CRTP技术让Sensitivity来使用它:

class Sensitivity : public NumericalFunctions<Sensitivity>
{
public:
    double getValue() const;
    void setValue(double value);
    // 其他接口...
};

为此,3个数值方法的实现需要访问Sensitivity类中的getValue和setValue方法:

template <typename T>
struct NumericalFunctions
{
    void scale(double multiplicator)
    {
        T& underlying = static_cast<T&>(*this);
        underlying.setValue(underlying.getValue() * multiplicator);
    }
    void square()
    {
        T& underlying = static_cast<T&>(*this);
        underlying.setValue(underlying.getValue() * underlying.getValue());
    }
    void setToOpposite()
    {
        scale(-1);
    };
};

这样,我们通过使用CRTP将功能有效地添加到了初始Sensitivity类中。 并且可以使用相同的技术让其他类继承该类。

为什么不使用非成员模板功函数?

为什么不使用可以对其他任何类进行处理的模板非成员函数,包括Sensitivity和其他要做数值运算的类? 它们可能如下所示:

template <typename T>
void scale(T& object, double multiplicator)
{
    object.setValue(object.getValue() * multiplicator);
}

template <typename T>
void square(T& object)
{
    object.setValue(object.getValue() * object.getValue());
}

template <typename T>
void setToOpposite(T& object)
{
    object.scale(object, -1);
}

CRTP有什么大惊小怪的?

CRTP比非成员模板函数至少有一个好处:CRTP体现了接口。

使用CRTP,你可以看到Sensitivity提供了NumericFunctions的接口:

class Sensitivity : public NumericalFunctions<Sensitivity>
{
public:
    double getValue() const;
    void setValue(double value);
    // 其他接口...
};

用非成员模板函数,你就没有了这个好处。 它们将隐藏在某个地方的#include之后。

即使你知道这3个非成员函数的存在,也无法保证它们与特定的类兼容(也许它们调用get()或getData()而不是getValue()?)。 而使用CRTP的代码在编译器已经绑定了Sensitivity类,因此你知道它们具有兼容的接口。

现在谁是你的接口?

需要注意的有趣一点是,尽管CRTP使用继承,但它的用法与其他继承情况并不具有相同的含义。

通常,派生自另一个类的类表示派生类在某种程度上在概念上是“基类”。目的是在通用代码中使用基类,并将对基类的调用重定向到派生类中的代码。

对于CRTP,情况截然不同。派生类未表达其“是”基类的事实。相反,它通过继承基类来扩展其接口,以添加更多功能。在这种情况下,可以直接使用派生类,而从不使用基类(对于CRTP的这种用法是正确的,但对于下面要讲的静态接口则不是)。

因此,基类不是接口,派生类也不是实现。相反,这是另一回事:基类使用派生的类方法(例如getValue和setValue)。从这方面讲,派生类提供了基类使用的接口。这再次说明了一个事实,即CRTP上下文中的继承可以表示与经典继承完全不同的东西。

静态接口

正如Stak Overflow中这个答案所说,CRTP的第二种用法是创建静态接口。 在这种情况下,基类确实表示接口,派生的类确实表示实现,与多态性一样。 但是,与传统多态性的不同之处在于,不涉及任何virtual,并且所有调用都在编译期间解决。

下面是它的工作原理。

让我们考虑一个对Amount进行建模的CRTP基类,它有一个getValue方法:

template <typename T>
class Amount
{
public:
    double getValue() const
    {
        return static_cast<T const&>(*this).getValue();
    }
};

假设我们对此接口有两种实现:一种总是返回常量,而另一种可以设置其值。 这两个实现继承自CRTP Amount基类:

class Constant42 : public Amount<Constant42>
{
public:
    double getValue() const {return 42;}
};

class Variable : public Amount<Variable>
{
public:
    explicit Variable(int value) : value_(value) {}
    double getValue() const {return value_;}
private:
    int value_;
};

最后,让我们为该接口构建一个客户端,该客户端需要一个amount对象并将其值打印到控制台:

template<typename T>
void print(Amount<T> const& amount)
{
    std::cout << amount.getValue() << '\n';
}

可以使用以下两种实现之一调用该函数:

Constant42 c42;
print(c42);
Variable v(43);
print(v);

然后都运行正确:

42
43

需要注意的最重要的一点是,尽管Amount类是多态使用的,但是代码中没有任何virutal。这意味着多态调用已在编译时解决,从而避免了虚函数的运行时成本。有关这种对性能的影响的更多信息,请参见Eli Bendersky在其网站上所做的研究。

从设计的角度来看,我们能够避免此处的虚拟调用,因为要使用的类的信息在编译时就确定了。就像我们在编译期提取接口的重构方法中看到的那样,当你知道信息时,为什么要等到最后一刻才使用它?

编辑:正如u/quicknir在Reddit上指出的那样,该技术不是用于静态接口的最佳方法,而且不如即将到来的Concepts(C++20新特性。球球你们了,我真的学不动了)好。确实,CRTP强制从接口继承,而Concepts也指定对类型的要求,但不将它们与特定接口耦合。这允许独立的库一起工作。

下一步:如何在实践中简化CRTP的实现()。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,784评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,745评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,702评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,229评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,245评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,376评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,798评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,471评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,655评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,485评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,535评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,235评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,793评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,863评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,096评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,654评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,233评论 2 341

推荐阅读更多精彩内容