tl;dr
Long Explanation
What the C++ Standard says
- 11.4.7 Destructors
If a class has no user-declared prospective destructor, a prospective destructor is implicitly declared as
defaulted (9.5). An implicitly-declared prospective destructor is an inline public member of its class.
- 11.4.4 Special member functions
Default constructors (11.4.5.2), copy constructors, move constructors (11.4.5.3), copy assignment operators,
move assignment operators (11.4.6), and prospective destructors (11.4.7) are special member functions.
An implicitly-declared special member function is declared at the closing } of the class-specifier. Programs
shall not define implicitly-declared special member functions.
So if you don't add a destructor to your class you'll get an implicit one that will be an inline public member of the class and declared at the the end of the class definition.
Note that it is only declared - not defined - so the compiler will not check if the destructor would compile at this point.
i.e. in your example it could look like this:
struct A {
std::string s;
// it's only *declared*, not defined
inline ~A();
};
The compiler only needs to define the destructor if it is used somewhere.
So unless you use A
somewhere it's implicit destructor will never be defined and you won't get any errors.
So the following is valid c++ even though the destructor of Foo
would not be able to be compiled because Miau
is never defined:
struct Miau;
class Foo {
public:
std::unique_ptr<Miau> miauPtr;
};
// Miau never defined
// Foo never used
The actual point where the compiler needs to define the destructor is:
- 11.4.7 Destructors
A destructor that is defaulted and not defined as deleted is implicitly defined when it is odr-used (6.3) or
when it is explicitly defaulted after its first declaration.
So the compiler needs to generate a definition for the destructor when you actually use it or when you explicitly default it (e.g. A::~A() = default;
, which in your case doesn't apply)
At this point you would actually get an error if your implicit destructor doesn't compile (e.g. because one of the members uses an incomplete type)
The declarations that will be visible to the implicitly defined destructor are defined as follows:
- 10.6 Instantiation context
During the implicit definition of a defaulted function (11.4.4, 11.11.1), the instantiation context is the union
of the instantiation context from the definition of the class and the instantiation context of the program
construct that resulted in the implicit definition of the defaulted function.
So everything that was accessible at the point of the class definition and additionally everything that is accessible from the point where you first used the class is visible to the destructor.
e.g.:
#include <memory>
#include <string>
struct Miau;
struct Foo {
std::unique_ptr<Miau> miauPtr;
};
struct Miau { std::string s; };
void useFoo() {
Foo f; // first usage of Foo
// this forces the compiler to define the destructor for Foo.
// it'll compile without any error because at this point Miau
// is already defined.
}
godbolt example
Note that this is only the case for implicitly defined destructors.
If you explicitly define the destructor (even if you default it), then instead of the point of first use the point where you explicitly defined it will be used instead.
e.g. if you replace Foo
from the example above with:
// ok - implicit declaration and implicit definition
struct Foo {
std::unique_ptr<Miau> miauPtr;
};
// ok - explicit declaration and implicit definition
struct Foo {
~Foo() = default;
std::unique_ptr<Miau> miauPtr;
};
// error - explicit declaration and explicit definition
struct Foo {
~Foo();
std::unique_ptr<Miau> miauPtr;
};
Foo::~Foo() = default; // if Miau is not defined before this line we'll get an error
godbolt example
What's up to the compilers
The standard only defines how the destructor will be declared in the class body and when it needs to be defined.
Everything else is the decision of the compiler - how he generates the code for the destructor (be it source code, some internal representation or directly bytecode) or where the code then gets stored.
So you might up with no code at all for the destructor when it gets inlined everywhere.
Or you might get one version of the destructor for each translation unit.
Or a single version of the destructor that gets called by all the translation units.
This might also depend on the compilation flags you use and even on the code itself - so without a concrete example there is no clear answer to what the compiler will do with the implicit destructor.
The only guarantee you have is that the code of the destructor will get executed when an A is destroyed, apart from that everything is in the hands of the compiler.