3

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?

Jarod42
  • 203,559
  • 14
  • 181
  • 302
Dan R
  • 1,412
  • 11
  • 21
  • 1
    What compiler / flags? After adding `A (const A&) { }`, I only see one line of output: https://wandbox.org/permlink/ex8AyRHqF1ChsNcc (the const ref version is being called) – jcai Jul 04 '18 at 23:22
  • 1
    Not sure, but latest gcc and clang will both call the const-ref version on [wandbox](https://wandbox.org/) – super Jul 04 '18 at 23:22
  • @Arcinde Thanks for the link to wandbox! It seems that the behavior is as I described until gcc 7 (I was using gcc 5.4). So I guess it was a bug in the compiler? Or is it unspecified behavior? – Dan R Jul 04 '18 at 23:31
  • @super It looks like the behavior changed as of GCC 7 and clang 4.0. I'm still not sure whether this was a bug (in both compilers) or unspecified behavior. – Dan R Jul 04 '18 at 23:34
  • 2
    It was always possible to have `A(A&)` and `A(A const&)` – M.M Jul 04 '18 at 23:49
  • [Demo](http://coliru.stacked-crooked.com/a/e0281d9388e114c1) showing both expected and unexpected result depending of version of compiler. – Jarod42 Jul 05 '18 at 00:10
  • @M.M My note about having multiple copy constructors being somehow new in C++11 is based on the documentation [here](https://en.cppreference.com/w/cpp/language/copy_constructor). I think you could always have both constructors but only since C++11 are they both considered to be copy constructors. – Dan R Jul 05 '18 at 06:08
  • 1
    @DanRoche I assume the section you refer to is supposed to indicate that `= default;` was added in C++11. You could still have the two user-provided copy constructors in C++03. In fact section 12.8/2 of the C++03 standard has an example of exactly that. – M.M Jul 05 '18 at 07:08

1 Answers1

6

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

Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • 1
    Thanks for the useful link from the docs. But the behavior is exactly the opposite of what you have here - the template constructor is chosen only in the **second** case, when both ctors are present. (It seems this was maybe a compiler bug that has been fixed in more recent versions?) – Dan R Jul 04 '18 at 23:40
  • 1
    @DanRoche: There is also a defect. so old compilers might still implement that. – Jarod42 Jul 05 '18 at 00:02