4

See the following code:

#include <iostream>
#include <chrono>

class Parent
{
public:
    Parent() = default;
    virtual ~Parent() = default;
    Parent(const Parent& pr) : i{pr.i} {std::cout << "Parent copy constructor\n";}
    Parent& operator=(const Parent& pr) {std::cout << "Parent copy assignment\n"; this->i = pr.i; return *this;}
    Parent(Parent&& pr) : i{std::move(pr.i)} {std::cout << "Parent move constructor\n";}
    Parent& operator=(Parent&& pr) {std::cout << "Parent move assignment\n"; this->i = std::move(pr.i); return *this;}

    virtual void print_i_j() = 0;

    int i = 10;
};

class Child : public Parent
{
public:
    Child() = default;
    Child(const Child& cr) : Parent{cr}, j{cr.j} {std::cout << "Child copy constructor\n";}
    Child& operator=(const Child& cr) {std::cout << "Child copy assignment\n"; this->j = cr.j; return *this;}
    Child(Child&& cr) : Parent{std::move(cr)}, j{std::move(cr.j)} {std::cout << "Child move constructor\n";}
    Child& operator=(Child&& cr) {std::cout << "Child move assignment\n"; Parent::operator=(std::move(cr)); this->j = std::move(cr.j); return *this;}

    void print_i_j() {std::cout << "i = "<< i << " j = " << j << std::endl;}

    int j = 100;
};

int main(int argc, const char * argv[])
{
    Child c;
    c.i = 30;
    c.j = 300;
    c.print_i_j();

    Child c2;               // leave c2 with defaults (i=10, j=100)
    Parent& p_ref = c2;
    p_ref.print_i_j();

    c2.j = 150;
    p_ref.print_i_j();

    p_ref = std::move(c);   // (1)
    p_ref.print_i_j();      // (2)

    return 0;
}

When I run this I get:

i = 30 j = 300
i = 10 j = 100
i = 10 j = 150
Parent move assignment
i = 30 j = 150

As far as I can tell, as indicated in this output, i changes as a result of moving an instance of the derived class into a reference to the parent class, but j does not.

Is the result printed in (2) an indication that the move in (1) caused slicing? Or is some other behavior (or even undefined behavior) kicking in?

ToddR
  • 300
  • 1
  • 11

2 Answers2

8

Can std::move cause slicing ...

No. std::move doesn't cause slicing.

However, assignment into a base object causes slicing i.e. only the base is assigned and rest of the object is unaffected. This happens both when copy assigning as well as move assigning. Move assignment does however have an extra consideration: Not only is only the base of the left hand operand assigned (like in the case of copy), but also only the base of the right hand operand has been moved from.

Whether the base is assigned through a reference or not does not affect slicing unless the assignment operator is virtual (don't use virtual assignment operators though; they are not an easy / good solution).


Either:

  • Make sure that derived classes can deal with assigned / moved from base sub objects (i.e. there should be no class invariants that could be violated by such assignment).
  • Or make base unassignable (maybe make assignment protected).
  • Or make base inaccessible (protected or private)

In any case, make sure that the static type of the left hand operand is what you expect it to be when assigning.

eerorika
  • 232,697
  • 12
  • 197
  • 326
  • "Whether the base is assigned through a reference or not does not affect slicing..." While I agree, one suggestion I often see to avoid slicing is to make the base abstract to prevent instantiation of a base object. My intent was to show that even if you can't create a base object due to the pure virtual `print_i_j()` method, you can still use a reference to cause the slicing to occur. That's why I mention l-value reference in the title. This example was just part of a bigger example I was using in an offline discussion, so I probably didn't convey that well here. – ToddR May 10 '19 at 15:32
  • @ToddR the suggestion to use a reference to avoid slicing applies to passing the object into a function that treats the objects polymorphically (invokes virtual functions). The assignment operator doesn't call any virtual functions, so it doesn't make a difference that you did pass a reference. Indeed, the pure virtual function makes it impossible to pass the base by value, so that wouldn't even have been an option. – eerorika May 10 '19 at 16:08
6

Yes, slicing occurs because the move assignment operator is selected statically (at compile time), and the left-hand side's static type is Parent&, not Child:

Child c;
Child c2;
Parent& p_ref = c2;
p_ref = std::move(c);   // (1)

To clarify, you don't "move into lvalue-reference". You move into the object, but not using the function which moves the entire object (Child::operator=) but the one which moves only the Parent part (Parent::operator=).

In particular, there is nothing special about move semantics; the same behavior would apply to any member function. The right-hand side operator's type is not relevant in this case.

class Parent
{
public:
    virtual ~Parent() = default;
    void func(); // non-virtual, like move assignment
};

class Child : public Parent
{
public:
    void func();
};

// usage:
Child c;
Parent& p_ref = c;
p_ref.func(); // calls Parent::func(), not Child::func()
TheOperator
  • 5,936
  • 29
  • 42