美文网首页Fluent C++
Fluent C++:CRTP可以为你的代码带来什么

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

作者: sunix | 来源:发表于2020-03-21 19:31 被阅读0次

    原文

    在系列第一节中定义了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的实现()。

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/fdvuyhtx.html