15

While reading the Bjarne Stroustrup's CoreCppGuidelines, I have found a guideline which contradicts my experience.

The C.21 requires the following:

If you define or =delete any default operation, define or =delete them all

With the following reason:

The semantics of the special functions are closely related, so if one needs to be non-default, the odds are that others need modification too.

From my experience, the two most common situations of redefinition of default operations are the following:

#1: Definition of virtual destructor with default body to allow inheritance:

class C1
{
...
    virtual ~C1() = default;
}

#2: Definition of default constructor making some initialization of RAII-typed members:

class C2
{
public:
    int a; float b; std::string c; std::unique_ptr<int> x;

    C2() : a(0), b(1), c("2"), x(std::make_unique<int>(5))
    {}
}

All other situations were rare in my experience.

What do you think of these examples? Are they exceptions of the C.21 rule or it's better to define all default operations here? Are there any other frequent exceptions?

user3840170
  • 26,597
  • 4
  • 30
  • 62
alexeykuzmin0
  • 6,344
  • 2
  • 28
  • 51
  • With #2 if you copy or assign the class what do you expect to happen to the unique_ptr? – Richard Critten Jul 31 '16 at 10:04
  • With `std::unique_ptr` I expect the class to be non-copyable, and the default movement assignment works perfectly well: the old `std::unique_ptr` is destroyed, and the new one is moved to the field `x`. – alexeykuzmin0 Jul 31 '16 at 10:49
  • May be worth raising this as an issue on github https://github.com/isocpp/CppCoreGuidelines/issues – Galik Jul 31 '16 at 11:09
  • 11
    Re (1). If you define a virtual destructor, you expect the class to be manipulated through pointers and references. In that case, copy is usually a disaster (slicing), so the default copy and move operations are almost certainly wrong. If you really want those, define them appropriately. – Bjarne Stroustrup Jul 31 '16 at 13:16
  • @BjarneStroustrup thx for the explanation, now understand why example #1 is not an exception – alexeykuzmin0 Jul 31 '16 at 13:31
  • @BjarneStroustrup If you define the destructor the move operations don't even exist. – NQA Jul 31 '16 at 13:49
  • I think the guideline holds. It's important to remember that it is a guideline, not a law of the universe. You probably want all defaulted members customized if you customize one of them, but there are exceptions. Constructors are the most likely exception, since it is common to customize them, and it is safe to pass in and store RAII types using the defaulted functions. – Marc Dec 17 '16 at 17:55

2 Answers2

14

I have significant reservations with this guideline. Even knowing that it is a guideline, and not a rule, I still have reservations.

Let's say you have a user-written class similar to std::complex<double>, or std::chrono::seconds. It is just a value type. It doesn't own any resources, it is meant to be simple. Let's say it has a non-special-member constructor.

class SimpleValue
{
    int value_;
public:
    explicit SimpleValue(int value);
};

Well, I also want SimpleValue to be default constructible, and I've inhibited the default constructor by providing another constructor, so I need to add that special member:

class SimpleValue
{
    int value_;
public:
    SimpleValue();
    explicit SimpleValue(int value);
};

I fear that people will memorize this guideline and reason: Well, since I've provided one special member, I should define or delete the rest, so here goes...

class SimpleValue
{
    int value_;
public:
    ~SimpleValue() = default;
    SimpleValue();
    SimpleValue(const SimpleValue&) = default;
    SimpleValue& operator=(const SimpleValue&) = default;

    explicit SimpleValue(int value);
};

Hmm... I don't need move members, but I need to mindlessly follow what the wise ones have told me, so I'll just delete those:

class SimpleValue
{
    int value_;
public:
    ~SimpleValue() = default;
    SimpleValue();
    SimpleValue(const SimpleValue&) = default;
    SimpleValue& operator=(const SimpleValue&) = default;
    SimpleValue(SimpleValue&&) = delete;
    SimpleValue& operator=(SimpleValue&&) = delete;

    explicit SimpleValue(int value);
};

I fear CoreCppGuidelines C.21 will lead to a ton of code that looks just like this. Why is that bad? A couple of reasons:

1. This is far more difficult to read than this correct version:

class SimpleValue
{
    int value_;
public:
    SimpleValue();
    explicit SimpleValue(int value);
};

2. It is broken. You'll find out the first time you try to return a SimpleValue from a function by value:

SimpleValue
make_SimpleValue(int i)
{
    // do some computations with i
    SimpleValue x{i};
    // do some more computations
    return x;
}

