12

When defining classes, it is now common to use = default for the destructor/copy constructor and copy assignment. Looking at my code base, these are nearly always in the header files only but some colleagues have put them in the .cpp file. What would be the best practice in this case?

Is the compiler generating these functions multiple times when it is in the header and relying on the linker to dedup them. Is it perhaps only worth putting them in the .cpp file if you have a huge class? With our mostly old C++98 code, functions that do nothing are also often defined only in the header. Do nothing virtual destructors seem to have often been moved to the .cpp file. Is (or was) it somehow important for virtual methods where their address is needed to populate the virtual method table.

Also is it recommended to ever put noexcept() clauses on = default functions? The compiler appears to derive this itself so it only serves as API documentation if it is there.

okapi
  • 1,340
  • 9
  • 17
  • Do you mean putting `MyClass() = default;` in the class definition in the header vs putting `MyClass::MyClass() = default` as the constructor definition in the cpp? – Kevin Jun 25 '20 at 16:50
  • 2
    This one bit me the other day: if you have a `std::unique_ptr` member, and want to hide the `forward_declared_class`, you need to put the destructor in the .cpp. Otherwise anyone that includes the header will also need the definition of the class that you want to hide. – molbdnilo Jun 25 '20 at 16:54
  • @molbdnilo PIMPL with unique pointer is a well-known such case. eerorika's answer should get more attention, since both other more-voted answers strongly recommend header-file-only solutions. – Daniel Langr Jun 25 '20 at 17:57
  • 1
    @DanielLangr imo the subtleties of defining explicitly-defaulted special member functions, particularly for the default constructor, elsewhere than their first declarations, combined with the common misconception of failsafeness of ”uniform initialization syntax” `S s{};` makes, imho, for a perfect recipe for UB disaster in the numerous code bases out there where the majority of the developers are not aware of these details, and where ”patterns” may quickly spread without being understood. For PIMPL implementations, even if the explicitly-default constructor is perfectly fine for the ... – dfrib Jun 25 '20 at 18:35
  • 1
    ... particular use case, it could be worth considering actually explicitly defining it yourself, e.g. via a typically semantically sound well-ordered member initializer list. It’s good that there are different answers, though, but for me safety always trumps cleverness (I do work in a safety critical domain, though). – dfrib Jun 25 '20 at 18:37

4 Answers4

16

An explicitly-defaulted function is not necessarily not user-provided

What would be the best practice in this case?

I would recommend, as a rule of thumb, unless you explicitly and wantonly know what you are getting into, to always define explicitly-defaulted functions at their (first) declaration; i.e., placing = default at the (first) declaration, meaning in (your case) the header (specifically, the class definition), as there are subtle but essential differences between the two w.r.t. whether a constructor is considered to be user-provided or not.

From [dcl.fct.def.default]/5 [extract, emphasis mine]:

[...] A function is user-provided if it is user-declared and not explicitly defaulted or deleted on its first declaration. [...]

Thus:

struct A {
    A() = default; // NOT user-provided.
    int a;
};


struct B {
    B(); // user-provided.
    int b;
};

// A user-provided explicitly-defaulted constructor.
B::B() = default;

Whether a constructor is user-provided or not does, in turn, affect the rules for which objects of the type are initialized. Particularly, a class type T, when value-initialized, will first zero-initialize the object if T's default constructor is not user-provided. Thus, this guarantee holds for A above, but not for B, and it can be quite surprising that a value-initialization of an object with a (user-provided!) defaulted constructor leaves data members of the object in an uninitialized state.

Quoting from cppreference [extract, emphasis mine]:

Value initialization

Value initialization is performed in these situations:

  • [...]
  • (4) when a named variable (automatic, static, or thread-local) is declared with the initializer consisting of a pair of braces.

The effects of value initialization are:

  • (1) if T is a class type with no default constructor or with a user-provided or deleted default constructor, the object is default-initialized;

  • (2) if T is a class type with a default constructor that is neither user-provided nor deleted (that is, it may be a class with an implicitly-defined or defaulted default constructor), the object is zero-initialized and then it is default-initialized if it has a non-trivial default constructor;

  • ...

Let's apply this on the class types A and B above:

A a{};
// Empty brace direct-list-init:
// -> A has no user-provided constructor
// -> aggregate initialization
// -> data member 'a' is value-initialized
// -> data member 'a' is zero-initialized

