使用虚拟析构函数会让非虚拟函数进行 v 表查找吗?

发布于 2024-09-27 10:20:16 字数 83 浏览 1 评论 0原文

正是题目所问的。还想知道为什么 CRTP 的常见示例没有提到虚拟 dtor。

编辑: 各位,请也发布有关 CRTP 问题的信息,谢谢。

Just what the topic asks. Also want to know why non of the usual examples of CRTP do not mention a virtual dtor.

EDIT:
Guys, Please post about the CRTP prob as well, thanks.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

不必在意 2024-10-04 10:20:18

确实不太可能。标准中没有任何内容可以阻止编译器执行整个类的愚蠢低效的事情,但非虚拟调用仍然是非虚拟调用,无论该类是否也具有虚拟函数。它必须调用与静态类型相对应的函数版本,而不是动态类型:

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"
}

因此,实现完全不需要将非虚函数放入 vtable 中,实际上不需要将非虚函数放入 Bar的 vtable 中code> 在此示例中,您需要两个不同的插槽用于 Foo::foo()Bar::foo()。这意味着即使实现想要这样做,这也将是vtable的特殊使用。实际上它不想这样做,这样做没有意义,不用担心。

CRTP 基类确实应该具有非虚拟且受保护的析构函数。

如果类的用户可能获取指向对象的指针,将其转换为基类指针类型,然后删除它,则需要虚拟析构函数。虚拟析构函数意味着这将起作用。基类中受保护的析构函数阻止他们尝试它(删除不会编译,因为没有可访问的析构函数)。因此,虚拟或受保护的任一者都可以解决用户意外引发未定义行为的问题。

请参阅此处的指南 #4,并注意本文中的“最近”是指近 10 年前:

http: //www.gotw.ca/publications/mill18.htm

没有用户会创建自己的 Base 对象,该对象不是 Derived 对象,因为这不是 CRTP 基类的用途。他们只是不需要能够访问析构函数 - 因此您可以将其保留在公共接口之外,或者为了保存一行代码,您可以将其保留为公共并依赖用户不要做一些愚蠢的事情。

之所以不希望它是虚拟的(因为它不需要是虚拟的),是因为如果类不需要虚拟函数,那么提供虚拟函数是没有意义的。有一天,它可能会在对象大小、代码复杂性甚至(不太可能)速度方面付出一些代价,因此总是让事物变得虚拟是一种过早的悲观情绪。使用 CRTP 的 C++ 程序员的首选方法是绝对清楚类的用途,它们是否被设计为基类,如果是,它们是否被设计为用作多态基类。 CRTP 基类则不然。

用户没有业务转换到 CRTP 基类(即使它是公共的)的原因是它并没有真正提供“更好”的接口。 CRTP 基类依赖于派生类,因此,如果将 Derived* 强制转换为 Base*,则并不像是切换到更通用的接口。任何其他类都不会以 Base 作为基类,除非它也以 Derived 作为基类。它作为多态性基础没有用处,所以不要将其作为多态性基础。

Very unlikely indeed. There's nothing in the standard to stop compilers doing whole classes of stupidly inefficient things, but a non-virtual call is still a non-virtual call, regardless of whether the class has virtual functions too. It has to call the version of the function corresponding to the static type, not the dynamic type:

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"
}

So there's absolutely no need for the implementation to put non-virtual functions in the vtable, and indeed in the vtable for Bar you'd need two different slots in this example for Foo::foo() and Bar::foo(). That means it would be a special-case use of the vtable even if the implementation wanted to do it. In practice it doesn't want to do it, it wouldn't make sense to do it, don't worry about it.

CRTP base classes really ought to have destructors that are non-virtual and protected.

A virtual destructor is required if the user of the class might take a pointer to the object, cast it to the base class pointer type, then delete it. A virtual destructor means this will work. A protected destructor in the base class stops them trying it (the delete won't compile since there's no accessible destructor). So either one of virtual or protected solves the problem of the user accidentally provoking undefined behavior.

See guideline #4 here, and note that "recently" in this article means nearly 10 years ago:

http://www.gotw.ca/publications/mill18.htm

No user will create a Base<Derived> object of their own, that isn't a Derived object, since that's not what the CRTP base class is for. They just don't need to be able to access the destructor - so you can leave it out of the public interface, or to save a line of code you can leave it public and rely on the user not doing something silly.