This won't compile. The error message will say something about accessing a deleted member of SimpleValue.

I've got some better guidelines:

1. Know when the compiler is defaulting or deleting special members for you, and what defaulted members will do.

This chart can help with that:

enter image description here

If this chart is far too complex, I understand. It is complex. But when it is explained to you a little bit at a time it is much easier to deal with. I will hopefully be updating this answer within a week with a link to a video of me explaining this chart. Here is the link to the explanation, after a longer delay than I would have liked (my apologies): https://www.youtube.com/watch?v=vLinb2fgkHk

2. Always define or delete a special member when the compiler's implicit action is not correct.

3. Don't depend on deprecated behavior (the red boxes in the chart above). If you declare any of the destructor, copy constructor, or copy assignment operator, then declare both the copy constructor and the copy assignment operator.

4. Never delete the move members. If you do, at best it will be redundant. At worst it will break your class (as in the SimpleValue example above). If you do delete the move members, and it is the redundant case, then you force your readers to constantly review your class to make sure it is not the broken case.

5. Give tender loving care to each of the 6 special members, even if the result is to let the compiler handle it for you (perhaps by inhibiting or deleting them implicitly).

6. Put your special members in a consistent order at the top of your class (only those you want to declare explicitly) so that your readers don't have to go searching for them. I've got my favorite order, if your preferred order is different, fine. My preferred order is that which I used in the SimpleValue example.

Here is a brief paper with more rationale for this style of class declaration.

Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • In case you do need the default constructor, why not write `SimpleValue() = default;` ? And if you need a value different from 0, just write `int value_ = 42;` and then again `SimpleValue() = default;`. – TemplateRex Jul 31 '16 at 19:20
  • 1
    @TemplateRex: I agree that's a good way to implement the default constructor, especially in the `SimpleValue` example. One nitpick with your words: In this example `SimpleValue() = default;` won't always zero `value_` unless you `int value_ = 0;`. What it will do is make `SimpleValue` behave just like a scalar: `SimpleValue x;` will leave `x.value_` with an unspecified value and `SimpleValue x{};` will zero-initialize. Fwiw, this is exactly what `std::chrono::duration` does. I hesitated to include all this in my answer as it already boarders on too long. – Howard Hinnant Jul 31 '16 at 19:26
  • Given what @TemplateRex said, and if I wanted to be really picky - which is totally not my thing, honest! :p - couldn't I point out that C.21 says "If you define or =delete any default operation..."? Wouldn't it be fair to say that in the correct SimpleValue example you haven't defined or =deleted any default operations at all? – NQA Jul 31 '16 at 20:46
  • @JohnPage: Good question. This is actually another problem I have with C.21. It doesn't really specify what `define` means. I assumed that "define" means "user-declared." And `=default` counts as "user-declared". So now we really don't have a 100% clear picture of the advice that is being given. – Howard Hinnant Jul 31 '16 at 20:59
  • "Note: If you want a default implementation of a default operation (_while defining another_), write =default to show you're doing so intentionally for that function." There is an implication there that `=default` and `define` are "opposites". But yes, I'm sure it could still easily be misunderstood... – NQA Jul 31 '16 at 21:08
  • I think, video is desirable here. Thanks for the detailed explanation. – alexeykuzmin0 Aug 01 '16 at 06:43
7

I think maybe your second example is a reasonable exception, and, after all, the guideline does say "the odds are...", so there will be some exceptions.

I'm wondering if this slide might help with your first example:

Special Members

Here are the slides: https://accu.org/content/conf2014/Howard_Hinnant_Accu_2014.pdf

EDIT: For further information regarding the first case, I've since discovered this: C++11 virtual destructors and auto generation of move special functions

Community
  • 1
  • 1
NQA
  • 315
  • 1
  • 3
  • 11
  • Unfortunately, the slides do not explain why should I declare copy and move constructors and assignments in case of the virtual destructor with default body. Can you explain a bit more in detail? – alexeykuzmin0 Jul 31 '16 at 12:56
  • 1
    @alexeykuzmin0 Well, if you don't you won't have any move ctors etc. And the copy ones are deprecated. – NQA Jul 31 '16 at 13:45
  • For a small class abscence of move operations is probably not a big issue, and the reason for deprecation of copy operations in case of declared destructor with default body was unclear for me. Fortunately, BjarneStroustrup explained this in comment to the question – alexeykuzmin0 Jul 31 '16 at 13:50
  • 1
    Right. Ignore everything I've said! The man himself has just turned up... :) – NQA Jul 31 '16 at 18:40