【问题标题】:Compiler not generating move constructors编译器不生成移动构造函数
【发布时间】:2019-04-16 22:59:52
【问题描述】:

我试图理解移动语义正在寻找编译器生成的移动构造函数(复制和赋值)。 在Modern Effective C++中,Scott Meyers 在 Item #17 中说,如果没有显式声明复制构造函数,编译器将生成移动构造函数,它将为 non-static 成员执行成员移动。

为了确认这一点,我正在尝试以下代码:

#include <iostream>
#include <string>
using namespace std;

class A
{
private:
    std::string str;

public:

    A() : str("Init string")
    {
        cout << "Default constructor" << endl;
    }

    A(std::string _str) : str(_str)
    {
        cout << "Constructor with string" << endl;
    }

    std::string getString()
    {
        return str;
    }
};

int main() {

    A obj1;
    A obj2("Obj2 string");

    cout << endl;
    cout << "obj1: " << obj1.getString() << endl;
    cout << "obj2: " << obj2.getString() << endl;

    obj1 = std::move(obj2);

    cout << endl;
    cout << "obj1: " << obj1.getString() << endl;
    cout << "obj2: " << obj2.getString() << endl;

    return 0;
}

输出是:

Default constructor
Constructor with string

obj1: Init string
obj2: Obj2 string

obj1: Obj2 string
obj2: Obj2 string

但我希望它是:

Default constructor
Constructor with string

obj1: Init string
obj2: Obj2 string

obj1: Obj2 string
obj2: 

因为 obj2.str 会被移动,现在有一个空字符串。

编译器没有生成移动赋值构造函数并调用复制赋值运算符的原因是什么?

编辑: 如下实现移动赋值运算符给出了预期的输出(即调用 std::move 后的空字符串)

A& operator=(A&& obj)
    {
        cout << "Move assignment operator" << endl;
        str = std::move(obj.str);
        return *this;
    }

【问题讨论】:

  • 请注意,2010 年代中期的 MSVC 版本并没有在应该生成移动构造函数时生成
  • 用长字符串(比如 40 个字符)再做一次测试

标签: c++ c++11 move-semantics


【解决方案1】:

首先obj1 = std::move(obj2);调用的是赋值运算符,所以和构造函数没有关系。

是的,编译器为A 生成一个移动赋值运算符,该运算符执行逐个成员的移动操作,包括数据成员str。问题是移动操作后str 留在valid, but unspecified state 中。另见std::basic_string::operator=

使用移动语义将内容替换为str 的内容。 str 之后处于有效但未指定的状态。

我想你可能只用std::string 观察到相同的结果,例如

std::string str1 = "Init string";
std::string str2 = "Obj2 string";
str1 = std::move(str2);
std::cout << str2;

LIVE with clang,仅供参考;它给出了你预期的结果,但仍然记得结果是未指定的。

【讨论】:

  • 谢谢。但是,我看到当我实现移动赋值构造函数时, obj1 = std::move(obj) 调用显式声明的移动赋值构造函数,而不是复制赋值运算符。
  • @madu 你是如何声明移动赋值构造函数的?它的签名是什么?
  • 签名为 A& operator=(A&& obj)。错了吗?
  • @madu 我明白了,是的,这是赋值运算符。 :) 移动构造函数看起来像A(A&amp;&amp; obj)。他们是不同的。所以我们不说赋值构造函数,这很混乱。
  • 谢谢。是的,我看到了区别。但是移动赋值运算符的签名是正确的,对吧?
【解决方案2】:

编译器正在调用移动赋值运算符,这会导致 obj1.strobj2.str 移动赋值。但是,移动并不能保证源对象是空的;对于大多数标准库类,已从中移动的对象处于“有效但未指定的状态”。 (最明显的例外是已被移出的std::unique_ptr&lt;T&gt; 保证为空。)通常但并非总是如此,移出的std::string 为空。在您的情况下,字符串 "Obj2 string" 足够短,可以内联存储(,使用短字符串优化)。如果是这种情况,则移动赋值运算符必须 复制 字符串。返回并清空源字符串会增加额外的开销,因此实现不会这样做。

【讨论】:

  • 谢谢布赖恩。但是,当我明确实现移动赋值运算符并在其中执行 str = std::move(obj.str) 时,我看到 obj2.str 实际上是空字符串。为什么在这种情况下我会看到不同的行为?
  • @madu 您能否提供重现 both 行为所需的步骤,即使用和不使用用户提供的移动赋值运算符?
  • 谢谢@Brian。我编辑了原始问题并包含了移动赋值运算符实现,它可以提供我期望的输出。
【解决方案3】:

标准没有指定移动对象的状态。

17.6.5.15 库类型的移出状态 [lib.types.movedfrom]

C++ 标准库中定义的类型的对象可以从 (12.8) 中移出。移动操作可以显式指定或隐式生成。除非另有说明,否则此类移出的对象应处于有效但未指定的状态。

因此,您从字符串中移出的是有效但未指定的状态。不要除空字符串或相同的值或包含"potato" 的字符串。

我的猜测是"Obj2 string" 适合小字符串优化,这使得小字符串可以存在于堆栈而不是堆中。在这种特殊情况下,通过 memcpy 将字符串对象分配给另一个对象而不进行任何清理(例如,不将旧字符串设置为空)实际上更快。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-10-21
    • 1970-01-01
    • 2018-10-01
    • 2013-04-09
    • 1970-01-01
    • 2020-09-26
    • 2017-07-20
    • 1970-01-01
    相关资源
    最近更新 更多