【发布时间】:2012-06-27 11:08:03
【问题描述】:
我想不出一个合适的问题标题来描述问题。希望下面的详细信息能清楚地解释我的问题。
考虑下面的代码
#include <iostream>
template <typename Derived>
class Base
{
public :
void call ()
{
static_cast<Derived *>(this)->call_impl();
}
};
class D1 : public Base<D1>
{
public :
void call_impl ()
{
data_ = 100;
std::cout << data_ << std::endl;
}
private :
int data_;
};
class D2 : public Base<D1> // This is wrong by intension
{
public :
void call_impl ()
{
std::cout << data_ << std::endl;
}
private :
int data_;
};
int main ()
{
D2 d2;
d2.call_impl();
d2.call();
d2.call_impl();
}
尽管D2 的定义是故意错误的,但它会编译并运行。第一次调用d2.call_impl() 将输出一些随机位,这是由于D2::data_ 未初始化而预期的。第二次和第三次调用都会为data_ 输出100。
我明白为什么它会编译和运行,如果我错了,请纠正我。
当我们调用 d2.call() 时,调用被解析为 Base<D1>::call,这会将 this 转换为 D1 并调用 D1::call_impl。因为D1 确实是从Base<D1> 派生的,所以在编译时转换是可以的。
在运行时,在转换之后,this,虽然它确实是一个D2 对象,但它被视为D1,并且对D1::call_impl 的调用将修改应该的内存位是D1::data_,然后输出。在这种情况下,这些位恰好在D2::data_ 所在的位置。我认为第二个d2.call_impl() 也应该是未定义的行为,具体取决于 C++ 实现。
关键是,这段代码,虽然在本质上是错误的,但不会给用户任何错误的迹象。我在我的项目中真正做的是我有一个 CRTP 基类,它就像一个调度引擎。库中的另一个类访问 CRTP 基类的接口,比如call,call 将分派到call_dispatch,它可以是基类默认实现或派生类实现。如果用户定义的派生类,比如D,确实是从Base<D> 派生的,那么这些都可以正常工作。如果它是从Base<Unrelated> 派生的,其中Unrelated 不是从Base<Unrelated> 派生的,它将引发编译时错误。但它不会阻止用户编写上述代码。
用户通过从基 CRTP 类派生并提供一些实现细节来使用该库。当然还有其他设计方案可以避免上述错误使用的问题(例如抽象基类)。但是,让我们暂时将它们放在一边,相信我,出于某种原因,我需要这种设计。
所以我的问题是,有什么方法可以防止用户编写错误的派生类,如上所示。也就是说,如果用户编写了一个派生的实现类,比如D,但他是从Base<OtherD>派生的,那么就会引发编译时错误。
一种解决方案是使用dynamic_cast。但是,这很广泛,即使它有效,它也是一个运行时错误。
【问题讨论】:
-
简短回答:不。
dynamic_cast可能很昂贵,但比尝试修复用户要便宜。 -
至少在单元测试中会更快地发现失败的 dynamic_cast。你不能让编译器保护你免受所有可能的错字,比如写
i + 1,意思是i - 1。这是相似的。