How Is "=Default" Different from "{}" for Default Constructor and Destructor

How is =default different from {} for default constructor and destructor?

This is a completely different question when asking about constructors than destructors.

If your destructor is virtual, then the difference is negligible, as Howard pointed out. However, if your destructor was non-virtual, it's a completely different story. The same is true of constructors.

Using = default syntax for special member functions (default constructor, copy/move constructors/assignment, destructors etc) means something very different from simply doing {}. With the latter, the function becomes "user-provided". And that changes everything.

This is a trivial class by C++11's definition:

struct Trivial
{
int foo;
};

If you attempt to default construct one, the compiler will generate a default constructor automatically. Same goes for copy/movement and destructing. Because the user did not provide any of these member functions, the C++11 specification considers this a "trivial" class. It therefore legal to do this, like memcpy their contents around to initialize them and so forth.

This:

struct NotTrivial
{
int foo;

NotTrivial() {}
};

As the name suggests, this is no longer trivial. It has a default constructor that is user-provided. It doesn't matter if it's empty; as far as the rules of C++11 are concerned, this cannot be a trivial type.

This:

struct Trivial2
{
int foo;

Trivial2() = default;
};

Again as the name suggests, this is a trivial type. Why? Because you told the compiler to automatically generate the default constructor. The constructor is therefore not "user-provided." And therefore, the type counts as trivial, since it doesn't have a user-provided default constructor.

The = default syntax is mainly there for doing things like copy constructors/assignment, when you add member functions that prevent the creation of such functions. But it also triggers special behavior from the compiler, so it's useful in default constructors/destructors too.

What's the difference between = default destructor and empty destructor?

Your first example should not compile. This represents a bug in the compiler that it does compile. This bug is fixed in gcc 4.9 and later.

The destructor defined with = default is trivial in this case. This can be detected with std::is_trivially_destructible<A>::value.

Update

