确实不太可能。标准中没有任何内容可以阻止编译器做整个类的愚蠢低效的事情,但是非虚拟调用仍然是非虚拟调用,无论该类是否也具有虚拟函数。它必须调用对应于静态类型的函数版本,而不是动态类型:
struct Foo {
void foo() { std::cout << "Foo\n"; }
virtual void virtfoo() { std::cout << "Foo\n"; }
};
struct Bar : public Foo {
void foo() { std::cout << "Bar\n"; }
void virtfoo() { std::cout << "Bar\n"; }
};
int main() {
Bar b;
Foo *pf = &b; // static type of *pf is Foo, dynamic type is Bar
pf->foo(); // MUST print "Foo"
pf->virtfoo(); // MUST print "Bar"
}
Run Code Online (Sandbox Code Playgroud)
因此,实现绝对不需要将非虚拟函数放在 vtable 中,并且实际上在 vtable 中,因为Bar
在此示例中您需要两个不同的插槽用于Foo::foo()
和Bar::foo()
。这意味着即使实现想要这样做,它也将是 vtable 的特殊使用。在实践中它不想这样做,这样做没有意义,不要担心。
CRTP 基类确实应该具有非虚拟和受保护的析构函数。
如果类的用户可能需要一个指向对象的指针,将其转换为基类指针类型,然后将其删除,则需要一个虚拟析构函数。虚拟析构函数意味着这将起作用。基类中的受保护析构函数阻止他们尝试它(delete
由于没有可访问的析构函数,因此不会编译)。因此,无论是 virtual 还是 protected 都解决了用户意外引发未定义行为的问题。
请参阅此处的准则 #4,并注意本文中的“最近”是指近 10 年前:
http://www.gotw.ca/publications/mill18.htm
没有用户会创建Base<Derived>
自己的对象,这不是Derived
对象,因为这不是 CRTP 基类的用途。他们只是不需要能够访问析构函数 - 因此您可以将其保留在公共界面之外,或者为了保存一行代码,您可以将其保留为公共并依靠用户不要做一些愚蠢的事情。
不希望它是虚拟的,因为它不需要是虚拟的,只是因为如果它不需要类,则提供虚拟函数是没有意义的。有一天,它可能会在对象大小、代码复杂性或什至(不太可能)速度方面付出一些代价,因此始终使事物虚拟化是一种过早的悲观情绪。使用 CRTP 的 C++ 程序员的首选方法是绝对清楚类的用途,它们是否被设计为基类,如果是,它们是否被设计为多态基。CRTP 基类不是。
用户没有对 CRTP 基类进行业务转换的原因,即使它是公共的,也是因为它并没有真正提供“更好”的接口。CRTP 基类依赖于派生类,因此如果您将其转换Derived*
为Base<Derived>*
. 任何其他类都不会Base<Derived>
作为基类,除非它也有Derived
基类。它只是作为多态基础没有用,所以不要把它变成一个。
只有虚函数需要动态调度(因此需要vtable查找),即使在所有情况下都不需要.如果编译器能够在编译时确定方法调用的最终覆盖器是什么,则它可以在运行时忽略执行调度.如果需要,用户代码也可以禁用动态分派:
struct base {
virtual void foo() const { std::cout << "base" << std::endl; }
void bar() const { std::cout << "bar" << std::endl; }
};
struct derived : base {
virtual void foo() const { std::cout << "derived" << std::endl; }
};
void test( base const & b ) {
b.foo(); // requires runtime dispatch, the type of the referred
// object is unknown at compile time.
b.base::foo();// runtime dispatch manually disabled: output will be "base"
b.bar(); // non-virtual, no runtime dispatch
}
int main() {
derived d;
d.foo(); // the type of the object is known, the compiler can substitute
// the call with d.derived::foo()
test( d );
}
Run Code Online (Sandbox Code Playgroud)
关于是否应该在所有继承情况下提供虚拟析构函数,答案是否定的,不一定是.仅当delete
派生类型的代码对象通过指向基类型的指针保存时,才需要虚拟析构函数.通常的规则是你应该
规则的第二部分确保用户代码不能通过指向基础的指针删除对象,这意味着析构函数不必是虚拟的.优点是如果你的类不包含任何虚方法,这不会改变你的类的任何属性 - 当添加第一个虚方法时类的内存布局会改变 - 你将保存vtable指针在每个实例中.从两个原因来看,第一个是重要原因.
struct base1 {};
struct base2 {
virtual ~base2() {}
};
struct base3 {
protected:
~base3() {}
};
typedef base1 base;
struct derived : base { int x; };
struct other { int y; };
int main() {
std::auto_ptr<derived> d( new derived() ); // ok: deleting at the right level
std::auto_ptr<base> b( new derived() ); // error: deleting through a base
// pointer with non-virtual destructor
}
Run Code Online (Sandbox Code Playgroud)
main的最后一行中的问题可以通过两种不同的方式解决.如果typedef
更改为,base1
则析构函数将正确分派给derived
对象,代码不会导致未定义的行为.成本是derived
现在需要一个虚拟表,每个实例都需要一个指针.更重要的derived
是,不再与布局兼容other
.另一个解决方案是更改typedef
to base3
,在这种情况下,通过让编译器在该行上大喊来解决问题.缺点是你不能通过指针删除base,优点是编译器可以静态地确保不会有未定义的行为.
在CRTP模式(原谅冗余的特殊情况下的图案),大多数作者甚至不关心,使析构函数保护,因为其目的是不给基地引用持有派生类型的对象(模板)类型.为了安全起见,他们应该将析构函数标记为受保护,但这很少成为问题.
归档时间: |
|
查看次数: |
1600 次 |
最近记录: |