Search code examples
c++c++11inheritanceconstructorusing-declaration

Surprising behavior in multiple copy constructor inheritance


Starting with C++11, there can be two copy constructors, one taking a parameter of type T&, and one taking a parameter of type const T&.

I have a situation where (seemingly) adding a second copy constructor causes neither one to get called, when the constructors are inherited in a derived class. The copy constructor is overridden by a templatized constructor when both are present.

Here is a MWE:

struct A {
  template <typename... Args>
  A (Args&&... args)
  { std::cout << "non-default ctor called\n"; }

  A (A&) { std::cout << "copy ctor from non-const ref\n"; }
};

struct C :public A { using A::A; };

int main() {
  C c1;
  C c2(c1);
}

Running this code, we see output

non-default ctor called
copy ctor from non-const ref

which is as expected.

However, adding an additional constructor to struct A as follows:

  A (const A&) { }

somehow causes the other copy constructor not to get called, so the output becomes

non-default ctor called
non-default ctor called

In my use case, I want to inherit all the constructors from a base class into a derived class, including the copy constructors and anything else. But it seems that somehow the two copy constructors don't get inherited when they are both present. What is going on here?


Solution

  • From https://en.cppreference.com/w/cpp/language/using_declaration

    If one of the inherited constructors of Base happens to have the signature that matches a copy/move constructor of the Derived, it does not prevent implicit generation of Derived copy/move constructor (which then hides the inherited version, similar to using operator=).

    So

    struct C :public A { using A::A; };
    

    is

    struct C :public A
    {
        using A::A;
        C(const C&) = default;
        C(C&&) = default;
    };
    

    where C(const C&) = default; is similar to

    C(const C& c) : A(static_cast<const A&>(c)) {}
    

    So with

    struct A {
      template <typename... Args>
      A (Args&&... args)
      { std::cout << "non-default ctor called\n"; }
    
      A (A&) { std::cout << "copy ctor from non-const ref\n"; }
    };
    

    template constructor is chosen, but

    struct A {
      template <typename... Args>
      A (Args&&... args)
      { std::cout << "non-default ctor called\n"; }
    
      A (const A&) { std::cout << "copy ctor from const ref\n"; }
      A (A&) { std::cout << "copy ctor from non-const ref\n"; }
    };
    

    A (const A&) is chosen.

    As you can notice, there is also a defect:

    The semantics of inheriting constructors were retroactively changed by a defect report against C++11. Previously, an inheriting constructor declaration caused a set of synthesized constructor declarations to be injected into the derived class, which caused redundant argument copies/moves, had problematic interactions with some forms of SFINAE, and in some cases can be unimplementable on major ABIs. Older compilers may still implement the previous semantics.

    http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0136r1.html

    With that defect, your class C would be

    struct C :public A
    {
        using A::A;
    
        template <typename ...Ts>
        C(Ts&&... ts) : A(std::forward<Ts>(ts)...) {} // Inherited.
    
        C(const C&) = default;
        C(C&&) = default;
    };
    

    So you call C(C& c) : A(c) {} (after template substitution).