在 CRTP 类层次结构中静态断言派生类没有实现某个方法

问题描述 投票:0回答:2

我有一个使用 CRTP 的类层次结构。基类定义了一个我无法更改的接口(在另一个库中)。它有一些

impl
方法,派生类可以“覆盖”(阴影)以专门化行为。

现在,我想介绍一个新的基类,它继承了原来的基类,并适配了这个接口。即,它实现了一些

impl
方法并提供了新方法。我想检查这个中间类,我实现的
impl
方法是 not 由最终派生类实现的。相反,最终的派生类应该使用新的、经过改编的
impl
方法。

让我们举个例子(完整例子:https://godbolt.org/z/9bWrezPv3)。

不能改变的基类接口是:

template <class Derived>
struct Greeter {
    void greet() {
        cout << "Hello ";
        static_cast<Derived*>(this)->greetImpl();
        cout << "\n";
    }

    void greetImpl() {
        cout << "world";
    }
};

通常的用法是:

struct MyGreeter: public Greeter<MyGreeter> {
    void greetImpl() {
        cout << "WORLD";
    }
};

现在,我想通过引入中间基类来提供额外的通用功能。

template <class Derived>
struct ExcitedGreeter: public Greeter<Derived> {
    using Base = Greeter<Derived>;

    void greetImpl() {
        static_cast<Derived*>(this)->greetCamlyImpl();
        cout << "!!!";
    }

    void greetCamlyImpl() {
        Base::greetImpl();
    }
};

ExcitedGreeter
的用户现在应该实施
greetCamlyImpl
。如果他们改为实现
greetImpl
,功能就会被破坏,例如:

struct MyWorkingGreeter: public ExcitedGreeter<MyWorkingGreeter> {
    void greetCamlyImpl() {
        cout << "beautiful world";
    }
};

struct MyBuggyGreeter: public ExcitedGreeter<MyBuggyGreeter> {
    // Ooops, forgot to switch from `greetImpl` to `greetCamlyImpl` when 
    // changing the base class from `Greeter` to `ExcitedGreeter`...
    void greetImpl() {
        cout << "buggy world";
    }
};

MyWorkingGreeter::greet
打印
Hello beautiful world!!!
,而
MyBuggyGreeter::greet
打印
Hello buggy world
。后者没有应有的兴奋……

我的问题是,我如何静态地断言

MyBuggyGreeter
没有在
greetImpl()
的定义中实现
ExcitedGreeter
?我试过的两件事是检查
&Derived::greetImpl
的类型:

using This = ExcitedGreeter<Derived>;
static_assert(std::is_same<decltype(&Derived::greetImpl), void (This::*)()>::value);

还比较函数指针:

static_assert(&Derived::greetImpl == &This::greetImpl);

我尝试将这些检查放在

ExcitedGreeter::greetImpl
的定义中。

但在这两种情况下,对于

Derived::greetImpl
ExcitedGreeter::greetImpl
,似乎
MyWorkingGreeter
总是解析为
MyBuggyGreeter

知道为什么会这样以及如何纠正这个问题吗?

c++ templates compile-time crtp
2个回答
3
投票

您的逻辑不适合调整界面,通过向

Derived
提供
Base
,它允许
Base
完全绕过您的中间:
ExcitedGreeter
.

如何解决这个问题?不要一直提供

Derived
你的链。

template <class Derived>
struct ExcitedGreeter: public Greeter<ExcitedGreeter> {
    using Base = Greeter<ExcitedGreeter>;

    void greetImpl() {
        static_cast<Derived*>(this)->greetCamlyImpl();
        cout << "!!!";
    }

    void greetCamlyImpl() {
        Base::greetImpl();
    }
};

这将迫使

Base
类利用
ExcitedGreeter
然后可以用您当前的实现完成链,因为
Base
只能在您的
greetImpl
类中调用
ExcitedGreeter

为了确保

Derived
提供“覆盖”(不是真正的覆盖),
static_assert
可用于检查
ExcitedGreeter
greetImpl
是否在
Derived
中被遮蔽:

 static_assert(&ExcitedGreeter<Derived>::greetImpl == &Derived::greetImpl)

简化示例:


template<class Child>
class A
{
  public:
    void my_func(){printf("Do a thing");}
};


class B : public A<B>
{
    public:
    void my_func(){printf("Shadowed");}
};

static_assert(&A<B>::my_func != &B::my_func);

神箭


0
投票

@mascoj 的回答很好,并显示了替代方案,例如将

ExcitedGreeter
传递给
Greeter
作为模板参数而不是
Derived
。这确保了
greetImpl()
中的
ExcitedGreeter
总是从
greet()
中调用,但是如果派生类也意外地实现了
greetImpl()
,它不会导致错误。讨论让我意识到为什么我在问题中尝试的静态断言不起作用以及如何解决它。

静态断言最初不起作用的原因是我将它们放在

ExcitedGreeter::greetImpl()
中。当它被
MyBuggyGreeter::greetImpl()
遮蔽时,它是一个永远不会被调用的方法。由于 SFINAE,编译器只是简单地从类定义中省略了函数并且不会产生错误。

如果我们将静态断言放在析构函数中,它会按预期运行。我在问题中描述的断言变体都可以正常工作,但我发现比较函数指针地址更容易阅读/理解:

    ~ExcitedGreeter() {
        using This = ExcitedGreeter<Derived>;
        static_assert(&Derived::greetImpl == &This::greetImpl &&
            "Don't shadow `greetImpl()`; instead implement `greetCalmlyImpl()`.");
    }

godbolt 上的工作示例

PS:错误的一个来源仍然是,如果您拼错

greetCalmlyImpl()
,您不会得到任何错误。您的专门实施从未被调用。这是这种静态多态性的一个普遍问题,同样的问题已经存在于
Greeter
的界面中。如果 C++ 支持像
final
override
这样的非虚拟方法,它就可以解决。具有讽刺意味的是,在我原来的例子中我拼错了函数
greetCamlyImpl()
...

© www.soinside.com 2019 - 2024. All rights reserved.