【问题标题】:Move constructor vs Copy elision移动构造函数与复制省略
【发布时间】:2023-03-03 18:02:01
【问题描述】:

谁能给我解释一件事。从一方面来看,move constructor 旨在通过消除不必要的从另一侧复制对象来优化内存和处理器的使用,但几乎在任何将要使用move constructor 的地方,编译器都会使用copy elision,禁用move ctor?不合理吗?

【问题讨论】:

  • “几乎所有要使用移动构造函数的地方,编译器都使用复制省略”——这完全错了。 (例如,尝试通过右值引用将右值插入容器。)
  • @Derek81 您必须通过minimal reproducible example 来支持您的声明。
  • “移动构造函数”也可以称为“rvalue构造函数”。
  • @Derek81 还有很多不是的。
  • @Derek81 执行 ctor + 复制省略,而不是 ctor + move 构造函数更便宜。如果可能,为什么编译器不应该这样做?移动构造创建一个新对象,旧对象的数据被“移动”(在最坏的情况下,如果源对象的所有数据都完全包含在其中,它与常规对象一样昂贵)复制)只有当你有可以像指针一样交换的成员变量或支持交换的容器(或者如果你想让对象不复制而只能移动)时,移动构造函数才比复制构造函数受益。

标签: c++ constructor move-semantics copy-elision


【解决方案1】:

在很多情况下,移动构造函数仍会被调用,并且不会使用复制省略:

// inserting existing objects into a container
MyObject myobject;
std::vector<MyObject> myvector;
myvector.push_back(std::move(myobject));

// inserting temporary objects into a container
myvector.push_back(MyObject());

// swapping
MyObject other;
std::swap(myobject, other);

// calling functions with existing objects
void foo(MyObject x);

foo(std::move(myobject));

...还有更多。

强制复制省略的唯一实例(C++17 起)是从函数调用或构造函数的结果构造值时。在这种情况下,甚至不允许编译器使用移动构造函数。例如:

MyObject bar() {
    return MyObject();
}

void example() {
    MyObject x = bar(); // copy elision here
    MyObject y = MyObject(); // also here
}

一般来说,复制省略的目的不是完全消除移动构造,而是在从纯右值初始化变量时避免不必要的构造。


cppreference on Copy Elision

【讨论】:

  • 好的,但是我已经完全禁用了优化(在我的 VS2019 中将优化设置为禁用 (/Od))并且在您的强制复制省略示例中仍然没有调用移动 ctor!
  • 你能回答我最后一个问题吗?
  • @Derek81 你的意思是第二个代码块吗?该代码块是复制省略是强制的示例,因此甚至不允许编译器在那里使用移动构造函数。 first 代码块显示了复制省略不会发生的示例。
  • @Derek81 请注意,即使从 C++17 开始,复制省略通常也由具有禁用优化的编译器应用 (-O0)。例如,GCC 允许您在此处禁用复制省略,但您必须使用特殊标志 -fno-elide-constructors。不了解 MSVC。
  • @Jan Schultke 好的,我明白了。但是另一种情况:调用 fun() where fun: fun () {return MyObject() } 也没有;不调用 move ctor?
【解决方案2】:

这是一个调用move 的简单示例。这是一个玩具示例,可能与零规则相关,但假设类中还有其他成员需要遵循五规则。

class A {
    std::string s;
public:
    A(const char* s = ""): s(s) {}
    ~A() {}
    A(const A& a): s(a.s) {
        std::cout << "copy ctor" << std::endl;
    }
    A& operator=(const A& a) {
        s = a.s;
        std::cout << "copy assignment" << std::endl;
        return *this;
    }
    A(A&& a): s(std::move(a.s)) {
        std::cout << "move ctor" << std::endl;
    }
    A& operator=(A&& a) {
        s = std::move(a.s);
        std::cout << "move assignment" << std::endl;
        return *this;
    }
};

int main() {
    A a;
    a = "hi"; // move
    // suppose we KNOW here that a is not needed anymore
    A a2 = std::move(a); // move
    a = "bye"; // move
}

代码:http://coliru.stacked-crooked.com/a/97d25c43e0edb00b

【讨论】:

    【解决方案3】:

    因为复制省略有限制,编译器必须知道对象的生命周期才能预测是否可以进行复制省略。 例如:

    std::vecter<MyObj> v;
    v.push(MyObj()); // compiler has a higher chance to do the copy elision
    

    但考虑一下:

    MyObj my_obj;
    v.push(my_obj)
    // ...
    // my_obj will never use
    

    在这种情况下,编译器不会知道my_obj 永远不会被使用,所以会执行正常的复制。如果效率很重要,您必须使用v.push(std::move(my_obj)); 明确告诉编译器“我永远不会再使用 my_obj”

    【讨论】:

      【解决方案4】:

      move 构造函数旨在通过消除不必要的对象复制来优化内存和处理器的使用

      那不是真的。移动构造会创建一个新对象,旧对象的数据 “移动” (在最坏的情况下,如果源对象的所有数据都完全包含在其中,那么它的成本与常规副本)只有在您拥有可以交换的成员变量(如支持交换的指针或容器)(或者如果它拥有无法复制的资源)时,移动构造函数才比复制构造函数受益。

      因此,在移动 ctor 上总是需要复制省略。但这并不意味着移动 ctor 没有任何用处。然而,在许多情况下,move ctor 只是 swap 和 reset/empty/destruct 的语法糖(不完全正确,但很接近)。

      除了swap 的情况外,移动 ctor 对于可复制或不可复制以及您不想使用指针的事物也很有用。例如std::uniqu_ptr 不应该是可复制的,因为它拥有唯一的所有权,但您可能希望在调用函数时传递所有权,因此移动其资源很重要。

      您可以将移动语义视为一个标准化的过程,因此如果可能的话,复制省略,如果它不是移动 ctor 的后备。

      【讨论】:

        猜你喜欢
        • 2014-01-02
        • 2019-09-29
        • 2016-06-01
        • 2018-09-17
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-03-12
        • 2013-03-27
        相关资源
        最近更新 更多