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

2

7

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?

Tetramethyldiarsine answered 24/10, 2019 at 14:10 Comment(16)
Where do you ODR-use Foo's destructor? Fails to compile if you actually make a Foo: godbolt.org/z/ydHIOlGodparent
@NathanOliver, "A virtual member function is odr-used if it is not pure." I interpret this as: ~Foo is odr-used simply by being declared. I could very well be wrong, but that's what I make of it.Tetramethyldiarsine
btw "compiles without errors" and "is valid code" are not necessarily the sameVirtuosity
Destructors are considered special member functions and later on in that paragraph it has A destructor for a class is odr-used if it is potentially invoked so I'm going to say A virtual member function is odr-used if it is not pure. doesn't actually apply here. I'll let someone else actually put that as an answer if they want as I'm not sure if that logic is correct.Godparent
@NathanOliver, that is the sane interpretation I was trying to get to, and I'm asking this question because I'm also not sure that it's correct. :)Tetramethyldiarsine
[basic.def.odr] "Every program shall contain exactly one definition of every non-inline function or variable that is odr-used [...]; no diagnostic required." Therefore, compilers are not required to generate a warning or error if the rule is violated, and it seems that none of them do in this case.Inhabitant
@NathanOliver: That paragraph gives a list of cases where a function or variable is odr-used by an expression. Later sentences don't supersede earlier ones, they just add more cases. So, for example, there are several sentences that give cases where a deallocation function is odr-used; a deallocation function is odr-used if any of those cases occurs in a program.Dourine
Looks like compilers haven't implemented the resolution of CWG2068 yet.Zamindar
@LanguageLawyer nice find. So the intent there seems to be that my example should compile, but the wording change might actually mean it should not compile.Tetramethyldiarsine
@RaymondChen No compiler (that I know of) even tries to detect these ODR violations; they just diagnose missing definitions that they believe are needed. Whether a function that is ODR-used is needed depends on details of the code generation.Coypu
@Tetramethyldiarsine "that is the sane interpretation" According to which sanity criteria?Coypu
@Godparent "so I'm going to say *A virtual member function is odr-used if it is not pure*. doesn't actually apply here" So you are concluding that some virtual functions not be automatically ODR-used. What makes dtor in any way shape or form special as a virtual function*? Why would a programming language semantic make some virtual functions inherently ODR-used and not some others?Coypu
@RaymondChen, I don't see an ODR violation in my example. Did you read my example as representing a complete program? If so, I've now tried to clarify it.Tetramethyldiarsine
I'm not saying whether an ODR violation exists or not. I'm saying that inferring "no ODR violation" from "compiler accepts without error" is invalid because compilers are allowed to ignore ODR violations!Inhabitant
@RaymondChen your quote is about non-inline functions whilst implicit destructors are inline.Zamindar
The point still stands: Compiler silence is not a valid basis for drawing conclusions about odr because compilers are allowed to ignore odr violations.Inhabitant
Z
0

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, i.e., 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.)

Zirkle answered 24/10, 2019 at 17:58 Comment(0)
C
0

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.

Coypu answered 26/10, 2019 at 3:29 Comment(5)
I understand what an ODR violation is, and why it frequently cannot be diagnosed. But I don't quite see how your answer relates to my example; do you see an ODR violation in there? Or did you think my example was supposed to represent a complete program? If so, that was not my intention and I have changed my example to reflect that.Tetramethyldiarsine
@Tetramethyldiarsine Yes, I did analyse your (initial) Q as one about a kind of ODR violation that is 1) not diagnosed and 2) harmless.Coypu
@Tetramethyldiarsine After re-reading your modified Q several times, I still don't see the point. You are saying the function is implicitly declared and ODR used; then what? Why would the compiler be forced to generate it? It isn't used. (If it's used elsewhere show us where.) How is the rest of the program relevant? My code examples can be compiled separately too. The point about link error would be less clear though.Coypu
Because the rules say an implicitly declared destructor is implicitly defined when it is odr-used and not defined as deleted. So, even though ~Foo() is clearly not needed, I think the standard still requires it to be defined. And this definition would be ill-formed, because Bar is incomplete here. Whether Bar is complete elsewhere should be irrelevant, because what may or may not make this code ill-formed does not involve the ODR itself, just odr-use.Tetramethyldiarsine
@Tetramethyldiarsine "When" is a point of instanciation? When does a virtual function becomes virtual? When it's declared? But "when" you declare a member, the class is incomplete...Coypu

© 2022 - 2024 — McMap. All rights reserved.