std::move 移动到左值引用时会导致切片吗?

Can std::move cause slicing when moving into l-value reference?

见以下代码:

#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;
}

当我 运行 我得到:

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

据我所知,如输出所示,i 由于将派生 class 的实例移动到对父 class 的引用而发生变化, 但 j 没有。

(2)中打印的结果是否表明(1)中的移动导致了切片?还是其他一些行为(甚至未定义的行为)开始起作用?

是的,发生切片是因为移动赋值运算符是静态选择的(在编译时),而left-hand端的静态类型是Parent&,而不是Child:

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

澄清一下,你没有 "move into lvalue-reference"。您移动到 ​​ 对象 ,但不使用移动整个对象 (Child::operator=) 的函数,而是使用仅移动 Parent 部分的函数 (Parent::operator=).

特别是,移动语义没有什么特别之处;相同的行为将适用于任何成员函数。 right-hand 侧运算符的类型在这种情况下不相关。

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

Can std::move cause slicing ...

没有。 std::move 不会导致切片。

但是,赋值到基础对象中会导致切片,即只有基础被赋值,对象的其余部分不受影响。这在复制分配和移动分配时都会发生。然而,移动赋值确实有一个额外的考虑:不仅分配了左手操作数的基数(就像在复制的情况下),而且还只移动了右手操作数的基数。

是否通过引用分配基数不会影响切片,除非赋值运算符是虚拟的(尽管不要使用虚拟赋值运算符;它们不是一个简单/好的解决方案)。


或者:

  • 确保派生的 classes 可以处理从基础子对象分配/移动的对象(即不应该有可能被此类分配违反的 class 不变量)。
  • 或者使基数不可分配(也许使分配受到保护)。
  • 或使基地不可访问(受保护或私有)

在任何情况下,请确保左侧操作数的静态类型是您在分配时期望的类型。