【问题标题】:Replacing non-pure virtual functions with CRTP用 CRTP 替换非纯虚函数
【发布时间】:2016-07-16 05:20:31
【问题描述】:

我正在通过其 C++ SDK 为应用程序编写插件。机制相当简单。插件通过预定义的接口提供其功能。这是通过让服务器类从每个接口的一个实现类继承来完成的,该实现类包含纯虚拟函数或具有默认实现的非纯函数。
这是非常实用的,因为 SDK 客户端只需覆盖插件所需的那些方法和/或为(罕见的)那些没有默认值的方法提供实现。

困扰我的是,在编译时一切都是已知的。与运行时多态性相关的虚函数表和机器在这里只是为了提供默认实现。
我试图在保持便利的同时消除这种开销。

作为一个(非常人为的)示例,假设我有几个服务器提供一个接口(名为 Blah),该接口仅由一个方法组成,没有默认实现。

// SDK header
struct OldImpl_Blah {
    virtual ~OldImpl_Blah() =default;
    virtual int mult(int) =0;
};

// plugin source
class OldServer3 : public OldImpl_Blah {
public:
    int mult(int i) override { return 3 * i; }
};

class OldServer5 : public OldImpl_Blah {
public:
    int mult(int i) override { return 5 * i; }
};

对于纯虚函数,直接 CRTP 就可以了。

// SDK header
template <typename T>
struct NewImpl_Blah {
    int mult(int i) { return static_cast<T*>(this)->mult(i); }
};

// plugin source
class NewServer3 : public NewImpl_Blah<NewServer3> {
public:
    int mult(int i) { return 3 * i; }
};

class NewServer5 : public NewImpl_Blah<NewServer5> {
public:
    int mult(int i) { return 5 * i; }
};

问题在于纯虚函数,即当方法有默认实现时。

// SDK header
struct OldImpl_Blah {
    virtual ~OldImpl_Blah() =default;
    virtual int mult(int i) { return i; }    // default
};

// plugin source
class OldServer3 : public OldImpl_Blah {
public:
    int mult(int i) override { return 3 * i; }
};

class OldServer5 : public OldImpl_Blah {
public:
    int mult(int i) override { return 5 * i; }
};

我尝试将 CRTP 与一些表达式 SFINAE 技巧结合起来,但失败了。
我想我需要的是某种代码调度,其中基类要么提供默认实现,要么将其参数转发给派生类中的实现(如果存在)。
问题似乎是调度应该依赖于基类中编译器尚不可用的信息。

一个简单的解决方案是删除代码中的virtualoverride 关键字。但是编译器不会检查函数签名是否匹配。
这种情况有一些众所周知的模式吗?我要问的有可能吗?

(请使用小字,因为我在模板方面的专业知识有点轻。谢谢。)

【问题讨论】:

  • 你可以在什么版本的C++下编译?
  • “编译器不会检查函数签名是否匹配”。您能否通过提供代码示例来说明这可能会产生哪些不良影响?
  • @AndyG:我正在使用 VS 2015 更新 1。那会是 C++14 吗?
  • @n.m.:我不确定我是否理解你的问题。例如,如果用户在名称中打错字,它将创建一个新函数。
  • CRTP 没有等效的“override”关键字(表示希望隐藏具有相同签名的基类的非虚拟成员函数)。你问的是这个吗?

标签: c++ inheritance virtual crtp


【解决方案1】:

与往常一样,另一种间​​接层级是解决方案。在这种特殊情况下,公共非虚拟函数调用私有或受保护的虚拟函数是众所周知的技术。它有自己的用途,与这里讨论的内容无关,所以无论如何都要检查一下。通常它是这样工作的:

struct OldImpl_Blah {
piblic:
    virtual ~OldImpl_Blah() = default;
    int mult(int i) { return mult_impl(i); }
protected:
    virtual int mult_impl(int i) { return i; }
};

// plugin source
class OldServer3 : public OldImpl_Blah {
protected:
    int mult_impl(int i) override { return 3 * i; }
};

CRTP 完全一样:

