Would a derived class ever have an implicit copy constructor or assignment operator when it's deleted in the base class?

Viewed 513

Qt defines Q_DISABLE_COPY as follows:

#define Q_DISABLE_COPY(Class) \
    Class(const Class &) = delete;\
    Class &operator=(const Class &) = delete;

Q_DISABLE_COPY is used in the QObject class, but the documentation for it says that it should be used in all of its subclasses as well:

when you create your own subclass of QObject (director or indirect), you should not give it a copy constructor or an assignment operator. However, it may not enough to simply omit them from your class, because, if you mistakenly write some code that requires a copy constructor or an assignment operator (it's easy to do), your compiler will thoughtfully create it for you. You must do more.

But consider this program:

struct Base {
    Base() = default;

private:
    Base(const Base &) = delete;
    Base &operator=(const Base &) = delete;
};

struct Derived : Base {};

int main() {
    Derived d1;
    Derived d2(d1); // error: call to implicitly-deleted copy constructor of 'Derived'
    Derived d3;
    d3 = d1; // error: object of type 'Derived' cannot be assigned because its copy assignment operator is implicitly deleted
}

The errors from trying to compile that program seem to indicate that the compiler will not create copy constructors or assignment operators in derived classes when they're deleted in base classes. Is Qt's documentation just wrong about this, or is there some edge case when it would create them?

Related, but not a duplicate: Repeating Q_DISABLE_COPY in QObject derived classes. It gives reasons why it may be useful to use Q_DISABLE_COPY in a class even if it wouldn't be copyable anyway, but doesn't confirm that it never will in fact be copyable without it.

2 Answers

Since the base class copy constructor is deleted, the derived class has no way to know how to copy the base class object. This will disable any implicit copy constructors provided by the compiler.

From cppreference:

The implicitly-declared or defaulted copy constructor for class T is defined as deleted if any of the following conditions are true:

  • T has direct or virtual base class that cannot be copied (has deleted, inaccessible, or ambiguous copy constructors)

  • T has direct or virtual base class with a deleted or inaccessible destructor;

Inheriting Q_DISABLE_COPY can be useful when the user inherits from a class that deletes the default copy constructor, but provides their default implementation to override it.

struct Base {
    Base() = default;

private:
    Base(const Base &) = delete;
    Base &operator=(const Base &) = delete;
};

struct Derived : Base {
    Derived() = default;
    Derived(const Derived&) : Derived() {}
    Derived &operator=(const Derived&) {
        return *this;
    }
};

struct MoreDerived : Derived {};


int main() {
    Derived d1;
    Derived d2(d1); // Works fine!
    Derived d3;
    d3 = d1; // Works fine!
    MoreDerived md1;
    MoreDerived md2(md1); // Works fine!
    MoreDerived md3;
    md3 = md1; // Works fine!!
}

Edit: As @SR_ rightly states, in the above implementation of Derived, Base is not being copy constructed. I just wanted to illustrate the fact that it is easy to introduce an unintentional copy constructor when another class is modified in the inheritance hierarchy.

Prior to commit a2b38f6, QT_DISABLE_COPY was instead defined like this (credit to Swift - Friday Pie for pointing this out in a comment):

#define Q_DISABLE_COPY(Class) \
    Class(const Class &) Q_DECL_EQ_DELETE;\
    Class &operator=(const Class &) Q_DECL_EQ_DELETE;

And Q_DECL_EQ_DELETE like this:

#ifdef Q_COMPILER_DELETE_MEMBERS
# define Q_DECL_EQ_DELETE = delete
#else
# define Q_DECL_EQ_DELETE
#endif

Q_COMPILER_DELETE_MEMBERS got defined if C++11 support (or at least a new enough draft of it to support = delete) was available.

Thus, if you compiled Qt back then against a C++03 compiler, it would instead have compiled something like this:

struct Base {
    Base() {};

private:
    Base(const Base &);
    Base &operator=(const Base &);
};

struct Derived : Base {};

int main() {
    Derived d1;
    Derived d2(d1);
    Derived d3;
    d3 = d1;
}

And compiling that with g++ -std=c++03 gives you these errors:

<source>: In copy constructor 'Derived::Derived(const Derived&)':
<source>:9:8: error: 'Base::Base(const Base&)' is private within this context
    9 | struct Derived : Base {};
      |        ^~~~~~~
<source>:5:5: note: declared private here
    5 |     Base(const Base &);
      |     ^~~~
<source>: In function 'int main()':
<source>:13:18: note: synthesized method 'Derived::Derived(const Derived&)' first required here
   13 |     Derived d2(d1);
      |                  ^
<source>: In member function 'Derived& Derived::operator=(const Derived&)':
<source>:9:8: error: 'Base& Base::operator=(const Base&)' is private within this context
    9 | struct Derived : Base {};
      |        ^~~~~~~
<source>:6:11: note: declared private here
    6 |     Base &operator=(const Base &);
      |           ^~~~~~~~
<source>: In function 'int main()':
<source>:15:10: note: synthesized method 'Derived& Derived::operator=(const Derived&)' first required here
   15 |     d3 = d1;
      |          ^~

So back then, "your compiler will thoughtfully create it for you" was technically true but not practically so, since the compiler creating it would cause compilation to fail, just with a different (and arguably less clear) error. I'm now convinced that it's not true at all anymore now that = delete is unconditionally used, so I plan to ask Qt's maintainers to remove/reword that section of their documentation.

Related