The reason it's undesirable for it to be virtual, given that it doesn't need to be, is just that there's no point giving a class virtual functions if it doesn't need them. Some day it might cost something, in terms of object size, code complexity or even (unlikely) speed, so it's a premature pessimization to make things virtual always. The preferred approach among the kind of C++ programmer who uses CRTP, is to be absolutely clear what classes are for, whether they are designed to be base classes at all, and if so whether they are designed to be used as polymorphic bases. CRTP base classes aren't.

The reason that the user has no business casting to the CRTP base class, even if it's public, is that it doesn't really provide a "better" interface. The CRTP base class depends on the derived class, so it's not as if you're switching to a more general interface if you cast Derived* to Base<Derived>*. No other class will ever have Base<Derived> as a base class, unless it also has Derived as a base class. It's just not useful as a polymorphic base, so don't make it one.

我的奇迹 2024-10-04 10:20:18

第一个问题的答案:不会。只有对虚拟函数的调用才会在运行时通过虚拟表引起间接。

第二个问题的答案:奇怪的重复模板模式通常使用私有继承来实现。您不建模“IS-A”关系,因此您不传递指向基类的指针。

例如,在

template <class Derived> class Base
{
};

class Derived : Base<Derived>
{
};

您没有采用 Base* 然后继续对其调用删除的代码中。因此,您永远不会尝试通过指向基类的指针删除派生类的对象。因此,析构函数不需要是虚拟的。

The answer to your first question: No. Only calls to virtual functions will cause an indirection via the virtual table at runtime.

The answer to your second question: The Curiously recurring template pattern is commonly implemented using private inheritance. You don't model an 'IS-A' relationship and hence you don't pass around pointers to the base class.

For instance, in

template <class Derived> class Base
{
};

class Derived : Base<Derived>
{
};

You don't have code which takes a Base<Derived>* and then goes on to call delete on it. So you never attempt to delete an object of a derived class through a pointer to the base class. Hence, the destructor doesn't need to be virtual.

如果没有 2024-10-04 10:20:18

首先,我认为OP问题的答案已经得到很好的回答——这是一个明确的否定。

但是,是我疯了还是社区出了严重问题?看到这么多人暗示持有对 Base 的指针/引用是无用/罕见的,我感到有点害怕。上面的一些流行答案表明我们不使用 CRTP 来模拟 IS-A 关系,我完全不同意这些观点。

众所周知,C++ 中不存在接口这样的东西。因此,为了编写可测试/可模拟的代码,很多人使用 ABC 作为“接口”。例如,您有一个函数 void MyFunc(Base* ptr),您可以通过以下方式使用它:MyFunc(ptr_driven)。这是建模 IS-A 关系的传统方法,当您调用 MyFunc 中的任何虚拟函数时,需要进行 vtable 查找。这是对 IS-A 关系进行建模的模式一。

在某些性能至关重要的领域,存在另一种方法(模式二)以可测试/可模拟的方式对 IS-A 关系进行建模 - 通过 CRTP。事实上,在某些情况下,性能提升可能令人印象深刻(本文中为 600%),请参阅此 链接。所以 MyFunc 看起来像这样 templatevoid MyFunc(Base*ptr)。当您使用 MyFunc 时,您执行 MyFunc(ptr_driven); 编译器将为 MyFunc() 生成与参数类型 ptr_driven 最匹配的代码副本 - MyFunc(Base; *ptr)。在 MyFunc 内部,我们很可能会假设调用了接口定义的某些函数,并且指针在编译时进行静态转换(查看链接中的 impl() 函数),没有 vtable 查找的开销。

现在,有人可以告诉我,要么我在胡说八道,要么上面的答案根本没有考虑用 CRTP 建模 IS-A 关系的第二种模式?

Firstly, I think the answer to the OP's question has been answered quite well - that's a solid NO.

But, is it just me going insane or is something going seriously wrong in the community? I felt a bit scared to see so many people suggesting that it's useless/rare to hold a pointer/reference to Base. Some of the popular answers above suggest that we don't model IS-A relationship with CRTP, and I completely disagree with those opinions.

