关于c ++:用CRTP替换非纯虚函数

Replacing non-pure virtual functions with CRTP

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

一直困扰我的是,在编译时什么都知道。这里与运行时多态性相关联的虚拟函数表和机器只是为了提供默认实现。我想在保持方便的同时把这个开销去掉。

作为一个(非常人为的)示例,假设我有两个服务器展示了一个单一的接口(名为blah),它只包含一个没有默认实现的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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工作得很好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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; }
};

问题出在非纯虚拟函数上,即当方法有默认实现时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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和一些表情结合起来,但失败了。我想我需要的是某种代码调度,在这种调度中,基类要么提供默认实现,要么将其参数转发给派生类中的实现(如果存在的话)。问题似乎是,调度应该依赖于基类中编译器尚不可用的信息。

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

(请使用小词,因为我对模板的专长有点轻描淡写。谢谢。


考虑使用类似策略设计的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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关键字,我希望现代编译器能够进行优化。

一些有用的参考资料:

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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#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习惯用法设计的。

我知道,成员检测类远远不够好。不管怎样,就这个问题而言已经足够了,所以…


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

1
2
3
4
5
6
7
8
9
10
11
12
13
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,一切都是一样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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。我不知道在保持函数virtual的情况下,crtp是否有任何性能优势。


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

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

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