我有一个使用 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
。
知道为什么会这样以及如何纠正这个问题吗?
您的逻辑不适合调整界面,通过向
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);
@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()`.");
}
PS:错误的一个来源仍然是,如果您拼错
greetCalmlyImpl()
,您不会得到任何错误。您的专门实施从未被调用。这是这种静态多态性的一个普遍问题,同样的问题已经存在于Greeter
的界面中。如果 C++ 支持像 final
和 override
这样的非虚拟方法,它就可以解决。具有讽刺意味的是,在我原来的例子中我拼错了函数greetCamlyImpl()
...