I'm confused about the behavior I'm seeing when derived class copy and move functions call their base class versions.

I have a base class with various constructors that tell me when they're called:

#include <iostream>

class Base {
public:     
  Base() {}

  template<typename T>
  Base(T&&) { std::cout << "URef ctor\n"; }

  Base(const Base&) { std::cout << "Copy ctor\n"; }

  Base(Base& rhs): Base(const_cast<const Base&>(rhs))
  { std::cout << "  (from non-const copy ctor)\n"; }

  Base(Base&&) { std::cout << "Move ctor\n"; }

  Base(const Base&& rhs): Base(rhs)
  { std::cout << "  (from const move ctor)\n"; }
};

For a derived class with compiler-generated copy and move operations

class Derived: public Base {};

and this test code,

int main()                              
{                                      
  Derived d;                         
  Derived copyNCLValue(d);          
  Derived copyNCRvalue(std::move(d)); 

  const Derived cd;
  Derived copyCLValue(cd);      
  Derived copyCRvalue(std::move(cd));    
}

gcc 4.8.1 produces this output:

Copy ctor
Move ctor
Copy ctor
Copy ctor

This surprises me. I expected the base class constructor taking the universal reference to be called, because it can be instantiated to create an exact match on the derived object that is presumably passed from the derived class's functions. The base class copy and move functions require a derived-to-base conversion.

If I change the derived class to declare the copy and move functions myself, but to give them the default implementations,

class Derived: public Base {
public:
  Derived(){}

  Derived(const Derived& rhs) = default;
  Derived(Derived&& rhs) = default;     
};

gcc produces the same output. But if I write the functions myself with what I believe are the default implementations,

class Derived: public Base {
public:
  Derived(){}

  Derived(const Derived& rhs): Base(rhs) {}
  Derived(Derived&& rhs): Base(std::move(rhs)) {}
};

I get the output I originally expected:

URef ctor
URef ctor
URef ctor
URef ctor

I'd expect to get the same output in each case. Is this a bug in gcc, or is there something I'm not understanding?

1

There are 1 best solutions below

2
On

This surprises me. I expected the base class constructor taking the universal reference to be called, because it can be instantiated to create an exact match on the derived object that is presumably passed from the derived class's functions. The base class copy and move functions require a derived-to-base conversion.

No. The compiler sees the line Derived copyCRvalue(std::move(cd)); that really means Derived copyCRvalue(static_cast<const Derived&&>(cd)); and it tries to find a constructor in Derived that matches that call. It finds two closely related constructors both implicitly declared:

Derived(Derived const &); // copy constructor
Derived(Derived &&);      // move constructor

The second one cannot be used, since the rvalue-reference is to a const object, but the first one is a match. The definition of the implicitly defined copy constructor is:

Derived(Derived const &rhs) : base(static_cast<Base const &>(rhs)) {}

Inside the constructor, the rhs is an lvalue, not an rvalue (and a templated constructor is not a copy-constructor anyways).

But if I write the functions myself with what I believe are the default implementations,

class Derived: public Base {
public:
  Derived(){}

  Derived(const Derived& rhs): Base(rhs) {}
  Derived(Derived&& rhs): Base(std::move(rhs)) {}
};

Except that those are not the definitions that the compiler will provide. The specific quota from the standard is in 12.8/15

The implicitly-defined copy/move constructor for a non-union class X performs a memberwise copy/move of its bases and members.

That is, the implicitly defined constructor will initialize the destination's base with the source's base, and similarly each member in the destination with the same member in the source.