B b{};
// Empty brace direct-list-init:
// -> B has a user-provided constructor
// -> value-initialization
// -> default-initialization
// -> the explicitly-defaulted constructor will
//    not initialize the data member 'b'
// -> data member 'b' is left in an unititialized state

a.a = b.b; // reading uninitialized b.b: UB!

Thus, even for use cases where you will not end up shooting yourself in the foot, just the presence of a pattern in your code base where explicitly defaulted (special member) functions are not being defined at their (first) declarations may lead to other developers, unknowingly of the subtleties of this pattern, blindly following it and subsequently shooting themselves in their feet instead.

dfrib
  • 70,367
  • 12
  • 127
  • 192
  • 3
    Have an upvote. I didn't know you could write `B::B() = default;`. I learnt something today! – Bathsheba Jun 25 '20 at 16:54
  • Thanks, I've been fixing my code. One case that came up where it didn't work was where a class contains a unique_ptr for a forward declared class. Apparently it needs the size in order to create the destructor. I'm reluctant to change the forward declaration to a `#include`, or should I? I think some of the minimal destructors were needed to avoid the [missingkeyfunction error](https://lld.llvm.org/missingkeyfunction). – okapi Jun 26 '20 at 16:00
  • 1
    @okapi This mainly applies for initialization; as always there other special cases, e.g. indirection idioms such as described in eerorika’s answer (PIMPL) which requires separating the declaration of the special member functions from their definitions. Even in this case, though, I would not recommend using `= default` for the out-of-header definition for the _user-provided_ default constructor (for PIMPL we generally have a customly defined default constructor anyway). – dfrib Jun 26 '20 at 16:10
6

Functions declared with = default; should go in the header file, and the compiler will automatically know when to mark them noexcept. We can actually observe this behavior, and prove that it happens.

Let's say that we have two classes, Foo and Bar. The first class, Foo, contains an int, and the second class, Bar, contains a string. These are the definitions:

struct Foo {
    int x;
    Foo() = default;
    Foo(Foo const&) = default;
    Foo(Foo&&) = default;
};

struct Bar {
    std::string s;
    Bar() = default;
    Bar(Bar const&) = default;
    Bar(Bar&&) = default;
};

For Foo, everything is noexcept because creating, copying, and moving an integer is noexcept. For Bar on the other hand, creating and moving strings are noexcept, but copy construction is not because it might require allocating memory, which might result in an exception if there is no more memory.

We can check if a function is noexcept by using noexcept:

std::cout << noexcept(Foo()) << '\n'; // Prints true, because `Foo()` is noexcept

Lets do this for all constructors in Foo and Bar:

// In C++, # will get a string representation of a macro argument
// So #x gets a string representation of x
#define IS_NOEXCEPT(x) \
  std::cout << "noexcept(" #x ") = \t" << noexcept(x) << '\n';
  
int main() {
    Foo f;
    IS_NOEXCEPT(Foo()); // Prints true
    IS_NOEXCEPT(Foo(f)) // Prints true
    IS_NOEXCEPT(Foo(std::move(f))); // Prints true
    
    Bar b;
    IS_NOEXCEPT(Bar()); // Prints true
    IS_NOEXCEPT(Bar(b)) // Copy constructor prints false
    IS_NOEXCEPT(Bar(std::move(b))); // Prints true
}

This shows us that the compiler will automatically deduce whether or not a defaulted function is noexcept. You can run the code for yourself here

Alecto Irene Perez
  • 10,321
  • 23
  • 46
5

Should functions declared with = default only go in the header file

Usually, the class definition is the ideal place to put the default definition.

However, sometimes that is not an option. In particular, if the class definition cannot depend on the definition of an indirect member. An example of where this is the case is the implementation of PIMPL pattern using unique pointer to opaque type.

eerorika
  • 232,697
  • 12
  • 197
  • 326
0

Preamble

My comments about = default or even noexcept doesn't apply to templated types like containers, which are, by design, fully in the headers (which is not the case described by the question), and whose noexcept-ness might depend on the type parameter of the template.

1 - TL;DR

What would be the best practice in this case?

You should, by default, write = default in the .cpp file.

Is the compiler generating these functions multiple times when it is in the header and relying on the linker to dedup them

