11
votes

The C++ standard defines some very specific behaviors when a class has a trivial constructor and/or a trivial destructor.

As an example, as per §3.8/1 of the standard:

The lifetime of an object of type T ends when:

— if T is a class type with a non-trivial destructor (12.4), the destructor call starts, or

— the storage which the object occupies is reused or released.

So,

  • if an object is not trivialy destructible, any attempt to access members of the object after the destructor is called is UB.
  • if an object is trivialy destructible, attempt to access members of the object after the destructor is called is safe and not UB.

Although this example may not be the best one, it shows that the difference in behavior maybe crucial (UB/non-UB) whether an object is trivialy destructible or not.

§12.4/3 of the Standard states that (to sumerize) a destructor of a class T is trivial if it is implicitely defined, not virtual, and if all base classes and members of class T are trivially destructible.

In my (modest) experience, I never saw any difference, in terms of code generated by the compiler, between :

  • a class with a trivial default ctor and/or a trivial dtor, and
  • a class with a user defined empty ctor and/or a non-virtual user defined empty dtor (as long as the class, its base classes and members classes also have non-virtual dtor user defined empty or trivial)

So, my questions are:

  • In what way a user defined empty ctor/dtor can or cannot be considered as a trivial-like ctor/dtor regarding compiler code generation, optimizations, trade-offs, ...
  • Same question with user defined non-empty ctor/dtor; what rules should follow a code implemented in ctor/dtor to consider them as trivial-like.

My question is not related to standard (please, do not answer the standard states what is a trivial ctor/dtor, so user defined ctor/dtor is not) but to the way compilers deal with user defined ctor/dtor and in what way the behavior of a compiled code may change (or not) compared to trivial ctor/dtor.

2

2 Answers

2
votes

You know the standards better than I, but going on the information you've provided, the standard defines a trivial destructor, but it doesn't define an empty destructor, which would make this question kind of misleading. A trivial destructor is then a special case that compilers can optimize to, and while an empty constructor makes sense to us, it's not something compiler writers have to consider.

Browsing a few SO links:

  • Why Does Default user defined destructors in C++ increases execution time? shows a case where a compiler does act differently for trivial vs. empty destructors. The answer there implies one difference is in exception handling. It doesn't look for an empty constructor because it's not required to, and so handles exceptions as if there was valid code inside the dtor.
  • Will an 'empty' constructor or destructor do the same thing as the generated one? seems to be such a close match to your question that it might be a duplicate. It's better to read it on your own instead of relying on my interpretation, but it makes mention of Microsoft compilers not being able to inline empty destructors, and all compilers wanting a working base class destructor (and it being a very bad programming practice for the base dtor not to be virtual).

To answer your second question, as soon as your ctor is non-empty, it isn't trivial. The closest you get to trivial is an empty ctor/dtor, and your careful reading of the standard already tells you that that's not defined to be trivial.

TL;DR: The standard defines a trivial dtor, but not an empty one. Smart compilers can choose to notice that it's user-defined empty and treat it as trivial, but the standard doesn't require any such consideration.

1
votes

In what way a user defined empty ctor/dtor can or cannot be considered as a trivial-like ctor/dtor regarding compiler code generation, optimizations, trade-offs, ...

If the constructor/destructor are not inlined, then the compiler may (depending on link-time optimizations) have to issue a call to them, even though they're no-ops.

For example, the following code:

struct Struct {
  Struct();
  ~Struct();
};

int main() {
  Struct s;
}

Is compiled to (with optimizations turned on):

main:
        subq    $24, %rsp
        leaq    15(%rsp), %rdi
        call    Struct::Struct()
        leaq    15(%rsp), %rdi
        call    Struct::~Struct()
        xorl    %eax, %eax
        addq    $24, %rsp
        ret

Notice that there is still a call to the constructor and destructor, even though in a separate file I could have defined them to just be empty functions.

If, however, you've inlined the definitions:

struct Struct {
  Struct() {}
  ~Struct() {}
};

Struct foo() {
  return Struct{};
}

Then the compiler can (and will if it doesn't totally suck) treat them just like trivial constructors/destructors:

foo():
        movq    %rdi, %rax
        ret

In that sample, any constructor/destructor calls are completely optimized away, and the generated code is the same as if Struct's definition were simple struct Struct {};.

Same question with user defined non-empty ctor/dtor; what rules should follow a code implemented in ctor/dtor to consider them as trivial-like.

This kind of depends. Again, if the constructor/destructor are not inlined, then the compiler might still have to issue calls to them, in which case they're not at all trivial-like.

However, inline non-empty constructors/destructors might still be "trivial-like" if the optimizer can completely optimize them away (for example, if they only contain for (int x = 0; x < 1000; ++x);, then that is useless code that can be optimized away) to the point that they're effectively empty.

But if they do useful work that can't just be optimized away, then they won't be trivial-like at all. They'll be run. They have to.