25

Let's assume we have a very basic class A:

class A {
    public:
        void SetName(const std::string& newName) {
            m_name=newName;
        }

        void Print() const {
            std::printf("A::Print(). Name: %s\n",m_name.c_str());
        }
    private:
        std::string m_name;  
};

We want to extend this class with class B so we add our virtual destructor, change a member to virtual and change private to protected for inh:

class A {
    public:
        virtual ~A() {}

        void SetName(const std::string& newName) {
            m_name=newName;
        }

        virtual void Print() const {
            std::printf("A::Print(). Name: %s\n",m_name.c_str());
        }
    protected:
        std::string m_name;

};

class B : public A {
    public:
        virtual void Print() const {
            std::printf("B::Print(). Name: %s\n",m_name.c_str());
        }
};

Now since we added a destructor in class A do we need to create a copy constructor and copy operator like so?

class A {
    public:
        virtual ~A() {}

        A() = default;
        A(const A& copyFrom){
            *this = copyFrom;
        }
        virtual A& operator=(const A& copyFrom){
            m_name=copyFrom.m_name;
            return *this;
        };

        void SetName(const std::string& newName) {
            m_name=newName;
        }

        virtual void Print() const {
            std::printf("A::Print(). Name: %s\n",m_name.c_str());
        }
    protected:
        std::string m_name;

};

To me this seems unnecessary as the default copy operator and copy constructor would do the same thing.

Grapes
  • 2,473
  • 3
  • 26
  • 42
  • 2
    @MarcoA. A derived class's destructor may need to do something extra, and you need the base class destructor to be virtual if you want to delete a derived from a pointer to base. – T.C. Sep 25 '14 at 13:36
  • @Angew that is a good reason. Right – Marco A. Sep 25 '14 at 13:40
  • Note: avoid using `protected` data members in general; because unless you tightly control the inheritance hierarchy (`final`), you have lost all guarantees about the state of this member in general. This is similar to returning a non-const reference. – Matthieu M. Sep 25 '14 at 14:02

3 Answers3

28

To be prepared for potential future evolution of the language, you should indeed explicitly default the copy/move constructors and assignment operators when you add a virtual destructor. That's because C++11, 12.8/7 makes implicit generation of copy constructors deprecated when the class has a user-declared destructor.

Fortunately, C++11's explicit defaulting makes their definition easy:

class A {
    public:
        virtual ~A() {}

        A() = default;
        A(const A& copyFrom) = default;
        A& operator=(const A& copyFrom) = default;
        A(A &&) = default;
        A& operator=(A &&) = default;

        void SetName(const std::string& newName) {
            m_name=newName;
        }

        virtual void Print() const {
            std::printf("A::Print(). Name: %s\n",m_name.c_str());
        }
    protected:
        std::string m_name;

};
Angew is no longer proud of SO
  • 167,307
  • 17
  • 350
  • 455
  • I like this answer, sounds like a good "specify/pay for what you use" practice – Marco A. Sep 25 '14 at 13:39
  • 4
    FWIW, you can also `= default` the virtual destructor. – R. Martinho Fernandes Sep 25 '14 at 13:41
  • 2
    Would a `virtual ~A() = default` destructor still count as user generated? – MatthiasB Sep 25 '14 at 13:42
  • This is great. It's much simpler this way. Is there any way to simplify this even further? For example `class A : public Polymorphic` for classes like these? And the `Polymorphic` class would generate all of the defaults? – Grapes Sep 25 '14 at 13:42
  • 6
    @MatthiasB Yes: the standard says "user-declared" and `virtual ~A() = default` is a declaration. – Angew is no longer proud of SO Sep 25 '14 at 13:44
  • 1
    `Explicitly-defaulted move assignment operator must return 'A &'` (Apple LLVM version 6.1.0 (clang-602.0.53) (based on LLVM 3.6.0svn)) – cubuspl42 Aug 28 '15 at 22:30
  • I actually think it's good practice to explicitly `default`/`delete` all copy/move constructors and assignment operators, regardless of virtuality. – Daniel Aug 29 '15 at 08:18
  • Somehow `{}` seems easier than `= default;` – dtech Aug 29 '15 at 09:04
  • @ddriver For the dtor? Because for anything else, it would be *wrong.* – Angew is no longer proud of SO Aug 31 '15 at 06:45
  • @ddriver A `{}` ctor will invoke the *default* ctor for all members - it will ignore its parameter. A `{}` assignment op will do nothing (not even `return` something meaningful)... – Angew is no longer proud of SO Aug 31 '15 at 06:52
  • @ddriver Yes, that's the whole point. `= default` means (for the copy ctor as an example) "I want the same copy ctor as would be generated if the class contained no specified copy ctor nor move ctor." DTTO for the other special members (move ctor, copy & move assignment ops). – Angew is no longer proud of SO Aug 31 '15 at 07:03
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/88345/discussion-between-angew-and-ddriver). – Angew is no longer proud of SO Aug 31 '15 at 07:06
  • Obviously, for methods which are actually supposed to do something, such as copy constructors or assignment operators, a `{}` will not do the trick. But for constructors and destructors it is OK and much shorter. – dtech Aug 31 '15 at 07:23
  • Create an abstract base class called `UniversalBase` that declares a virtual default destructor and the other 4 special member functions as default and then make all your potential real base classes inherit from this so they can follow Rule of 0 wherever possible - please tell me why this is a bad idea. – davidA Feb 23 '23 at 21:26
18

The rule of three applies to everything.

If your class is intended to be used as a polymorphic base, it's highly unlikely you will want to use its copy constructor because it slices. So you have to make a decision. That's what the rule of three is about: you can't choose to have a destructor without considering the copy special members.

Note that the rule of three doesn't say you're supposed to implement the copy constructor and copy assignment operator. You're supposed to deal with them somehow, because the default-generated one is highly likely not suitable if you have your own destructor (it slices!), but the way you deal with them doesn't have to be implementing them.

You should probably just forbid it since using polymorphic bases and value semantics tend to mix like water and oil.

I guess you could maybe make it protected so derived classes can call it for their own copies, though I still consider that a questionable choice.

Additionally, since C++11 the generation of copy special members is deprecated when a destructor is user-declared. That means that if you want your code to be forward-compatible, even if you want the default copy constructor behaviour (a questionable choice), you will want to make that explicit. You can use = default for that.

R. Martinho Fernandes
  • 228,013
  • 71
  • 433
  • 510
  • 2
    This is better than the accepted answer. No response here is complete without mentioning slicing. Also, "polymorphic bases and value semantics tend to mix like water and oil" is a very good observation and worth considering if (like me) you are searching for this Q&A. I suppose wacky type-erasure stuff could create an exception, but that is far from the common case. – Nemo Jan 18 '18 at 20:14
5

If the destructor doesn't do anything, then there's (usually) no need for the copy/move operations to do anything other than the default. There's certainly no need to write versions that do what the defaults would, just to satisfy an over-simplification of the rule. All that does is increase the complexity of the code and the scope for error.

However, if you do declare a virtual destructor, indicating that the class is intended to be a polymorphic base class, you might consider deleting the copy/move operations to prevent slicing.

This article gives a useful wording for the rule, including

If a class has a nonempty destructor, it almost always needs a copy constructor and an assignment operator.

Mike Seymour
  • 249,747
  • 28
  • 448
  • 644