C++11 (and C++14) state that if one has a user-declared destructor (and if you don't have either user-declared move special member), then the implicit generation of the copy constructor and copy assignment operator still happen, but that behavior is deprecated. Meaning if you rely on it, your compiler might give you a deprecation warning (or might not).

Both:

~A() = default;

and:

~A() {};

are user-declared, and so they have no difference with respect to this point. If you use either of these forms (and don't declare move members), you should explicitly default, explicitly delete, or explicitly provide your copy members in order to avoid relying on deprecated behavior.

If you do declare move members (with or without declaring a destructor), then the copy members are implicitly deleted.

When to make a destructor defaulted using =default?

(Several of the points below were already mentioned in comments or in the linked questions; this answer serves to organize and interrelate them.)

There are of course three ways to get a “simple destructor”:

struct Implicit {};
struct Empty {~Empty() {}};
struct Defaulted {~Defaulted()=default;};

Like a default (and not a copy or move) constructor, {} and =default; mean largely the same thing for destructors. The interesting properties of Defaulted are then those (combinations) that differ from both of the others.

Versus Empty the main difference is simple: the explicitly defaulted destructor can be trivial. This applies only if it is defaulted inside the class, so there is no difference between {} and =default; on an out-of-line definition. Similarly, being virtual removes any distinction, as does having any member or base class with a non-trivial destructor. There is also the distinction that an explicitly defaulted destructor can be implicitly defined as deleted. Both of these properties are shared with implicitly declared destructors, so we have to find a distinction from those as well.

Versus Implicit, an explicitly defaulted destructor suppresses move operations, can be declared private, protected, or noexcept(false), and in C++20 can be constrained (but not consteval). Very marginally, it can be declared constexpr to verify that it would be anyway. Declaring it inline doesn’t do anything. (It can also be out of line or virtual, but as stated above that can’t be a reason to use it.)

So the answer is “when you want a trivial (or potentially deleted) destructor that has other special properties”—most usefully, access control or noexcept status.

Do you always have to declare a default constructor and destructor for unions containing members with user-defined default constructor?

It seems silly to write an empty constructor and destructor for ...

Not only silly, but actually wrong.

Why [does the compiler doesn't generate one and you need to]?

A union doesn't know which of its members is active. As such if at least one of its members has a non-trivial special method the union can't generate that special method because it doesn't know on which member to call that special method.

And now we come to your case. You do need to write a special method, but having it empty achieves nothing. You need to write a special method that correctly delegates to the active member. For instance inside foo you can have a data member (an enum or an index) which tells you which union member is active. And in your union special method you check that index and call the special method on the active member of the union.

Or you can forget about all this and just use the type in the standard library that not only has everything set up for you, but also is type safe: std::variant.

Where should a default destructor C++11 style go, header or cpp?

You can do both:

  • in the first case (header) the destructor will be considered as non-user defined
  • in the second case (cpp) the compiler will consider it as user defined.

A user-provided destructor is non-trivial, making the class itself necessarily non-trivial.

Unless you have a good reason to for the second option, putting it in the header is the usual way to go.

Should I default virtual destructors?

Yes, you can definitely use = default for such destructors. Especially if you were just going to replace it with {}. I think the = default one is nicer, because it's more explicit, so it immediately catches the eye and leaves no room for doubt.

However, here are a couple of notes to take into consideration when doing so.

When you = default a destructor in the header file (see edit) (or any other special function for that matter), it's basically defining it in the header. When designing a shared library, you might want to explicitly have the destructor provided only by the library rather than in the header, so that you could change it more easily in the future without requiring a rebuild of the dependent binary. But again, that's for when the question isn't simply whether to = default or to {}.


EDIT: As Sean keenly noted in the comments, you can also use = default outside of the class declaration, which gets the best of both worlds here.


The other crucial technical difference, is that the standard says that an explicitly defaulted function that can't be generated, will simply not be generated. Consider the following example:

struct A { ~A() = delete; };
struct B : A { ~B() {}; }

This would not compile, since you're forcing the compiler to generate the specified code (and its implicit requisites, such as calling A's destructor) for B's destructor--and it can't, because A's destructor is deleted. Consider this, however:

struct A { ~A() = delete; };
struct B : A { ~B() = default; }

This, in fact, would compile, because the compiler sees that ~B() cannot be generated, so it simply doesn't generate it--and declares it as deleted. This means that you'll only get the error when you're trying to actually use/call B::~B().

This has at least two implications which you should be aware of:

  1. If you're looking to get the error simply when compiling anything that includes the class declaration, you won't get it, since it's technically valid.
  2. If you initially always use = default for such destructors, then you won't have to worry about your super class's destructor being deleted, if it turns out it's ok and you never really use it. It's kind of an exotic use, but to that extent it's more correct and future-proof. You'll only get the error if you really use the destructor--otherwise, you'll be left alone.

So if you're going for a defensive programming approach, you might want to consider simply using {}. Otherwise, you're probably better off = defaulting, since that better adheres to taking the minimum programmatic instructions necessary to get a correct, working codebase, and avoiding unintended consequences1.


As for = 0: Yes, that's still the right way to do it. But please note that it in fact does not specify that there's "no default implementation," but rather, that (1) The class is not instantiatable; and (2) Any derived classes must override that function (though they can use an optional implementation provided by the super class). In other words, you can both define a function, and declare it as pure virtual. Here's an example:

struct A { virtual ~A() = 0; }
A::~A() = default;

This will ensure these constraints on A (and its destructor).


1) A good example of why this can be useful in unexpected ways, is how some people always used return with parentheses, and then C++14 added decltype(auto) which essentially created a technical difference between using it with and without parentheses.

The new syntax = default in C++11

A defaulted default constructor is specifically defined as being the same as a user-defined default constructor with no initialization list and an empty compound statement.

§12.1/6 [class.ctor] A default constructor that is defaulted and not defined as deleted is implicitly defined when it is odr-used to create an object of its class type or when it is explicitly defaulted after its first declaration. The implicitly-defined default constructor performs the set of initializations of the class that would be performed by a user-written default constructor for that class with no ctor-initializer (12.6.2) and an empty compound-statement. [...]

However, while both constructors will behave the same, providing an empty implementation does affect some properties of the class. Giving a user-defined constructor, even though it does nothing, makes the type not an aggregate and also not trivial. If you want your class to be an aggregate or a trivial type (or by transitivity, a POD type), then you need to use = default.

§8.5.1/1 [dcl.init.aggr] An aggregate is an array or a class with no user-provided constructors, [and...]

§12.1/5 [class.ctor] A default constructor is trivial if it is not user-provided and [...]

§9/6 [class] A trivial class is a class that has a trivial default constructor and [...]

To demonstrate:

#include <type_traits>

struct X {
X() = default;
};

struct Y {
Y() { };
};

int main() {
static_assert(std::is_trivial<X>::value, "X should be trivial");
static_assert(std::is_pod<X>::value, "X should be POD");

static_assert(!std::is_trivial<Y>::value, "Y should not be trivial");
static_assert(!std::is_pod<Y>::value, "Y should not be POD");
}

Additionally, explicitly defaulting a constructor will make it constexpr if the implicit constructor would have been and will also give it the same exception specification that the implicit constructor would have had. In the case you've given, the implicit constructor would not have been constexpr (because it would leave a data member uninitialized) and it would also have an empty exception specification, so there is no difference. But yes, in the general case you could manually specify constexpr and the exception specification to match the implicit constructor.

Using = default does bring some uniformity, because it can also be used with copy/move constructors and destructors. An empty copy constructor, for example, will not do the same as a defaulted copy constructor (which will perform member-wise copy of its members). Using the = default (or = delete) syntax uniformly for each of these special member functions makes your code easier to read by explicitly stating your intent.

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

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.

When to use =default vs =delete

Good question.

Also important: Where to use = default and = delete.

I have somewhat controversial advice on this. It contradicts what we all learned (including myself) for C++98/03.

Start your class declaration with your data members:

class MyClass
{
std::unique_ptr<OtherClass> ptr_;
std::string name_;
std::vector<double> data_;
// ...
};

Then, as close as is practical, list all of the six special members that you want to explicitly declare, and in a predictable order (and don't list the ones you want the compiler to handle). The order I prefer is:

  1. destructor // this tells me the very most important things about this class.
  2. default constructor
  3. copy constructor // I like to see my copy members together
  4. copy assignment operator
  5. move constructor // I like to see my move members together
  6. move assignment operator

The reason for this order is:

  • Whatever special members you default, the reader is more likely to understand what the defaults do if they know what the data members are.
  • By listing the special members in a consistent place near the top, and in a consistent order, the reader is more likely to quickly realize which special members are not explicitly declared ‐ and thus either implicitly declared, or not do not exist at all.
  • Typically both copy members (constructor and assignment) are similar. Either both will
    be implicitly defaulted or deleted, explicitly defaulted or deleted, or explicitly supplied. It is nice to confirm this in two lines of code right next to each other.
  • Typically both move members (constructor and assignment) are similar...

For example:

class MyClass
{
std::unique_ptr<OtherClass> ptr_;
std::string name_;
std::vector<double> data_;
public:
MyClass() = default;
MyClass(const MyClass& other);
MyClass& operator=(const MyClass& other);
MyClass(MyClass&&) = default;
MyClass& operator=(MyClass&&) = default;
// Other constructors...
// Other public member functions
// friend functions
// friend types
// private member functions
// ...
};

Knowing the convention, one can quickly see, without having to examine the entire class declaration that ~MyClass() is implicitly defaulted, and with the data members nearby, it is easy to see what that compiler-declared and supplied destructor does.

Next we can see that MyClass has an explicitly defaulted default constructor, and with the data members declared nearby, it is easy to see what that compiler-supplied default constructor does. It is also easy to see why the default constructor has been explicitly declared: Because we need a user-defined copy constructor, and that would inhibit a compiler-supplied default constructor if not explicitly defaulted.

Next we see that there is a user-supplied copy constructor and copy assignment operator. Why? Well, with the data members nearby, it is easy to speculate that perhaps a deep-copy of the unique_ptr ptr_ is needed. We can't know that for sure of course without inspecting the definition of the copy members. But even without having those definitions handy, we are already pretty well informed.

With user-declared copy members, move members would be implicitly not declared if we did nothing. But here we easily see (because everything is predictably grouped and ordered at the top of the MyClass declaration) that we have explicitly defaulted move members. And again, because the data members are nearby, we can immediately see what these compiler-supplied move members will do.

In summary, we don't yet have a clue exactly what MyClass does and what role it will play in this program. However even lacking that knowledge, we already know a great deal about MyClass.

We know MyClass:

  • Holds a uniquely owning pointer to some (probably polymorphic) OtherClass.
  • Holds a string serving as a name.
  • Holds a bunch of doubles severing as some kind of data.
  • Will properly destruct itself without leaking anything.
  • Will default construct itself with a null ptr_, empty name_ and data_.
  • Will copy itself, not positive exactly how, but there is a likely algorithm we can easily check elsewhere.
  • Will efficiently (and correctly) move itself by moving each of the three data members.

That's a lot to know within 10 or so lines of code. And we didn't have to go hunting through hundreds of lines of code that I'm sure are needed for a proper implementation of MyClass to learn all this: because it was all at the top and in a predictable order.

One might want to tweak this recipe say to place nested types prior to the data members so that the data members can be declared in terms of the nested types. However the spirit of this recommendation is to declare the private data members, and special members, both as close to the top as practical, and as close to each other as practical. This runs contrary to advice given in the past (probably even by myself), that private data members are an implementation detail, not important enough to be at the top of the class declaration.

But in hindsight (hindsight is always 20/20), private data members, even though being inaccessible to distant code (which is a good thing) do dictate and describe the fundamental behaviors of a type when any of its special members are compiler-supplied.
And knowing what the special members of a class do, is one of the most important aspects of understanding any type.

  • Is it destructible?
  • Is it default constructible?
  • Is it copyable?
  • Is it movable?
  • Does it have value semantics or reference semantics?

Every type has answers to these questions, and it is best to get these questions & answers out of the way ASAP. Then you can more easily concentrate on what makes this type different from every other type.



Related Topics



Leave a reply



Submit