1

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?

KnowItAllWannabe
  • 12,972
  • 8
  • 50
  • 91
  • Why const Derive&& rhs? It doesn't really make sense, and shouldn't be any different than the copy constructor... – IdeaHat Jul 29 '13 at 19:10
  • "behave differently depending on how they're defined?" - huh? Definiton = implementation. If you tell your computer to do something else, then it will, er, do something else. –  Jul 29 '13 at 19:10
  • @MadScienceDreams: the derived constructor taking the const Derived *is* the copy constructor. – KnowItAllWannabe Jul 29 '13 at 19:12
  • 1
    std::move(rhs) yields a Derived&&, not a Base&&. A Derived&& can be implicitly cast to a Base&, but you have to explicitly tell it to cast to a Base&& (because you are going to steal parts of the class) with a cast call. – IdeaHat Jul 29 '13 at 19:14

1 Answers1

6

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.

Casey
  • 41,449
  • 7
  • 95
  • 125
David Rodríguez - dribeas
  • 204,818
  • 23
  • 294
  • 489
  • A templated constructor is still viable for use for *copy/move initialization*. – Xeo Jul 29 '13 at 19:19
  • @Xeo: The implicitly defined constructors will use the copy-constructor or move-constructor or the base, not *any* constructor. Yes, they are clearly suitable for use within any constructor of the derived type, as his last example demonstrates, when the user is explicitly calling the constructor of the base with an argument that is a better match than the copy/move constructor, but the standard guarantees that the implicitly defined copy constructor will call the base's copy constructor (which by definition cannot be templated) and similarly for the move constructor (move/copy will be picked) – David Rodríguez - dribeas Jul 29 '13 at 19:23