Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 825
Dan R Avatar asked Jun 17 '26 19:06

Dan R


1 Answers

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).

like image 198
Jarod42 Avatar answered Jun 19 '26 09:06

Jarod42