244

I originally posted this as a question only about destructors, but now I'm adding consideration of the default constructor. Here's the original question:

If I want to give my class a destructor that is virtual, but is otherwise the same as what the compiler would generate, I can use =default:

class Widget {
public:
   virtual ~Widget() = default;
};

But it seems that I can get the same effect with less typing using an empty definition:

class Widget {
public:
   virtual ~Widget() {}
};

Is there any way in which these two definitions behave differently?

Based on the replies posted for this question, the situation for the default constructor seems similar. Given that there is almost no difference in meaning between "=default" and "{}" for destructors, is there similarly almost no difference in meaning between these options for default constructors? That is, assuming I want to create a type where the objects of that type will be both created and destroyed, why would I want to say

Widget() = default;

instead of

Widget() {}

?

I apologize if extending this question after its original posting is violating some SO rules. Posting an almost-identical question for default constructors struck me as the less desirable option.

TemplateRex
  • 69,038
  • 19
  • 164
  • 304
KnowItAllWannabe
  • 12,972
  • 8
  • 50
  • 91

3 Answers3

150

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.

Community
  • 1
  • 1
Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • 4
    So the key issue seems to be whether the resulting class is trivial, and underlying that issue is the difference between a special function being *user-declared* (which is the case for `=default` functions) and *user-provided* (which is the case for `{}`) functions. Both user-declared and user-provided functions can prevent the generation of other special member function (e.g., a user-declared destructor prevents generation of the move operations), but only a user-provided special function renders a class non-trivial. Right? – KnowItAllWannabe Nov 27 '12 at 19:16
  • @KnowItAllWannabe: That's the general idea, yes. – Nicol Bolas Nov 27 '12 at 19:19
  • I'm choosing this as the accepted answer, only because it covers both constructors and (by reference to Howard's answer) destructors. – KnowItAllWannabe Nov 27 '12 at 22:26
  • Seems to be a missing word in here "as far as the rules of C++11 are concerned, you the rights of a trivial type" I'd fix it but I'm not quuite 100% sure what was intended. – jcoder Nov 28 '12 at 08:50
  • 6
    `= default` seems to be useful for forcing the compiler to generated a default constructor despite the presence of other constructors; the default constructor is not implicitly declared if any other user-declared constructors are provided. – bgfvdu3w Dec 07 '17 at 02:12
  • There's another very important case, where there is a declaration in the class body and an out-of-body definition. Both `= default` and `{}` at the definition are possible and the choice has different effects than it does at an inline definition. – Ben Voigt Apr 29 '18 at 18:33
  • @KnowItAllWannabe Does that mean (addressing your first comment) that a user-declared (i.e. `=default`) might prevent the automatic generation of a copy constructor, for instance? – LCsa Sep 29 '18 at 08:42
53

The important difference between

class B {
    public:
    B(){}
    int i;
    int j;
};

and

class B {
    public:
    B() = default;
    int i;
    int j;
};

is that default constructor defined with B() = default; is considered not-user defined. This means that in case of value-initialization as in

B* pb = new B();  // use of () triggers value-initialization

special kind of initialization that doesn't use a constructor at all will take place and for built-in types this will result in zero-initialization. In case of B(){} this won't take place. The C++ Standard n3337 § 8.5/7 says

To value-initialize an object of type T means:

— if T is a (possibly cv-qualified) class type (Clause 9) with a user-provided constructor (12.1), then the default constructor for T is called (and the initialization is ill-formed if T has no accessible default constructor);

— if T is a (possibly cv-qualified) non-union class type without a user-provided constructor, then the object is zero-initialized and, if T’s implicitly-declared default constructor is non-trivial, that constructor is called.

— if T is an array type, then each element is value-initialized; — otherwise, the object is zero-initialized.

For example:

#include <iostream>

class A {
    public:
    A(){}
    int i;
    int j;
};

class B {
    public:
    B() = default;
    int i;
    int j;
};

int main()
{
    for( int i = 0; i < 100; ++i) {
        A* pa = new A();
        B* pb = new B();
        std::cout << pa->i << "," << pa->j << std::endl;
        std::cout << pb->i << "," << pb->j << std::endl;
        delete pa;
        delete pb;
    }
  return 0;
}

possible result:

0,0
0,0
145084416,0
0,0
145084432,0
0,0
145084416,0
//...

http://ideone.com/k8mBrd

Patryk
  • 22,602
  • 44
  • 128
  • 244
4pie0
  • 29,204
  • 9
  • 82
  • 118
  • Then, why do "{}" and "= default" always initialize an std::string http://ideone.com/LMv5Uf ? – nawfel bgh Feb 01 '16 at 22:54
  • 3
    @nawfelbgh The default constructor A(){} calls default constructor for std::string as this is non-POD type. The default ctor of std::string [initializes it](http://stackoverflow.com/a/17738467/1141471) to empty, 0 size string. The default ctor for scalars does nothing: the objects with automatic storage duration (and their subobjects) are initialized to indeterminate values. – 4pie0 May 06 '16 at 19:30
  • 3
    Just to be clear with the example above, class B always results in the value 0: https://ideone.com/XOcHNq A: 0,0 B: 0,0 A: 145084416,0 B: 0,0 A: 145084432,0 B: 0,0 A: 145084416,0 //... – JSoet Oct 30 '20 at 21:08
49

They are both non-trivial.

They both have the same noexcept specification depending upon the noexcept specification of the bases and members.

The only difference I'm detecting so far is that if Widget contains a base or member with an inaccessible or deleted destructor:

struct A
{
private:
    ~A();
};

class Widget {
    A a_;
public:
#if 1
   virtual ~Widget() = default;
#else
   virtual ~Widget() {}
#endif
};

Then the =default solution will compile, but Widget won't be a destructible type. I.e. if you try to destruct a Widget, you'll get a compile-time error. But if you don't, you've got a working program.

Otoh, if you supply the user-provided destructor, then things won't compile whether or not you destruct a Widget:

test.cpp:8:7: error: field of type 'A' has private destructor
    A a_;
      ^
test.cpp:4:5: note: declared private here
    ~A();
    ^
1 error generated.
BartoszKP
  • 34,786
  • 15
  • 102
  • 130
Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • 12
    Interesting: in other words, with `=default;` the compiler won't generate the destructor unless it's used, and therefore won't trigger an error. This seems weird to me, even if not necessarily a bug. I can't imagine this behavior is *mandated* in the standard. – Nik Bougalis Nov 27 '12 at 06:51
  • 2
    "Then the =default solution will compile" No it will not. Just tested in vs. – Minimus Heximus Jan 31 '19 at 11:23
  • 2
    What was the error message, and what version of VS? – Howard Hinnant Jan 31 '19 at 13:51
  • If I'm not mistaken, you need to allocate the object on the heap for it to be a valid test - declaring a local `Widget` automatically tries to destroy it, so it won't compile, but writing `new Widget` would work in the `=default` case as long as you never `delete` it. – celticminstrel Jan 06 '22 at 00:00