简体   繁体   中英

Does the standard allow an implicit virtual destructor not being implicitly defined when no instances of its class are created?

While thinking about this question , I stumbled upon something else I don't understand.

Standard says...

[class.dtor]/4

If a class has no user-declared destructor, a destructor is implicitly declared as defaulted. An implicitly-declared destructor is an inline public member of its class.

[class.dtor]/10

[...] If a class has a base class with a virtual destructor, its destructor (whether user- or implicitly-declared) is virtual.

[class.dtor]/7

A destructor that is defaulted and not defined as deleted is implicitly defined when it is odr-used or when it is explicitly defaulted after its first declaration.

[basic.def.odr]/3

[...] A virtual member function is odr-used if it is not pure. [...]

So now I'm wondering whether this code should compile:

#include <memory>

struct Base {
    virtual ~Base() = default;
};

struct Bar;
struct Foo : Base {
    std::unique_ptr<Bar> bar_{};
};

https://godbolt.org/z/B0wvzd

I would think that ~Foo() has to be implicitly defined, because it is virtual, but it would not compile because Bar is incomplete in this TU. Yet the code compiles in all major compilers.

What am I missing?

OK, since the issue has been clarified by various answers and comments that have been posted, let me give another shot at answering it. References are to the C++17 standard.

The issue with OP's code is that Foo has a virtual destructor that is not pure, and all non-pure virtual functions are implicitly odr-used ([basic.def.odr]/3). Since the destructor is odr-used, it seems that the implementation must generate a definition ([class.dtor]/7), and that definition must result in the instantiation of std::default_delete<Bar>::operator() , which makes the program ill-formed because Bar is incomplete ([unique.ptr.dltr.dflt]/4). So why do compilers not produce a diagnostic?

I think there is an issue with the wording of [class.dtor]/7, which reads as follows and has not been substantially changed in newer standard editions to date:

A destructor that is defaulted and not defined as deleted is implicitly defined when it is odr-used (6.2) or when it is explicitly defaulted after its first declaration.

When exactly is the implicit definition of Foo::~Foo generated? It is generated "when" Foo::~Foo is odr-used. But... when is Foo::~Foo odr-used?

Obviously "when" does not refer to physical time, and probably means something like "where". And that presumably implies that:

  • if the destructor is odr-used by an expression , then the definition must be generated in the translation unit where the expression appears, and
  • if the destructor is for a class template, [temp.point]/1 also applies.

But in the case of an implicit odr-use of a virtual destructor which occurs purely by dint of it being virtual, where is it odr-used? One way to interpret it is that, in every translation unit where a definition of class Foo appears, Foo::~Foo is considered odr-used in that translation unit, and the compiler must behave as if it generates a definition in that translation unit. If that's the case, then your program requires a diagnostic.

Another way to interpret it is that since the standard doesn't define any place where the implicit odr-use occurs, it's considered unspecified, and the implementation can choose where to define the destructor, or even not at all. (That is, if "when" means "where", and "where" means "in the set of places where the odr-use occurs", it's unspecified what that set actually is, and it might be empty.) In practice, compilers probably define the destructor in translation units where it is "odr-used by the vtable" (I put it in quotation marks because technically the standard doesn't define the concept of a vtable, and so a vtable cannot truly odr-use anything) and the vtable is "odr-used" by certain expressions such as potential invocations of the constructor, dynamic_cast , typeid , and exception handling involving Foo . And if you don't do any of those things, the compiler doesn't implicitly define the virtual destructor in that TU.

I think that the standard wording should probably be amended in order to codify existing practice, ie , the standard should say that it's unspecified where the implicit odr-use of virtual functions occurs. (Also, if the implementation does decide that the answer is "nowhere", it shouldn't be allowed to then treat the program as ill-formed NDR under [basic.def.odr]/4. That would be perverse.)

I think that ~Foo() must be getting defined as deleted . The rules for that are in [class.dtor] clause 5 :

₅ A defaulted destructor for a class X is defined as deleted if:

  • ₍₅.₁₎ X is a union-like class that has a variant member with a non-trivial destructor,
  • ₍₅.₂₎ any potentially constructed subobject has class type M (or array thereof) and M has a deleted destructor or a destructor that is inaccessible from the defaulted destructor,
  • ₍₅.₃₎ or, for a virtual destructor, lookup of the non-array deallocation function results in an ambiguity or in a function that is deleted or inaccessible from the defaulted destructor.

That would break your inference chain, because [class.dtor] clause 7 would no longer apply.

That said, I must admit that I don't see exactly which of those rules would cause ~Foo() to be defined as deleted in your case, or why.

So now I'm wondering whether this code should compile:

Are you "wondering" whether this complete program "should" compile ?

void f();
void g() { auto ff = &f; }
int main() {}

If you assume it will compile, " how many times " do you conclude the code will compile? (Is it a single truth, or a double independent truth, that it should compile?)

  1. f() is "used" in a non-use way (the address is assigned to a local variable that is optimized away).
g():
        ret
  1. g() itself isn't even used

Any "dumb" linker could see that because g() isn't needed, f() isn't needed; that is assuming a "dumb" compiler would assume that f() is even needed in g() !

Yet an undeclared function ( f() ) was clearly ODR-used . Did you expect that implementation behavior, or do you "wonder" and ask questions about it?

If you expected no error, you probably should rethink that whole "ODR-use violation gets a pass by the compiler makes me wondering" thing :

Implementation don't diagnose the lack of definition they don't need to make a working program.

What they need depends on the cleverness of the implementation, as obviously you can make the argument 1. above way more complicated if the non-need of f() in g() is buried in complex code. I intentionally gave a trivial example, one valid at -O0 .

[Note: this is assembly for g() at level -O0 :

g():
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], OFFSET FLAT:_Z1fv
        nop
        pop     rbp
        ret

No dependency on symbol f() as you can see.]

Knowledge depends on optimization level and on the amount of data provided at code generation time (compiling many translation units at the same time might help).

For non virtual functions, the function is either needed (that is named ) by a needed thing (another function or a global object initialized with that function address).

For virtual functions, getting knowledge is much more difficult: knowing that an overrider is not going to be called isn't as trivial as with a function that is not an overrider (including a non virtual function), as an overrider can be called via "late binding", when a virtual call is made on a (g)lvalue.

The compiler may or may not be able to determine precisely which virtual functions are never needed, depending on the complexity of the program (the halting program issue shows that they will never to know exactly for all programs), but non static member functions of objects that are never instantiated obviously are never needed .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM