2

EDIT: Added a field to the example class to make it non-trivially-destructible, and a clarification that it was not meant to be the point of the question.

Consider a simple class with no user-declared destructor, e.g.

struct A {
    std::string s;
};

Note that the actual field doesn't matter - it's only here to make the classes non-trivially-destructible, so that it doesn't distract. In other words, imagine it's a "normal" everyday class, not exceptional in any way.

In this case, the compiler is going to generate the destructor for us. The overall question is: when exactly is this destructor generated and where is its body placed?

To be more specific:

  • does the compiler actually generate any source code? and if so, where is this source code placed?
  • if no actual source code is generated, and we move directly to some internal represention / machine code, the questions are the same - when is the generation triggered, and where is its representation stored?

The practical motivation for this question is the issue of using unique_ptr with incomplete types, as in std::unique_ptr with an incomplete type won't compile or related questions. The solution is to prevent the compiler from generating the destructor on its own, and to precisely control the location by ourselves, so that the generation happens only after the complete type definition has been seen. And while most answers mention that it's generated "too early", I couldn't find any specifics on when exactly does it happen.

I'd speculate that the destructor (and other auto-generated members) would be placed at the end of class definition, right before the closing brace, but it's just a guess.

Any normative references would be much appreciated as well.

Please also note that this question is not about the contents of the destructor - it's not about what it does, it's about where it's located.

Kassiar
  • 180
  • 9
  • 2
    _does the compiler actually generate any source code? and if so, where is this source code placed?_: no a compiler doesn't create source code at all. _Second question_: you can't tell unless you inspect the compiler generated assembly code. Anyway in this case, no destructor at all is created. – Jabberwocky Nov 23 '21 at 16:50
  • _"...In this case, the compiler is going to generate the destructor for us. ..."_ No - live - https://godbolt.org/z/7G9nfvo5Y Please post a [mcve] where you have check your assumptions using one of the online compilers. – Richard Critten Nov 23 '21 at 16:51
  • 1
    Maybe post your code with `unique_ptr`? A destructor for an empty `struct` exists only theoretically; it won't lead to any problems in practice. Also, implementation details, which you want to find out, may depend on what exactly you do. Having a simple but real example would be a good starting point. – anatolyg Nov 23 '21 at 16:52
  • Answers to both your question are "it depends on the implementation". I don't know a C++ compiler that would generate C++ source code for the default implementations, but there is nothing in the standard to prevent compiler writers from implementing it that way. It is perfectly fine for the compiler to generate default code on demand, which implies that it's also fine to not generate default code unless there is a demand for it. – Sergey Kalinichenko Nov 23 '21 at 16:53
  • 1
    Read this : https://en.cppreference.com/w/cpp/language/destructor specifically "Trivial destructor". Which states : A trivial destructor is a destructor that performs no action. Objects with trivial destructors don't require a delete-expression and may be disposed of by simply deallocating their storage. And since there is no action a compiler may completely optimize that away. – Pepijn Kramer Nov 23 '21 at 16:59
  • Please note that I specifically mentioned that other fileds and/or methods can be added to the class in order to make it not trivially constructible/destructible/anything. The provided example is only to demonstrate the absense of any destructor traces, neither declaration nor definition. What would be an acceptable godbolt example that would demonstrate the process of (not) generating the destructor in this case? – Kassiar Nov 23 '21 at 20:47

1 Answers1

4

tl;dr

  • Implicitly declared destructors are declared public inline and at the end of the class definition, e.g.:
    struct A {
      std::string s;
      public: inline ~A(); // implicit destructor
    };
    
  • Destructors only get defined when they're used in the current translation unit.
  • The Destructor Body will be able to use all types that were accessible at the class definition and additionally all types that were accessible at the point were the class was first used.
  • How the compiler generates the code for the destructor and where it'll ultimately end up with is not easily answerable, since it depends on the compiler, compilation flags and even the code that gets compiled.

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.

Turtlefight
  • 9,420
  • 2
  • 23
  • 40
  • 1
    That's a lot of info, but we still don't know what OP asked for. I had a vague guess that the question was "if my program has 100 cpp-files, and the destructor is `inline`, which of the 100 o-files will have its definition?". Not in line with your answer at all. Maybe an unclear question calls for a huge answer? Or none at all? Or a guess? – anatolyg Nov 23 '21 at 19:28
  • First of all, thank you for your time. Let me clarify some points. The reference to the question about `unique_ptr` and incomplete classes was added just to demonstrate that it's not a purely theoretical question on my side, and the exact point at which the destructor is generated and compiled can be important. I'm quite familiar (or at least I'd like to think that) with that problem, and the idea of incomplete classes overall. – Kassiar Nov 23 '21 at 20:59
  • As for the vagueness of the question - I was hoping I stated it quite clearly, but obviously not. Will it be better if I restate it like this - I'd like to know at which exact point during compilation is the compiler-generated destructor actually compiled, and whether it's somehow mandated, or is it purely an implementation detail of the compiler? – Kassiar Nov 23 '21 at 21:02
  • @Kassiar i've updated my answer to (hopefully) answer all the questions you've asked. The only thing that is mandated by the standard is when the destructor needs to be defined (at which point the compiler needs to check if the destructor can be instanciated, and complain e.g. if type definitions are missing). when (or even if at all) it actually gets compiled into something else is entirely up to the compiler (as well as the format the compiler wants to use for this) – Turtlefight Nov 24 '21 at 00:07
  • 1
    Just to make things more complicated, if a base class has virtual destructor and a derived class has implicitly declared destructor, that implicitly-declared destructor is virtual and not pure, and therefore odr-used for no other reason and at no particular location. But in practice I've seen compilers will define that derived destructor if and when any constructor for the derived class is defined (in source or implicitly defined). – aschepler Nov 24 '21 at 00:57
  • 1
    @Turtlefight Once again, thank you! This is an amazing answer, and it's exactly the information I was looking for. And with standard references too! – Kassiar Nov 25 '21 at 03:02