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_assert
s. 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.