template <class T>
struct OldImpl_Blah {
piblic:
    virtual ~OldImpl_Blah() = default;
    int mult(int i) { return static_cast<T*>(this)->mult_impl(i); }
protected:
    virtual int mult_impl(int i) { return i; }
};

// plugin source
class OldServer3 : public OldImpl_Blah<OldServer3> {
protected:
    int mult_impl(int i) override { return 3 * i; }
};

免责声明:据说 CRTP 通过 nit 要求函数为 virtual 来消除虚拟调用开销。我不知道 CRTP 在保留函数时是否有任何性能优势virtual

【讨论】:

  • 我相信,OP 正试图完全避免虚拟通话。在我看来,将 CRTP 与虚拟通话相结合是一种可恶的做法。
  • @SergeyA 我相信 OP 希望保持功能虚拟,但相信 CRTP 将消除开销。我不知道是否是这种情况(或者是否有任何开销值得消除,但这是另一回事)。如果删除所有virtual 关键字,上述技术同样有效。
  • @n.m.:感谢您的回答,但不,我不想保留虚拟功能。从概念上讲,不需要它们,因为服务器类型的对象不是通过通用的基本类型来操作的。正如我所说,它们仅用于提供默认实现。
  • 所以只要去掉上面实现中的virtual关键字即可。
  • @Garp n.m.是对的。即使我尝试解决它很有趣(请参阅我的答案),如果您不打算通过基类使用它们,您可以简单地删除 virtual 关键字。让派生类中的方法隐藏基类中定义的方法,仅此而已。
【解决方案2】:

考虑使用策略设计之类的东西:

struct DefaultMult {
    int mult(int i) { return i; }
};

// SDK header
template <typename MultPolicy = DefaultMult>
struct NewImpl_Blah {
    int mult(int i) { return multPolicy.mult(i); }
  private:
    MultPolicy multPolicy;
};

// plugin source
class NewServer3 {
public:
    int mult(int i) { return 3 * i; }
};

class NewServer5 {
public:
    int mult(int i) { return 5 * i; }
};

void client() {
  NewImpl_Blah<NewServer5> myServer;
}

还请注意,理论上使用 final 关键字和 override 可以使编译器比 vtable 方法更优化地调度。如果您在第一个实现中使用 final 关键字,我希望现代编译器能够进行优化。

一些有用的参考:

  • mixin design
  • 有关基于策略的设计的更多信息,您可以观看视频或阅读 Andrei Alexandrescu 的书籍/文章

【讨论】:

  • 请注意,问题的标题是with CRTP
  • 是的,确实如此,但如果动机是跳过 vtable 开销并可以选择使用不同的实现以及默认实现,那么策略设计比 CRTP 更适合。恕我直言,对于大多数这些任务,策略设计可以是一个更清洁的解决方案。
  • 尽管标题说了什么,但我并不热衷于 CRTP。这个想法是让慢速 SDK 运行得更快(它适用于插件可能必须处理数百万个对象的集合的 3D 应用程序)。我不熟悉政策设计。恐怕 SDK 的客户需要做更多的工作。这绝对很有趣,我会更多地研究它。
  • @Garp 好吧,如果您正在寻找不应将 CRTP 作为要求的通用解决方案,我建议您更改创建新问题的标题。
  • 事实证明,使用的标题帮助了另一个正在考虑用 CRTP 替换食品的人找到“政策设计”。换句话说,我认为他从可能会提出这个问题的人的角度正确地提出了这个问题,而不是从一个回答的角度来看。
【解决方案3】:

老实说,我不确定我是否会使用以下代码,但我认为它可以满足 OP 的要求。
这是一个最小的工作示例:

#include<iostream>
#include<utility>

template<class D>
struct B {
    template <typename T>
    struct hasFoo {
        template<typename C>
        static std::true_type check(decltype(&C::foo));

        template<typename>
        static std::false_type check(...);

        static const bool value = decltype(check<T>(0))::value;
    };