Yes. If translation units are compiled separately, and if the functions are used in those T.U., they will be generated. Hopefully, the linker will clean that up, but in case of DLLs/shared objects, each might have its own separate copy. You might risk compilation performance problems/code bloat.

Also is it recommended to ever put noexcept() clauses on = default functions? The compiler appears to derive this itself so it only serves as API documentation if it is there.

noexcept might be part of the interface of your type. Leaving it implicit means your function's noexcept-ness might change (e.g. with member variable change) without the author nor the user noticing.

IMHO, this is a design choice that should be explicit: If, as its author, a function being noexcept is important for you, then don't rely on it implicitly. Instead explicitly qualify it with noexcept. (And also try to make sure its implementation doesn't throw).

2 - Discussing other answers

Please Note that, for very specific cases, these answers are right (and I learned a lot from them). But I'll explain below why they might not be that relevant for the general case.

2.A - Type initialization should be done by the author of the type, not its user

The use-case...:

A a{};
B b{};
a.a = b.b; // reading uninitialized b.b: UB!

... is problematic.

In C++, as a code author, you should strive (unless very specific cases) to make sure all your user-defined types are once and for all fully and automatically initialized at construction, not manually initialized each time by the user (the {} braces).

In other words, the example is not representative of desirable production code. A more representative use-case would be...:

A a;        // no braces needed!
B b;        // no braces needed!
a.a = b.b;

And for this user code to work properly, what you need to do is initialize the member variables, as in:

struct A {
    int a = 0;
};
struct B {
    int b = 0;
};

This is why I believe the use of = default to "guarantee" proper initialization should not be applied to 95% of the production code.

2.B - noexcept should be explicit for normal types because it's part of their interfaces

noexcept has two uses:

  • compiler optimizations
  • writing higher-quality code (e.g. exception-safe code).

When a function's noexcept is implicit, to know about it, a human user will need careful reviewing, or a massive use of static_asserts. This results in that code being less readable (including for its author, years after).

Instead, consider explicitly writing noexcept or noexcept(false) as much as you can.

3 - My 2 cents: the answer depends on your codebase

If your codebase is of moderate size, or larger, and/or if you have multiple developers working on it (including you, years after), you might want to not incur an invisible technical debt:

Advice 1: Hide your implementation as much as you can

In the header below, the difference between A and B...:

struct A {
   A() = default;
   // etc.
};

struct B {
   B();  // declared as B::B() = default in the .cpp file
   // etc.
};

... is subtle:

  • A tells the whole world an implementation detail (which thus become part of the interface of A).
  • while B keeps that detail hidden (which means its author can change it without any user noticing it).

Also, noexcept is not an implementation detail: It's an interface guarantee, so be as explicit as possible with what you want to guarantee (or not).

Advice 2: Avoid inlining unless you can prove this makes your program better

The second case is like the other side of the coin of the first case, but less philosophical, and more practical.

Code inlining costs more from a maintenance viewpoint...:

  • Compilation is slower, takes more memory
  • A change means user code must be recompiled

In other words, in large codebase, inlining will slow your development cycle.

... and it probably doesn't have the benefits one might think it has:

  • Inlining as an optimization is decided by the compiler, not by explicitly declaring implementation details "inline"
  • Forced inlining can actually pessimize optimization (you are interfering with the compiler's optimization attempts, for example, by causing code bloat)

Your question might be: "How's that relevant with = default?"

= default in the header is one form of inlining. With a small class, no one cares. But as your class grows, it might become problematic. Imagine:

  • a large class (lots of member variables)
  • with its default constructor = default in a header
  • with that header included and used in 1000 modules
  • each module being a DLL/shared object

This means the implementation of that = default constructor will actually be done in each of the separate module.

And now, imagine all your classes, in that 1000+ DLLs codebase, use = default.

The compilation slowness/code bloat might be exponentially problematic.

paercebal
  • 81,378
  • 38
  • 130
  • 159
  • The move of a defaulted special member into a source file is not necessarily faster, regarding compilation speed, actually you are producing more work. Only in the case, where you have plenty of pointer-like fields to forward declared types, the overhead of loading and parsing another header will be higher than the link penalty. Also, other drawbacks, should be considered (probably slower, incorrect, implicit deletion of other special members...) You should update your TLDR: Always try to default special members in the header, unless you can replace includes with forward declarations. – Fabian Keßler Apr 06 '23 at 08:57