It's widely known that there's no such thing as interface in C++. So to write testable/mockable code, a lot of people use ABC as an "interface". For example, you have a function void MyFunc(Base* ptr) and you can use it this way: MyFunc(ptr_derived). This is the conventional way to model IS-A relationship which requires vtable lookups when you call any virtual functions in MyFunc. So this is pattern one to model IS-A relationship.

In some domain where performance is critical, there exists another way(pattern two) to model IS-A relationship in a testable/mockable manner - via CRTP. And really, performance boost can be impressive(600% in the article) in some cases, see this link. So MyFunc will look like this template<typename Derived> void MyFunc(Base<Derived> *ptr). When you use MyFunc, you do MyFunc(ptr_derived); The compiler is going to generate a copy of code for MyFunc() that matches best with the parameter type ptr_derived - MyFunc(Base<Derived> *ptr). Inside MyFunc, we may well assume some function defined by the interface is called, and pointers are statically cast-ed at compile time(check out the impl() function in the link), there's no overheads for vtable lookups.

Now, can someone please tell me either I am talking insane nonsense or the answers above simply did not consider the second pattern to model IS-A relationship with CRTP?

離殇 2024-10-04 10:20:17

只有虚拟函数需要动态调度(因此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 );
}

关于是否应该在所有继承情况下提供虚拟析构函数,答案是否定的,也不一定。仅当代码删除通过指向基类型的指针保存的派生类型的对象时,才需要虚拟析构函数。通用规则是您应该

  • 提供一个公共虚拟析构函数或一个受保护的非虚拟析构函数

规则的第二部分确保用户代码无法通过指向基类的指针删除您的对象,这意味着析构函数不必是虚拟的。优点是,如果你的类不包含任何虚方法,这不会改变你的类的任何属性——添加第一个虚方法时类的内存布局会改变——并且你将保存 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
}

main 最后一行的问题可以通过两种不同的方式解决。如果将typedef更改为base1,则析构函数将正确分派到派生对象,并且代码不会导致未定义的行为。代价是派生现在需要一个虚拟表,并且每个实例都需要一个指针。更重要的是,衍生不再与其他布局兼容。另一个解决方案是将 typedef 更改为 base3,在这种情况下,通过让编译器在该行大喊大叫即可解决问题。缺点是不能通过基址指针进行删除,优点是编译器可以静态地保证不会出现未定义的行为。

在 CRTP 模式的特定情况下(请原谅多余的模式),大多数作者甚至不关心使析构函数受保护,因为其目的不是通过引用来保存派生类型的对象基本(模板)类型。为了安全起见,他们应该将析构函数标记为受保护,但这很少是一个问题。

Only virtual functions require dynamic dispatch (and hence vtable lookups) and not even in all cases. If the compiler is able to determine at compile time what is the final overrider for a method call, it can elide performing the dispatch at runtime. User code can also disable the dynamic dispatch if it so desires:

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 );
}

On whether you should provide virtual destructors in all cases of inheritance, the answer is no, not necessarily. The virtual destructor is required only if code deletes objects of the derived type held through pointers to the base type. The common rule is that you should

  • provide a public virtual destructor or a protected non-virtual destructor

The second part of the rule ensures that user code cannot delete your object through a pointer to the base, and this implies that the destructor need not be virtual. The advantage is that if your class does not contain any virtual method, this will not change any of the properties of your class --the memory layout of the class changes when the first virtual method is added-- and you will save the vtable pointer in each instance. From the two reasons, the first being the important one.

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
}

The problem in the last line of main can be resolved in two different ways. If the typedef is changed to base1 then the destructor will correctly be dispatched to the derived object and the code will not cause undefined behavior. The cost is that derived now requires a virtual table and each instance requires a pointer. More importantly, derived is no longer layout compatible with other. The other solution is changing the typedef to base3, in which case the problem is solved by having the compiler yell at that line. The shortcoming is that you cannot delete through pointers to base, the advantage is that the compiler can statically ensure that there will be no undefined behavior.

In the particular case of the CRTP pattern (excuse the redundant pattern), most authors do not even care to make the destructor protected, as the intention is not to hold objects of the derived type by references to the base (templated) type. To be in the safe side, they should mark the destructor as protected, but that is rarely an issue.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文