C++ tidbit #8: Damaging Default Destructor
Special member functions are implicitly generated by the compiler if the user didn't provide them - constructors, destructors, assignments, and moves. Here , for example, a move ctor for C is used although it was never declared or defined in code:
struct C {
std::string s;
};
void f() {
? ? C c1;
? ? c1.s = "asdf";
? ? C c2 = std::move(c1); // <-- calls an auto-generated C(C&&)
}
Kinda-Reasonable, Part I
Auto generation of move ctors - as well as of some other special member functions - is dependent on the compiler being certain that it doesn't need to do anything fancy. This does not entail any real fancy-detection logic: per the standard, if the user has provided some (for example) copy ctor - copy operations aren't trivial and it would be a mistake to let the compiler implicitly generate move, that could be missing needed functionality. Similarly, if the code has a user-given dtor, something non-automatic probably needs to be done to the object that is the potential move target - and implicit generation of moves (ctor and assignment) is inhibited.
Note that 'user given' could mean an empty definition like `{}`- no analysis whatsoever is applied to the special members in question:
struct C
std::string s;
~C() {} // Prevents implicitly generating C::C(C&&)
};
void f() {?
? C c1;?
? c1.s = "asdf";?
? C c2 = std::move(c1); // <-- now calls C::C(C const&)
}
Let it be said already that this reasoning isn't entirely sound. The line `C c2 = ...` does not involve any destruction of previous `c2` contents, and one could imagine a C++ standard in which an auto-generated move would apply here.
Still-Mostly-Reasonable, Part II
`default constructor` was a very unfortunate historical choice of term for a ctor with no parameters. In C++11 true `defaults` joined in:
C() = default;
~C() = default;
C(C&) = default;
C(C&&) = default;
...
In C++ jargon these are called 'defaulted' to distinguish them from 'default', and confusing idioms like 'defaulted default constructor' started to appear. The motivation for these was mostly to re-enable implicit special members that were inhibited by the logic in part I:
struct C {
? ? std::string s;
? ? ~C() {}; // <-- prevents implicit move ctor
? ? C(C&&) = default; // <-- restores move-ctor functionality
// but inhibits default (=no-param) ctor
? ? C() = default; // <-- restores defaulted-default ctor
};
void f() {
? ? C c1;
? ? c1.s = "asdf";
? ? C c2 = std::move(c1); <-- Huzzah! move is called again
}
Another minor motivation for `=default` was to easily change the access specifier of a special member function without touching its implementation, and you sometimes see 'better code documentation' as an added value. Some good discussion is available in MS docs .
领英推荐
Utter Madness, Part III
Here's some more exact wording for the implicit-move inhibition rules, quoting cppreference :
" If no user-defined move assignment operators are provided ..., and all of the following is true:
then the compiler will declare a move assignment operator... ."
This phrasing contains a small but potent landmine: user-declared.
Not provided, declared.
For reasons unbeknownst to me, `=default` counts as user-declared.
In this code, implicit move is inhibited:
struct C
? ? std::string s;
? ? ~C() = default; // <-- !
};
void f() {
? ? C c1;
? ? c1.s = "asdf";
? ? C c2 = std::move(c1); // <-- no move. !?
}
You might have reasonably assumed that spreading `=default`s around is at worst meaningless. It isn't - at worst it's a (potentially substantial) pessimization.
<sigh/>
There might be some real use cases lurking out of my sight where this is in fact the desired impact of `=default` (do tell me if you have one!) - but I suspect this is just another accidental side effect of the standard complexity. If this was really a design choice by the committee - it certainly brought about more confusion then benefit.
Software architect at ViNotion / Owner at Kris van Rens / Trainer at High Tech Institute
1 年Yes, you're right. The "defaulted default constructor" is confusing. But honestly I usually just refer to it as "user-declared ..." in general to avoid this confusion. User-declared either means: 1) custom implementation, 2) '=default;', (request a default implementation), or 3) '=delete;'; yes, this also counts as user-declared, and means something like: "declared, but marked as unusable". This means "user-declared" always participates in overload resolution! Officially the behavior to implicitly generate the copy operations in case of a user-declared destructor (or another copy operation) is deprecated. But consider this case: you have pre-C++11 code in use in your post-C++11 code base, and there is a user-declared destructor (which means the move ops will not be declared as per the rules of the standard). Then, if this type is "moved" (e.g. using std::move) in some context, it is nice that the copy-fallback is actually in place (so the copy ops *are* generated in practice!). Otherwise legacy pre-C++11 without user-declared move operations for this type would fail in this context. Just sprinkling '= default;' around is never useful. Use the rule of zero or all, or a more intelligent guideline for specific categories of types.
Co-Founder & CTO at DragonflyDB
2 年The fact that there is no visibility to which c'tor is called as a result of some action is super confusing in c++. The same applies to hidden allocations in lambdas.