    int foo() {
        return B::foo<D>(0, this);
    }

private:
    template<class T>
    static auto foo(int, B* p) -> typename std::enable_if<hasFoo<T>::value, int>::type {
        std::cout << "D::foo" << std::endl;
        return static_cast<T*>(p)->foo();
    }

    template<class T>
    static auto foo(char, B*) -> typename std::enable_if<not hasFoo<T>::value, int>::type {
        std::cout << "B::foo" << std::endl;
        return 42;
    }
};

struct A: B<A> { };

struct C: B<C> {
    int foo() {
        std::cout << "C::foo" << std::endl;
        return 0;
    }
};

int main() {
    A a;
    a.foo();
    std::cout << "---" << std::endl;
    B<A> *ba = new A;
    ba->foo();
    std::cout << "---" << std::endl;
    C c;
    c.foo();
    std::cout << "---" << std::endl;
    B<C> *bc = new C;
    bc->foo();
}

如果我做对了,就不会有虚方法,但会调用foo 的正确实现,无论您使用的是基类还是派生类。
当然,它是围绕 CRTP 习语设计的。

我知道,成员检测器类远非好。
无论如何,对于问题的目的来说已经足够了,所以......

【讨论】:

  • 谢谢你,skypjack。明天我会更深入地研究它(这里已经过了就寝时间)。
  • @Garp 欢迎您。让我知道它是否按预期工作。我不确定我是否完全理解这个问题。如果您需要更多详细信息,请随时询问,我会更新我的答案。
  • 由于您使用的是现代 C++ (tell-tale decltype),我建议您远离 C++03 - 检查函数并采用 void_t
  • @SergeyA 我没能弄清楚你说的 adopt void_t 是什么意思!!我稍微改变了成员检测器(它现在使用std::true/false_type),但我很想知道你的想法......
  • @skypjack:代码可以满足我的要求,谢谢。我必须检查组件以确保它不会用另一个替换虚拟的开销。这也是一个非常简单的例子。 SDK(实际上是一个库)大致包含跨 300 个接口的大约 3000 个方法。我想要一种更紧凑的方式来进行选择(最好在编译时)。在查看代码时,我也在考虑 void_t,但我的记忆有点模糊。我将不得不回顾 Walter Brown 在 ccpcon 2014 上的两部分演讲中的幻灯片。
【解决方案4】:

我相信,我明白你想要做什么。如果我的理解是正确的,那是做不到的。

从逻辑上讲,您可能希望在Base 中有mult 以检查子结构中是否存在mult - 如果存在,则调用它,如果不存在,则提供一些默认实现。这里的缺陷是子类中总是mult - 因为它会从Base 继承检查mult 的实现。不可避免。

解决方案是在子类中以不同的方式命名函数,并在基类中检查是否存在不同命名的函数 - 并调用它。这是一件简单的事情,如果你喜欢这个例子,请告诉我。但是,当然,您会在这里失去覆盖的美感。

【讨论】:

  • 实际上,我的派生类中的函数在 CRTP 版本中命名为my_mult。我不知道为什么我改变了名字。为了检查派生类中存在my_mult 的基类,我将mult 变成了一个模板函数,其中一个未命名的模板参数默认为decltype(static_cast&lt;T*&gt;(this)-&gt;my_mult(0))。这没有用。我的猜测是 SFINAE 系统地启动是因为编译器还不知道 my_mult 的任何信息。
  • @Garp,我有一个 sfinae 检查不同命名函数的工作示例(当然没有覆盖)。如果你愿意,我明天可以发。
  • 这确实非常有用。谢谢 :) 我不确定它是否会起作用。我对 CRTP 的理解是,它之所以起作用,只是因为在实例化期间发生在静态转换为派生之后的函数调用评估,此时编译器知道基类 派生类。看起来 SFINAE 检查是在第一次通过时执行的,所以编译器不会知道派生类,更不用说它有哪些成员函数了。感觉就像鸡蛋和鸡的问题。
  • Garp,@skypjack 已经提供了一个可行的答案。我会做的稍微不同,但这是一个你可以使用的有效解决方案,所以我认为没有理由复制它。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-06
  • 2013-10-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多