什么是移动语义?

发布于 2024-09-06 15:48:18 字数 269 浏览 3 评论 0原文

我刚刚听完软件工程广播 对 Scott Meyers 的播客采访,内容涉及 C++11。大多数新功能对我来说都很有意义,但有一个例外。我仍然不明白移动语义...它到底是什么?

I've just finished listening to the Software Engineering radio podcast interview with Scott Meyers regarding C++11. Most of the new features made sense to me, with the exception of one. I still don't get move semantics... What is it exactly?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(12

ま柒月 2024-09-13 15:48:18

我发现使用示例代码最容易理解移动语义。让我们从一个非常简单的字符串类开始,它只保存一个指向堆分配的内存块的指针:

#include <cstring>
#include <algorithm>

class string
{
    char* data;

public:

    string(const char* p)
    {
        size_t size = std::strlen(p) + 1;
        data = new char[size];
        std::memcpy(data, p, size);
    }

由于我们选择自己管理内存,所以我们需要遵循 三规则。我将推迟编写赋值运算符,现在只实现析构函数和复制构造函数:

    ~string()
    {
        delete[] data;
    }

    string(const string& that)
    {
        size_t size = std::strlen(that.data) + 1;
        data = new char[size];
        std::memcpy(data, that.data, size);
    }

复制构造函数定义复制字符串对象的含义。参数const string& 绑定到字符串类型的所有表达式,允许您在以下示例中进行复制:

string a(x);                                    // Line 1
string b(x + y);                                // Line 2
string c(some_function_returning_a_string());   // Line 3

现在是移动语义的关键见解。请注意,只有在我们复制 x 的第一行中,这种深复制才真正必要,因为我们可能想稍后检查 x 并且如果 x 不知何故发生了变化。您是否注意到我刚刚说了三次x(如果包含这句话则四次)并且每次都表示完全相同的对象?我们将诸如 x 之类的表达式称为“左值”。

第 2 行和第 3 行中的参数不是左值,而是右值,因为底层字符串对象没有名称,因此客户端无法在稍后的时间点再次检查它们。
右值表示在下一个分号处销毁的临时对象(更准确地说:在词法上包含右值的完整表达式的末尾)。这很重要,因为在 bc 初始化期间,我们可以对源字符串执行任何我们想要的操作,并且客户端无法区分< /em>!

C++0x 引入了一种称为“右值引用”的新机制,其中包括:
允许我们通过函数重载来检测右值参数。我们所要做的就是编写一个带有右值引用参数的构造函数。在该构造函数中,我们可以对源代码执行任何我们想要的操作,只要我们将其保留在某个有效状态:

    string(string&& that)   // string&& is an rvalue reference to a string
    {
        data = that.data;
        that.data = nullptr;
    }

我们在这里做了什么?我们没有深度复制堆数据,而是复制了指针,然后将原始指针设置为空(以防止源对象的析构函数中的“delete[]”释放我们的“刚刚窃取的数据”)。实际上,我们“窃取”了原本属于源字符串的数据。同样,关键的见解是在任何情况下客户端都无法检测到源已被修改。由于我们在这里并没有真正进行复制,因此我们将此构造函数称为“移动构造函数”。它的工作是将资源从一个对象移动到另一个对象,而不是复制它们。

恭喜,您现在了解了移动语义的基础知识!让我们继续实现赋值运算符。如果您不熟悉复制和交换习惯用法,请学习它然后回来,因为这是一个与异常安全相关的很棒的 C++ 习惯用法。

    string& operator=(string that)
    {
        std::swap(data, that.data);
        return *this;
    }
};

哈,就这样吧? “右值引用在哪里?”你可能会问。 “我们这里不需要它!”这是我的答案:)

请注意,我们通过值传递参数 that,因此必须像任何其他字符串对象一样初始化 thatthat 究竟如何初始化?在 C++98 的过去,答案是“通过复制构造函数”。在 C++0x 中,编译器根据赋值运算符的参数是左值还是右值来选择复制构造函数和移动构造函数。

因此,如果您说 a = b复制构造函数 将初始化 that(因为表达式 b 是一个左值),赋值运算符将内容与新创建的深层副本交换。这就是复制和交换习惯用法的定义——制作一个副本,将内容与副本交换,然后通过离开作用域来删除副本。这里没什么新鲜事。

但如果您说a = x + y移动构造函数将初始化that(因为表达式x + y > 是一个右值),因此不涉及深度复制,只有高效的移动。
that 仍然是一个独立于参数的对象,但它的构造很简单,
因为堆数据不必复制,只需移动即可。没有必要复制它,因为 x + y 是一个右值,而且,可以从右值表示的字符串对象中移动。

总而言之,复制构造函数进行深层复制,因为源必须保持不变。
另一方面,移动构造函数可以只复制指针,然后将源中的指针设置为 null。以这种方式“无效”源对象是可以的,因为客户端无法再次检查该对象。

我希望这个例子能够表达要点。右值引用和移动语义还有很多内容,为了简单起见,我故意省略了这些内容。如果您想了解更多详细信息,请参阅我的补充答案

I find it easiest to understand move semantics with example code. Let's start with a very simple string class which only holds a pointer to a heap-allocated block of memory:

#include <cstring>
#include <algorithm>

class string
{
    char* data;

public:

    string(const char* p)
    {
        size_t size = std::strlen(p) + 1;
        data = new char[size];
        std::memcpy(data, p, size);
    }

Since we chose to manage the memory ourselves, we need to follow the rule of three. I am going to defer writing the assignment operator and only implement the destructor and the copy constructor for now:

    ~string()
    {
        delete[] data;
    }

    string(const string& that)
    {
        size_t size = std::strlen(that.data) + 1;
        data = new char[size];
        std::memcpy(data, that.data, size);
    }

The copy constructor defines what it means to copy string objects. The parameter const string& that binds to all expressions of type string which allows you to make copies in the following examples:

string a(x);                                    // Line 1
string b(x + y);                                // Line 2
string c(some_function_returning_a_string());   // Line 3

Now comes the key insight into move semantics. Note that only in the first line where we copy x is this deep copy really necessary, because we might want to inspect x later and would be very surprised if x had changed somehow. Did you notice how I just said x three times (four times if you include this sentence) and meant the exact same object every time? We call expressions such as x "lvalues".

The arguments in lines 2 and 3 are not lvalues, but rvalues, because the underlying string objects have no names, so the client has no way to inspect them again at a later point in time.
rvalues denote temporary objects which are destroyed at the next semicolon (to be more precise: at the end of the full-expression that lexically contains the rvalue). This is important because during the initialization of b and c, we could do whatever we wanted with the source string, and the client couldn't tell a difference!

C++0x introduces a new mechanism called "rvalue reference" which, among other things,
allows us to detect rvalue arguments via function overloading. All we have to do is write a constructor with an rvalue reference parameter. Inside that constructor we can do anything we want with the source, as long as we leave it in some valid state:

    string(string&& that)   // string&& is an rvalue reference to a string
    {
        data = that.data;
        that.data = nullptr;
    }

What have we done here? Instead of deeply copying the heap data, we have just copied the pointer and then set the original pointer to null (to prevent 'delete[]' from source object's destructor from releasing our 'just stolen data'). In effect, we have "stolen" the data that originally belonged to the source string. Again, the key insight is that under no circumstance could the client detect that the source had been modified. Since we don't really do a copy here, we call this constructor a "move constructor". Its job is to move resources from one object to another instead of copying them.

Congratulations, you now understand the basics of move semantics! Let's continue by implementing the assignment operator. If you're unfamiliar with the copy and swap idiom, learn it and come back, because it's an awesome C++ idiom related to exception safety.

    string& operator=(string that)
    {
        std::swap(data, that.data);
        return *this;
    }
};

Huh, that's it? "Where's the rvalue reference?" you might ask. "We don't need it here!" is my answer :)

Note that we pass the parameter that by value, so that has to be initialized just like any other string object. Exactly how is that going to be initialized? In the olden days of C++98, the answer would have been "by the copy constructor". In C++0x, the compiler chooses between the copy constructor and the move constructor based on whether the argument to the assignment operator is an lvalue or an rvalue.

So if you say a = b, the copy constructor will initialize that (because the expression b is an lvalue), and the assignment operator swaps the contents with a freshly created, deep copy. That is the very definition of the copy and swap idiom -- make a copy, swap the contents with the copy, and then get rid of the copy by leaving the scope. Nothing new here.

But if you say a = x + y, the move constructor will initialize that (because the expression x + y is an rvalue), so there is no deep copy involved, only an efficient move.
that is still an independent object from the argument, but its construction was trivial,
since the heap data didn't have to be copied, just moved. It wasn't necessary to copy it because x + y is an rvalue, and again, it is okay to move from string objects denoted by rvalues.

To summarize, the copy constructor makes a deep copy, because the source must remain untouched.
The move constructor, on the other hand, can just copy the pointer and then set the pointer in the source to null. It is okay to "nullify" the source object in this manner, because the client has no way of inspecting the object again.

I hope this example got the main point across. There is a lot more to rvalue references and move semantics which I intentionally left out to keep it simple. If you want more details please see my supplementary answer.

余生再见 2024-09-13 15:48:18

我的第一个答案是对移动语义的极其简化的介绍,为了简单起见,故意省略了许多细节。
然而,还有很多需要移动语义的地方,我认为是时候用第二个答案来填补空白了。
第一个答案已经很旧了,简单地用完全不同的文本替换它感觉不合适。我认为它仍然可以作为第一个介绍。但如果您想深入了解,请继续阅读:)

Stephan T. Lavavej 花时间提供了宝贵的反馈。非常感谢你,斯蒂芬!

简介

移动语义允许对象在某些条件下获得其他对象的外部资源的所有权。这在两个方面很重要:

  1. 将昂贵的副本变成便宜的动作。请参阅我的第一个答案作为示例。请注意,如果对象不管理至少一种外部资源(直接管理或通过其成员对象间接管理),则移动语义不会比复制语义提供任何优势。在这种情况下,复制对象和移动对象意味着完全相同的事情:

     类cannot_benefit_from_move_semantics
     {
         整数a; // 移动 int 意味着复制 int
         浮动b; // 移动浮点数意味着复制浮点数
         双c; // 移动 double 意味着复制 double
         字符d[64]; // 移动 char 数组意味着复制 char 数组
    
         // ...
     };
    
  2. 实现安全的“仅移动”类型;也就是说,复制没有意义但移动有意义的类型。示例包括锁、文件句柄和具有独特所有权语义的智能指针。注意:本答案讨论了 std::auto_ptr,这是一个已弃用的 C++98 标准库模板,在 C++11 中被 std::unique_ptr 取代。中级 C++ 程序员可能至少对 std::auto_ptr 有所熟悉,并且由于它显示的“移动语义”,它似乎是讨论 C++11 中移动语义的一个很好的起点。 YMMV。

什么是举动?

C++98 标准库提供了一个具有独特所有权语义的智能指针,称为 std::auto_ptr。如果您不熟悉 auto_ptr,它的目的是保证动态分配的对象始终被释放,即使面对异常:

{
    std::auto_ptr<Shape> a(new Triangle);
    // ...
    // arbitrary code, could throw exceptions
    // ...
}   // <--- when a goes out of scope, the triangle is deleted automatically

auto_ptr 的不寻常之处在于它“复制”行为:

auto_ptr<Shape> a(new Triangle);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        |
        |
  +-----|---+
  |   +-|-+ |
a | p | | | |
  |   +---+ |
  +---------+

auto_ptr<Shape> b(a);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        +----------------------+
                               |
  +---------+            +-----|---+
  |   +---+ |            |   +-|-+ |
a | p |   | |          b | p | | | |
  |   +---+ |            |   +---+ |
  +---------+            +---------+

请注意使用 a 初始化 b 时,不会复制三角形,而是从 b 转移三角形的所有权代码>a到b。我们也说“a移入b”或“三角形a<移动” /code>b”。这可能听起来令人困惑,因为三角形本身始终保留在内存中的同一位置。

移动对象意味着将其管理的某些资源的所有权转移给另一个对象。

auto_ptr 的复制构造函数可能看起来像这样(有些简化):

auto_ptr(auto_ptr& source)   // note the missing const
{
    p = source.p;
    source.p = 0;   // now the source no longer owns the object
}

危险且无害的移动

auto_ptr 的危险之处在于,语法上看起来像副本的东西实际上是移动。尝试在移出的 auto_ptr 上调用成员函数将调用未定义的行为,因此您必须非常小心,不要在移出后使用 auto_ptr

auto_ptr<Shape> a(new Triangle);   // create triangle
auto_ptr<Shape> b(a);              // move a into b
double area = a->area();           // undefined behavior

但 auto_ptr 并不总是危险的。工厂函数是 auto_ptr 的一个完美用例:

auto_ptr<Shape> make_triangle()
{
    return auto_ptr<Shape>(new Triangle);
}

auto_ptr<Shape> c(make_triangle());      // move temporary into c
double area = make_triangle()->area();   // perfectly safe

请注意这两个示例如何遵循相同的语法模式:

auto_ptr<Shape> variable(expression);
double area = expression->area();

然而,其中一个会调用未定义的行为,而另一个则不会。那么表达式amake_triangle()有什么区别呢?他们俩不是同一类型吗?确实如此,但它们有不同的价值类别

值类别

显然,表示 auto_ptr 变量的表达式 a 和表示调用按值返回 auto_ptr 的函数,从而在每次调用时创建一个新的临时 auto_ptr 对象。 a左值 的示例,而make_triangle()右值 的示例。

从诸如 a 之类的左值转移是危险的,因为我们稍后可以尝试通过 a 调用成员函数,从而调用未定义的行为。另一方面,从诸如 make_triangle() 之类的右值转移是完全安全的,因为在复制构造函数完成其工作后,我们无法再次使用临时值。没有任何表达方式表示所述暂时的;如果我们简单地再次编写 make_triangle(),我们会得到一个不同的临时值。事实上,移出的临时文件已经在下一行中消失了:

auto_ptr<Shape> c(make_triangle());
                                  ^ the moved-from temporary dies right here

请注意,字母 lr 在左侧和右侧有一个历史起源 -作业的手边。在 C++ 中,情况不再如此,因为有些左值不能出现在赋值的左侧(如数组或没有赋值运算符的用户定义类型),而有些右值可以出现在(类类型的所有右值)与赋值运算符)。

类类型的右值是一个表达式,其计算结果会创建一个临时对象。
一般情况下,同一作用域内没有其他表达式表示同一个临时对象。

右值引用

我们现在知道,从左值转移有潜在危险,但从右值转移是无害的。如果 C++ 具有区分左值参数和右值参数的语言支持,我们可以完全禁止从左值移动,或者至少在调用站点显式地从左值移动,这样我们就不会再意外移动。

C++11 对这个问题的回答是右值引用。右值引用是一种仅绑定到右值的新型引用,语法为X&&。良好的旧引用 X& 现在被称为左值引用。 (请注意,X&&不是对引用的引用;C++ 中没有这样的东西。)

如果我们将 const 放入混合后,我们已经有了四种不同类型的参考。它们可以绑定到哪些类型的 X 表达式?

左值const 左值右值const 右值
X&yes
const X&yesyesyesyes
X&&是的
const X&&yesyes

在实践中,您可以忘记 const X&&。限制读取右值并不是很有用。

右值引用 X&& 是一种仅绑定到右值的新型引用。

隐式转换

右值引用经历了多个版本。从版本 2.1 开始,右值引用 X&& 也会绑定到不同类型 Y 的所有值类别,前提是存在来自 Y 的隐式转换。代码> 到 <代码>X。在这种情况下,将创建 X 类型的临时变量,并将右值引用绑定到该临时变量:

void some_function(std::string&& r);

some_function("hello world");

在上面的示例中,"hello world" 是类型的左值const char[12]。由于存在从 const char[12] 通过 const char*std::string 的隐式转换,因此是 类型的临时变量std::string 被创建,并且 r 绑定到该临时变量。这是右值(表达式)和临时变量(对象)之间的区别有点模糊的情况之一。

移动构造函数

带有 X&& 参数的函数的一个有用示例是 移动构造函数 X::X(X&& source)。其目的是将托管资源的所有权从源转移到当前对象。

在 C++11 中,std::auto_ptr 已被 std::unique_ptr 取代,后者利用了右值引用。我将开发并讨论 unique_ptr 的简化版本。首先,我们封装一个原始指针并重载运算符 ->*,因此我们的类感觉就像一个指针:

template<typename T>
class unique_ptr
{
    T* ptr;

public:

    T* operator->() const
    {
        return ptr;
    }

    T& operator*() const
    {
        return *ptr;
    }

构造函数取得对象的所有权,析构函数取得对象的所有权删除它:

    explicit unique_ptr(T* p = nullptr)
    {
        ptr = p;
    }

    ~unique_ptr()
    {
        delete ptr;
    }

现在是有趣的部分,移动构造函数:

    unique_ptr(unique_ptr&& source)   // note the rvalue reference
    {
        ptr = source.ptr;
        source.ptr = nullptr;
    }

这个移动构造函数的作用与 auto_ptr 复制构造函数的作用完全相同,但它只能提供右值:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);                 // error
unique_ptr<Shape> c(make_triangle());   // okay

第二行无法编译,因为 < code>a 是左值,但参数unique_ptr&& source 只能绑定到右值。这正是我们想要的;危险的举动绝不应该是隐含的。第三行编译得很好,因为 make_triangle() 是一个右值。移动构造函数会将所有权从临时对象转移到 c。再说一遍,这正是我们想要的。

移动构造函数将托管资源的所有权转移到当前对象。

移动赋值运算符

最后缺失的部分是移动赋值运算符。它的工作是释放旧资源并从其参数获取新资源:

    unique_ptr& operator=(unique_ptr&& source)   // note the rvalue reference
    {
        if (this != &source)    // beware of self-assignment
        {
            delete ptr;         // release the old resource

            ptr = source.ptr;   // acquire the new resource
            source.ptr = nullptr;
        }
        return *this;
    }
};

请注意移动赋值运算符的这种实现如何复制析构函数和移动构造函数的逻辑。您熟悉复制和交换的习惯用法吗?它也可以像 move-and-swap 习惯用法一样应用于移动语义:

    unique_ptr& operator=(unique_ptr source)   // note the missing reference
    {
        std::swap(ptr, source.ptr);
        return *this;
    }
};

现在 source 是一个 unique_ptr 类型的变量,它将由移动构造函数初始化;也就是说,实参将被移动到形参中。该参数仍然需要是右值,因为移动构造函数本身有一个右值引用参数。当控制流到达 operator= 的右大括号时,source 超出范围,自动释放旧资源。

移动赋值运算符将托管资源的所有权转移到当前对象中,并释放旧资源。
移动和交换惯用法简化了实现。

有时,我们想要从左值转移 也就是说,有时我们希望编译器将左值视为右值,以便它可以调用移动构造函数,即使它可能是不安全的。
为此,C++11 在标头 内提供了一个名为 std::move 的标准库函数模板。
这个名字有点不幸,因为 std::move 只是将左值转换为右值;它本身移动任何东西。它仅仅允许移动。也许它应该被命名为 std::cast_to_rvaluestd::enable_move,但我们现在仍坚持使用这个名称。

以下是显式从左值移动的方法:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);              // still an error
unique_ptr<Shape> c(std::move(a));   // okay

请注意,在第三行之后,a 不再拥有三角形。没关系,因为通过显式编写 std::move(a),我们明确了我们的意图:“亲爱的构造函数,用 a 做任何你想做的事情 code> 来初始化 c;我不再关心 a 了,随意使用 a 吧。”

std::move(some_lvalue) 将左值转换为右值,从而启用后续移动。

Xvalues

请注意,即使 std::move(a) 是一个右值,它的计算也不会创建一个临时对象。这个难题迫使委员会引入第三个价值类别。可以绑定到右值引用的东西,即使它不是传统意义上的右值,也被称为xvalue(到期值)。传统的右值被重命名为纯右值(纯右值)。

prvalues 和 xvalues 都是右值。 X值和左值都是glvalues(广义左值)。通过图表更容易掌握这些关系:

        expressions
          /     \
         /       \
        /         \
    glvalues   rvalues
      /  \       /  \
     /    \     /    \
    /      \   /      \
lvalues   xvalues   prvalues

请注意,只有 xvalues 是真正新的;其余的只是由于重命名和分组。

C++98 右值在 C++11 中称为纯右值。在心里将前面段落中所有出现的“右值”替换为“纯右值”。

移出函数

到目前为止,我们已经看到了移入局部变量和函数参数。但向相反方向移动也是可能的。如果函数按值返回,则调用站点的某个对象(可能是局部变量或临时对象,但可以是任何类型的对象)将使用 return 语句之后的表达式作为参数进行初始化移动构造函数:

unique_ptr<Shape> make_triangle()
{
    return unique_ptr<Shape>(new Triangle);
}          \-----------------------------/
                  |
                  | temporary is moved into c
                  |
                  v
unique_ptr<Shape> c(make_triangle());

也许令人惊讶的是,自动对象(未声明为静态的局部变量)也可以隐式移出函数:

unique_ptr<Shape> make_square()
{
    unique_ptr<Shape> result(new Square);
    return result;   // note the missing std::move
}

移动构造函数如何接受左值< code>result 作为参数? result 的范围即将结束,它将在堆栈展开期间被销毁。后来没有人会抱怨结果发生了某种变化;当控制流返回调用者时,result 不再存在!因此,C++11 有一个特殊规则,允许从函数返回自动对象,而无需编写 std::move。事实上,您不应该使用 std::move 将自动对象移出函数,因为这会抑制“命名返回值优化”(NRVO)。

切勿使用 std::move 将自动对象移出函数。

请注意,在这两个工厂函数中,返回类型都是值,而不是右值引用。右值引用仍然是引用,并且一如既往,您永远不应该返回对自动对象的引用;如果你欺骗编译器接受你的代码,调用者最终会得到一个悬空引用,如下所示:

unique_ptr<Shape>&& flawed_attempt()   // DO NOT DO THIS!
{
    unique_ptr<Shape> very_bad_idea(new Square);
    return std::move(very_bad_idea);   // WRONG!
}

永远不要通过右值引用返回自动对象。移动仅由移动构造函数执行,而不是由 std::move 执行,并且不仅仅通过将右值绑定到右值引用来执行。

进入成员

迟早,您将编写如下代码:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(parameter)   // error
    {}
};

基本上,编译器会抱怨 parameter 是左值。如果查看它的类型,您会看到一个右值引用,但右值引用仅意味着“绑定到右值的引用”;它确实意味着引用本身是一个右值!事实上,parameter 只是一个带有名称的普通变量。您可以在构造函数体内随意使用参数,并且它始终表示同一个对象。隐式地离开它是危险的,因此语言禁止这样做。

命名右值引用是左值,就像任何其他变量一样。

解决方案是手动启用移动:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(std::move(parameter))   // note the std::move
    {}
};

您可能会认为在 member 初始化后不再使用 parameter。为什么没有像返回值一样静默插入 std::move 的特殊规则?可能是因为这会给编译器实现者带来太大的负担。例如,如果构造函数主体位于另一个翻译单元中怎么办?相比之下,返回值规则只需检查符号表即可确定 return 关键字后面的标识符是否表示自动对象。

您还可以按值传递参数。对于像 unique_ptr 这样的仅移动类型,似乎还没有既定的习惯用法。就我个人而言,我更喜欢按值传递,因为它会减少界面的混乱。

特殊成员函数

C++98 根据需要隐式声明了三个特殊成员函数,即当某处需要它们时:复制构造函数、复制赋值运算符和析构函数。

X::X(const X&);              // copy constructor
X& X::operator=(const X&);   // copy assignment operator
X::~X();                     // destructor

右值引用经历了多个版本。从 3.0 版本开始,C++11 根据需要声明了两个额外的特殊成员函数:移动构造函数和移动赋值运算符。请注意,VC10 和 VC11 都不符合 3.0 版,因此您必须自己实现它们。

X::X(X&&);                   // move constructor
X& X::operator=(X&&);        // move assignment operator

如果没有手动声明特殊成员函数,则仅隐式声明这两个新的特殊成员函数。另外,如果您声明自己的移动构造函数或移动赋值运算符,则复制构造函数和复制赋值运算符都不会被隐式声明。

这些规则在实践中意味着什么?

如果您编写一个没有非托管资源的类,则无需自己声明这五个特殊成员函数中的任何一个,并且您将免费获得正确的复制语义和移动语义。否则,您将必须自己实现特殊的成员函数。当然,如果您的类没有受益于移动语义,则无需实现特殊的移动操作。

请注意,复制赋值运算符和移动赋值运算符可以融合为一个统一的赋值运算符,并按值获取其参数:

X& X::operator=(X source)    // unified assignment operator
{
    swap(source);            // see my first answer for an explanation
    return *this;
}

这样,要实现的特殊成员函数的数量从五个减少到四个。这里需要在异常安全和效率之间进行权衡,但我不是这个问题的专家。

转发参考文献(以前称为< em>通用引用)

考虑以下函数模板:

template<typename T>
void foo(T&&);

您可能期望 T&& 仅绑定到右值,因为乍一看,它看起来像一个右值引用。但事实证明,T&& 也绑定到左值:

foo(make_triangle());   // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a);                 // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&

如果参数是 X 类型的右值,则 T 被推导为是 X,因此 T&& 表示 X&&。这是任何人都会期望的。
但如果参数是 X 类型的左值,则由于特殊规则,T 会被推导为 X&,因此 T&& 的意思类似于 X& &&。但由于 C++ 仍然没有引用到引用的概念,因此类型 X& && 折叠X&。乍一听这可能令人困惑且毫无用处,但引用折叠对于完美转发至关重要(这里不会讨论)。

T&&不是右值引用,而是转发引用。它还绑定到左值,在这种情况下,TT&& 都是左值引用。

如果要将函数模板限制为右值,可以将 SFINAE 与类型结合起来特征:

#include <type_traits>

template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);

move 的实现

现在您已经了解了引用折叠,下面是 std::move 的实现方式:

template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}

如您所见,move 接受任何类型的参数,这要归功于转发引用T&&,并且它返回一个右值引用。 std::remove_reference::type 元函数调用是必要的,因为否则,对于 X 类型的左值,返回类型将为 X& &&,它将折叠成 X&。由于 t 始终是左值(请记住,命名右值引用是左值),但我们希望将 t 绑定到右值引用,因此我们必须显式转换 t 到正确的返回类型。
返回右值引用的函数的调用本身就是一个 xvalue。现在您知道 xvalues 从哪里来了;)

返回右值引用的函数(例如 std::move)的调用是 xvalue。

请注意,在本例中,通过右值引用返回是可以的,因为 t 并不表示自动对象,而是表示调用者传入的对象。

My first answer was an extremely simplified introduction to move semantics, and many details were left out on purpose to keep it simple.
However, there is a lot more to move semantics, and I thought it was time for a second answer to fill the gaps.
The first answer is already quite old, and it did not feel right to simply replace it with a completely different text. I think it still serves well as a first introduction. But if you want to dig deeper, read on :)

Stephan T. Lavavej took the time to provide valuable feedback. Thank you very much, Stephan!

Introduction

Move semantics allows an object, under certain conditions, to take ownership of some other object's external resources. This is important in two ways:

  1. Turning expensive copies into cheap moves. See my first answer for an example. Note that if an object does not manage at least one external resource (either directly, or indirectly through its member objects), move semantics will not offer any advantages over copy semantics. In that case, copying an object and moving an object means the exact same thing:

     class cannot_benefit_from_move_semantics
     {
         int a;        // moving an int means copying an int
         float b;      // moving a float means copying a float
         double c;     // moving a double means copying a double
         char d[64];   // moving a char array means copying a char array
    
         // ...
     };
    
  2. Implementing safe "move-only" types; that is, types for which copying does not make sense, but moving does. Examples include locks, file handles, and smart pointers with unique ownership semantics. Note: This answer discusses std::auto_ptr, a deprecated C++98 standard library template, which was replaced by std::unique_ptr in C++11. Intermediate C++ programmers are probably at least somewhat familiar with std::auto_ptr, and because of the "move semantics" it displays, it seems like a good starting point for discussing move semantics in C++11. YMMV.

What is a move?

The C++98 standard library offers a smart pointer with unique ownership semantics called std::auto_ptr<T>. In case you are unfamiliar with auto_ptr, its purpose is to guarantee that a dynamically allocated object is always released, even in the face of exceptions:

{
    std::auto_ptr<Shape> a(new Triangle);
    // ...
    // arbitrary code, could throw exceptions
    // ...
}   // <--- when a goes out of scope, the triangle is deleted automatically

The unusual thing about auto_ptr is its "copying" behavior:

auto_ptr<Shape> a(new Triangle);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        |
        |
  +-----|---+
  |   +-|-+ |
a | p | | | |
  |   +---+ |
  +---------+

auto_ptr<Shape> b(a);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        +----------------------+
                               |
  +---------+            +-----|---+
  |   +---+ |            |   +-|-+ |
a | p |   | |          b | p | | | |
  |   +---+ |            |   +---+ |
  +---------+            +---------+

Note how the initialization of b with a does not copy the triangle, but instead transfers the ownership of the triangle from a to b. We also say "a is moved into b" or "the triangle is moved from a to b". This may sound confusing because the triangle itself always stays at the same place in memory.

To move an object means to transfer ownership of some resource it manages to another object.

The copy constructor of auto_ptr probably looks something like this (somewhat simplified):

auto_ptr(auto_ptr& source)   // note the missing const
{
    p = source.p;
    source.p = 0;   // now the source no longer owns the object
}

Dangerous and harmless moves

The dangerous thing about auto_ptr is that what syntactically looks like a copy is actually a move. Trying to call a member function on a moved-from auto_ptr will invoke undefined behavior, so you have to be very careful not to use an auto_ptr after it has been moved from:

auto_ptr<Shape> a(new Triangle);   // create triangle
auto_ptr<Shape> b(a);              // move a into b
double area = a->area();           // undefined behavior

But auto_ptr is not always dangerous. Factory functions are a perfectly fine use case for auto_ptr:

auto_ptr<Shape> make_triangle()
{
    return auto_ptr<Shape>(new Triangle);
}

auto_ptr<Shape> c(make_triangle());      // move temporary into c
double area = make_triangle()->area();   // perfectly safe

Note how both examples follow the same syntactic pattern:

auto_ptr<Shape> variable(expression);
double area = expression->area();

And yet, one of them invokes undefined behavior, whereas the other one does not. So what is the difference between the expressions a and make_triangle()? Aren't they both of the same type? Indeed they are, but they have different value categories.

Value categories

Obviously, there must be some profound difference between the expression a which denotes an auto_ptr variable, and the expression make_triangle() which denotes the call of a function that returns an auto_ptr by value, thus creating a fresh temporary auto_ptr object every time it is called. a is an example of an lvalue, whereas make_triangle() is an example of an rvalue.

Moving from lvalues such as a is dangerous, because we could later try to call a member function via a, invoking undefined behavior. On the other hand, moving from rvalues such as make_triangle() is perfectly safe, because after the copy constructor has done its job, we cannot use the temporary again. There is no expression that denotes said temporary; if we simply write make_triangle() again, we get a different temporary. In fact, the moved-from temporary is already gone on the next line:

auto_ptr<Shape> c(make_triangle());
                                  ^ the moved-from temporary dies right here

Note that the letters l and r have a historic origin in the left-hand side and right-hand side of an assignment. This is no longer true in C++, because there are lvalues that cannot appear on the left-hand side of an assignment (like arrays or user-defined types without an assignment operator), and there are rvalues which can (all rvalues of class types with an assignment operator).

An rvalue of class type is an expression whose evaluation creates a temporary object.
Under normal circumstances, no other expression inside the same scope denotes the same temporary object.

Rvalue references

We now understand that moving from lvalues is potentially dangerous, but moving from rvalues is harmless. If C++ had language support to distinguish lvalue arguments from rvalue arguments, we could either completely forbid moving from lvalues, or at least make moving from lvalues explicit at call site, so that we no longer move by accident.

C++11's answer to this problem is rvalue references. An rvalue reference is a new kind of reference that only binds to rvalues, and the syntax is X&&. The good old reference X& is now known as an lvalue reference. (Note that X&& is not a reference to a reference; there is no such thing in C++.)

If we throw const into the mix, we already have four different kinds of references. What kinds of expressions of type X can they bind to?

lvalueconst lvaluervalueconst rvalue
X&yes
const X&yesyesyesyes
X&&yes
const X&&yesyes

In practice, you can forget about const X&&. Being restricted to read from rvalues is not very useful.

An rvalue reference X&& is a new kind of reference that only binds to rvalues.

Implicit conversions

Rvalue references went through several versions. Since version 2.1, an rvalue reference X&& also binds to all value categories of a different type Y, provided there is an implicit conversion from Y to X. In that case, a temporary of type X is created, and the rvalue reference is bound to that temporary:

void some_function(std::string&& r);

some_function("hello world");

In the above example, "hello world" is an lvalue of type const char[12]. Since there is an implicit conversion from const char[12] through const char* to std::string, a temporary of type std::string is created, and r is bound to that temporary. This is one of the cases where the distinction between rvalues (expressions) and temporaries (objects) is a bit blurry.

Move constructors

A useful example of a function with an X&& parameter is the move constructor X::X(X&& source). Its purpose is to transfer ownership of the managed resource from the source into the current object.

In C++11, std::auto_ptr<T> has been replaced by std::unique_ptr<T> which takes advantage of rvalue references. I will develop and discuss a simplified version of unique_ptr. First, we encapsulate a raw pointer and overload the operators -> and *, so our class feels like a pointer:

template<typename T>
class unique_ptr
{
    T* ptr;

public:

    T* operator->() const
    {
        return ptr;
    }

    T& operator*() const
    {
        return *ptr;
    }

The constructor takes ownership of the object, and the destructor deletes it:

    explicit unique_ptr(T* p = nullptr)
    {
        ptr = p;
    }

    ~unique_ptr()
    {
        delete ptr;
    }

Now comes the interesting part, the move constructor:

    unique_ptr(unique_ptr&& source)   // note the rvalue reference
    {
        ptr = source.ptr;
        source.ptr = nullptr;
    }

This move constructor does exactly what the auto_ptr copy constructor did, but it can only be supplied with rvalues:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);                 // error
unique_ptr<Shape> c(make_triangle());   // okay

The second line fails to compile, because a is an lvalue, but the parameter unique_ptr&& source can only be bound to rvalues. This is exactly what we wanted; dangerous moves should never be implicit. The third line compiles just fine, because make_triangle() is an rvalue. The move constructor will transfer ownership from the temporary to c. Again, this is exactly what we wanted.

The move constructor transfers ownership of a managed resource into the current object.

Move assignment operators

The last missing piece is the move assignment operator. Its job is to release the old resource and acquire the new resource from its argument:

    unique_ptr& operator=(unique_ptr&& source)   // note the rvalue reference
    {
        if (this != &source)    // beware of self-assignment
        {
            delete ptr;         // release the old resource

            ptr = source.ptr;   // acquire the new resource
            source.ptr = nullptr;
        }
        return *this;
    }
};

Note how this implementation of the move assignment operator duplicates logic of both the destructor and the move constructor. Are you familiar with the copy-and-swap idiom? It can also be applied to move semantics as the move-and-swap idiom:

    unique_ptr& operator=(unique_ptr source)   // note the missing reference
    {
        std::swap(ptr, source.ptr);
        return *this;
    }
};

Now that source is a variable of type unique_ptr, it will be initialized by the move constructor; that is, the argument will be moved into the parameter. The argument is still required to be an rvalue, because the move constructor itself has an rvalue reference parameter. When control flow reaches the closing brace of operator=, source goes out of scope, releasing the old resource automatically.

The move assignment operator transfers ownership of a managed resource into the current object, releasing the old resource.
The move-and-swap idiom simplifies the implementation.

Moving from lvalues

Sometimes, we want to move from lvalues. That is, sometimes we want the compiler to treat an lvalue as if it were an rvalue, so it can invoke the move constructor, even though it could be potentially unsafe.
For this purpose, C++11 offers a standard library function template called std::move inside the header <utility>.
This name is a bit unfortunate, because std::move simply casts an lvalue to an rvalue; it does not move anything by itself. It merely enables moving. Maybe it should have been named std::cast_to_rvalue or std::enable_move, but we are stuck with the name by now.

Here is how you explicitly move from an lvalue:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);              // still an error
unique_ptr<Shape> c(std::move(a));   // okay

Note that after the third line, a no longer owns a triangle. That's okay, because by explicitly writing std::move(a), we made our intentions clear: "Dear constructor, do whatever you want with a in order to initialize c; I don't care about a anymore. Feel free to have your way with a."

std::move(some_lvalue) casts an lvalue to an rvalue, thus enabling a subsequent move.

Xvalues

Note that even though std::move(a) is an rvalue, its evaluation does not create a temporary object. This conundrum forced the committee to introduce a third value category. Something that can be bound to an rvalue reference, even though it is not an rvalue in the traditional sense, is called an xvalue (eXpiring value). The traditional rvalues were renamed to prvalues (Pure rvalues).

Both prvalues and xvalues are rvalues. Xvalues and lvalues are both glvalues (Generalized lvalues). The relationships are easier to grasp with a diagram:

        expressions
          /     \
         /       \
        /         \
    glvalues   rvalues
      /  \       /  \
     /    \     /    \
    /      \   /      \
lvalues   xvalues   prvalues

Note that only xvalues are really new; the rest is just due to renaming and grouping.

C++98 rvalues are known as prvalues in C++11. Mentally replace all occurrences of "rvalue" in the preceding paragraphs with "prvalue".

Moving out of functions

So far, we have seen movement into local variables, and into function parameters. But moving is also possible in the opposite direction. If a function returns by value, some object at call site (probably a local variable or a temporary, but could be any kind of object) is initialized with the expression after the return statement as an argument to the move constructor:

unique_ptr<Shape> make_triangle()
{
    return unique_ptr<Shape>(new Triangle);
}          \-----------------------------/
                  |
                  | temporary is moved into c
                  |
                  v
unique_ptr<Shape> c(make_triangle());

Perhaps surprisingly, automatic objects (local variables that are not declared as static) can also be implicitly moved out of functions:

unique_ptr<Shape> make_square()
{
    unique_ptr<Shape> result(new Square);
    return result;   // note the missing std::move
}

How come the move constructor accepts the lvalue result as an argument? The scope of result is about to end, and it will be destroyed during stack unwinding. Nobody could possibly complain afterward that result had changed somehow; when control flow is back at the caller, result does not exist anymore! For that reason, C++11 has a special rule that allows returning automatic objects from functions without having to write std::move. In fact, you should never use std::move to move automatic objects out of functions, as this inhibits the "named return value optimization" (NRVO).

Never use std::move to move automatic objects out of functions.

Note that in both factory functions, the return type is a value, not an rvalue reference. Rvalue references are still references, and as always, you should never return a reference to an automatic object; the caller would end up with a dangling reference if you tricked the compiler into accepting your code, like this:

unique_ptr<Shape>&& flawed_attempt()   // DO NOT DO THIS!
{
    unique_ptr<Shape> very_bad_idea(new Square);
    return std::move(very_bad_idea);   // WRONG!
}

Never return automatic objects by rvalue reference. Moving is exclusively performed by the move constructor, not by std::move, and not by merely binding an rvalue to an rvalue reference.

Moving into members

Sooner or later, you are going to write code like this:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(parameter)   // error
    {}
};

Basically, the compiler will complain that parameter is an lvalue. If you look at its type, you see an rvalue reference, but an rvalue reference simply means "a reference that is bound to an rvalue"; it does not mean that the reference itself is an rvalue! Indeed, parameter is just an ordinary variable with a name. You can use parameter as often as you like inside the body of the constructor, and it always denotes the same object. Implicitly moving from it would be dangerous, hence the language forbids it.

A named rvalue reference is an lvalue, just like any other variable.

The solution is to manually enable the move:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(std::move(parameter))   // note the std::move
    {}
};

You could argue that parameter is not used anymore after the initialization of member. Why is there no special rule to silently insert std::move just as with return values? Probably because it would be too much burden on the compiler implementors. For example, what if the constructor body was in another translation unit? By contrast, the return value rule simply has to check the symbol tables to determine whether or not the identifier after the return keyword denotes an automatic object.

You can also pass the parameter by value. For move-only types like unique_ptr, it seems there is no established idiom yet. Personally, I prefer to pass by value, as it causes less clutter in the interface.

Special member functions

C++98 implicitly declares three special member functions on demand, that is, when they are needed somewhere: the copy constructor, the copy assignment operator, and the destructor.

X::X(const X&);              // copy constructor
X& X::operator=(const X&);   // copy assignment operator
X::~X();                     // destructor

Rvalue references went through several versions. Since version 3.0, C++11 declares two additional special member functions on demand: the move constructor and the move assignment operator. Note that neither VC10 nor VC11 conforms to version 3.0 yet, so you will have to implement them yourself.

X::X(X&&);                   // move constructor
X& X::operator=(X&&);        // move assignment operator

These two new special member functions are only implicitly declared if none of the special member functions are declared manually. Also, if you declare your own move constructor or move assignment operator, neither the copy constructor nor the copy assignment operator will be declared implicitly.

What do these rules mean in practice?

If you write a class without unmanaged resources, there is no need to declare any of the five special member functions yourself, and you will get correct copy semantics and move semantics for free. Otherwise, you will have to implement the special member functions yourself. Of course, if your class does not benefit from move semantics, there is no need to implement the special move operations.

Note that the copy assignment operator and the move assignment operator can be fused into a single, unified assignment operator, taking its argument by value:

X& X::operator=(X source)    // unified assignment operator
{
    swap(source);            // see my first answer for an explanation
    return *this;
}

This way, the number of special member functions to implement drops from five to four. There is a tradeoff between exception-safety and efficiency here, but I am not an expert on this issue.

Forwarding references (previously known as Universal references)

Consider the following function template:

template<typename T>
void foo(T&&);

You might expect T&& to only bind to rvalues, because at first glance, it looks like an rvalue reference. As it turns out though, T&& also binds to lvalues:

foo(make_triangle());   // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a);                 // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&

If the argument is an rvalue of type X, T is deduced to be X, hence T&& means X&&. This is what anyone would expect.
But if the argument is an lvalue of type X, due to a special rule, T is deduced to be X&, hence T&& would mean something like X& &&. But since C++ still has no notion of references to references, the type X& && is collapsed into X&. This may sound confusing and useless at first, but reference collapsing is essential for perfect forwarding (which will not be discussed here).

T&& is not an rvalue reference, but a forwarding reference. It also binds to lvalues, in which case T and T&& are both lvalue references.

If you want to constrain a function template to rvalues, you can combine SFINAE with type traits:

#include <type_traits>

template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);

Implementation of move

Now that you understand reference collapsing, here is how std::move is implemented:

template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}

As you can see, move accepts any kind of parameter thanks to the forwarding reference T&&, and it returns an rvalue reference. The std::remove_reference<T>::type meta-function call is necessary because otherwise, for lvalues of type X, the return type would be X& &&, which would collapse into X&. Since t is always an lvalue (remember that a named rvalue reference is an lvalue), but we want to bind t to an rvalue reference, we have to explicitly cast t to the correct return type.
The call of a function that returns an rvalue reference is itself an xvalue. Now you know where xvalues come from ;)

The call of a function that returns an rvalue reference, such as std::move, is an xvalue.

Note that returning by rvalue reference is fine in this example, because t does not denote an automatic object, but instead an object that was passed in by the caller.

双马尾 2024-09-13 15:48:18

假设你有一个返回实体对象的函数:

Matrix multiply(const Matrix &a, const Matrix &b);

当你编写这样的代码时:

Matrix r = multiply(a, b);

那么普通的 C++ 编译器将为 multiply() 的结果创建一个临时对象,调用复制构造函数来初始化 < code>r,然后销毁临时返回值。 C++0x 中的移动语义允许调用“移动构造函数”,通过复制 r 的内容来初始化 r,然后丢弃临时值,而不必破坏它。

如果(就像上面的 Matrix 示例)被复制的对象在堆上分配额外的内存来存储其内部表示,这一点尤其重要。复制构造函数必须制作内部表示的完整副本,或者内部使用引用计数和写时复制语义。移动构造函数将保留堆内存,仅将指针复制到 Matrix 对象内。

Suppose you have a function that returns a substantial object:

Matrix multiply(const Matrix &a, const Matrix &b);

When you write code like this:

Matrix r = multiply(a, b);

then an ordinary C++ compiler will create a temporary object for the result of multiply(), call the copy constructor to initialise r, and then destruct the temporary return value. Move semantics in C++0x allow the "move constructor" to be called to initialise r by copying its contents, and then discard the temporary value without having to destruct it.

This is especially important if (like perhaps the Matrix example above), the object being copied allocates extra memory on the heap to store its internal representation. A copy constructor would have to either make a full copy of the internal representation, or use reference counting and copy-on-write semantics interally. A move constructor would leave the heap memory alone and just copy the pointer inside the Matrix object.

红尘作伴 2024-09-13 15:48:18

移动语义是指当没有人再需要源值时传输资源而不是复制资源

在 C++03 中,对象经常被复制,只是在任何代码再次使用该值之前被销毁或分配。例如,当您从函数按值返回时(除非 RVO 启动),您返回的值将被复制到调用者的堆栈帧,然后超出范围并被销毁。这只是许多示例之一:当源对象是临时对象时,请参阅按值传递;像 sort 这样的算法只是重新排列项目,当 vector 的 < code>capacity() 被超出,等等。

当这样的复制/销毁对很昂贵时,通常是因为该对象拥有一些重量级资源。例如,vector可以拥有一个动态分配的内存块,其中包含string对象的数组,每个对象都有自己的动态内存。复制这样的对象代价高昂:您必须为源中的每个动态分配的块分配新的内存,并复制所有值。 然后您需要释放刚刚复制的所有内存。但是,移动一个大向量意味着只需将一些指针(引用动态内存块)复制到目标并将它们在源中清零。

Move semantics is about transferring resources rather than copying them when nobody needs the source value anymore.

In C++03, objects are often copied, only to be destroyed or assigned-over before any code uses the value again. For example, when you return by value from a function—unless RVO kicks in—the value you're returning is copied to the caller's stack frame, and then it goes out of scope and is destroyed. This is just one of many examples: see pass-by-value when the source object is a temporary, algorithms like sort that just rearrange items, reallocation in vector when its capacity() is exceeded, etc.

When such copy/destroy pairs are expensive, it's typically because the object owns some heavyweight resource. For example, vector<string> may own a dynamically-allocated memory block containing an array of string objects, each with its own dynamic memory. Copying such an object is costly: you have to allocate new memory for each dynamically-allocated blocks in the source, and copy all the values across. Then you need deallocate all that memory you just copied. However, moving a large vector<string> means just copying a few pointers (that refer to the dynamic memory block) to the destination and zeroing them out in the source.

慕巷 2024-09-13 15:48:18

用简单(实用)的术语来说:

复制对象意味着复制其“静态”成员并为其动态对象调用 new 运算符。正确的?

class A
{
   int i, *p;

public:
   A(const A& a) : i(a.i), p(new int(*a.p)) {}
   ~A() { delete p; }
};

然而,移动一个对象(我重复一遍,从实际的角度来看)仅意味着复制动态对象的指针,而不是创建新的对象。

但是,这样不危险吗?当然,您可以两次破坏动态对象(分段错误)。因此,为了避免这种情况,您应该“无效”源指针以避免两次破坏它们:

class A
{
   int i, *p;

public:
   // Movement of an object inside a copy constructor.
   A(const A& a) : i(a.i), p(a.p)
   {
     a.p = nullptr; // pointer invalidated.
   }

   ~A() { delete p; }
   // Deleting NULL, 0 or nullptr (address 0x0) is safe. 
};

好吧,但是如果我移动一个对象,源对象就会变得无用,不是吗?当然可以,但在某些情况下这非常有用。最明显的一个是当我使用匿名对象(时间对象、右值对象等)调用函数时:

void heavyFunction(HeavyType());

在这种情况下,会创建一个匿名对象,然后将其复制到函数参数,并随后删除。所以,这里最好移动对象,因为不需要匿名对象,并且可以节省时间和内存。

这就引出了“右值”引用的概念。它们存在于 C++11 中只是为了检测接收到的对象是否是匿名的。我认为您已经知道“左值”是一个可分配的实体(= 运算符的左侧部分),因此您需要一个对对象的命名引用才能充当左值。右值正好相反,是一个没有命名引用的对象。因此,匿名对象和右值是同义词。因此:

class A
{
   int i, *p;

public:
   // Copy
   A(const A& a) : i(a.i), p(new int(*a.p)) {}

   // Movement (&& means "rvalue reference to")
   A(A&& a) : i(a.i), p(a.p)
   {
      a.p = nullptr;
   }

   ~A() { delete p; }
};

在这种情况下,当应“复制”类型 A 的对象时,编译器会根据传递的对象是否命名来创建左值引用或右值引用。如果没有,您的移动构造函数将被调用,并且您知道该对象是临时的,您可以移动其动态对象而不是复制它们,从而节省空间和内存。

重要的是要记住“静态”对象总是被复制。没有办法“移动”静态对象(堆栈中的对象而不是堆上的对象)。因此,当对象没有动态成员(直接或间接)时,“移动”/“复制”的区别是无关紧要的。

如果您的对象很复杂,并且析构函数具有其他次要效果,例如调用库的函数、调用其他全局函数或其他任何内容,也许最好用标志来表示运动:

class Heavy
{
   bool b_moved;
   // staff

public:
   A(const A& a) { /* definition */ }
   A(A&& a) : // initialization list
   {
      a.b_moved = true;
   }

   ~A() { if (!b_moved) /* destruct object */ }
};

因此,您的代码更短(您不需要不需要为每个动态成员执行 nullptr 分配)并且更通用。

其他典型问题:A&&const A&& 之间有什么区别?当然,在第一种情况下,您可以修改对象,而在第二种情况下则不能,但是,实际意义是什么?在第二种情况下,您无法修改它,因此您无法使对象无效(除非使用可变标志或类似的东西),并且与复制构造函数没有实际区别。

什么是完美转发?重要的是要知道“右值引用”是对“调用者范围”中命名对象的引用。但在实际作用域中,右值引用是对象的名称,因此,它充当命名对象。如果将右值引用传递给另一个函数,则传递的是命名对象,因此,该对象不会像临时对象一样被接收。

void some_function(A&& a)
{
   other_function(a);
}

对象a将被复制到other_function的实际参数。如果您希望对象 a 继续被视为临时对象,则应使用 std::move 函数:

other_function(std::move(a));

在这一行中,std::move< /code> 将把 a 转换为右值,而 other_function 将接收该对象作为未命名对象。当然,如果 other_function 没有特定的重载来处理未命名的对象,那么这种区别并不重要。

这样就完美转发了吗?不是,但我们非常接近。完美转发仅对使用模板有用,其目的是说:如果我需要将对象传递给另一个函数,我需要如果我收到命名对象,则该对象作为命名对象传递,而当没有时,我想像传递未命名对象一样传递它:

template<typename T>
void some_function(T&& a)
{
   other_function(std::forward<T>(a));
}

这是使用完美转发的原型函数的签名,通过 std::forward 在 C++11 中实现。此函数利用了模板实例化的一些规则:

 `A& && == A&`
 `A&& && == A&&`

因此,如果 T 是对 A 的左值引用 (T = A&),a 也 (A& && => A&)。如果 T 是对 A 的右值引用,则 a 也是 (A&& && => A&&)。在这两种情况下,a 都是实际作用域中的命名对象,但从调用者作用域的角度来看,T 包含其“引用类型”的信息。此信息 (T) 作为模板参数传递给 forward,并且根据 T 的类型移动或不移动“a”。

In easy (practical) terms:

Copying an object means copying its "static" members and calling the new operator for its dynamic objects. Right?

class A
{
   int i, *p;

public:
   A(const A& a) : i(a.i), p(new int(*a.p)) {}
   ~A() { delete p; }
};

However, to move an object (I repeat, in a practical point of view) implies only to copy the pointers of dynamic objects, and not to create new ones.

But, is that not dangerous? Of course, you could destruct a dynamic object twice (segmentation fault). So, to avoid that, you should "invalidate" the source pointers to avoid destructing them twice:

class A
{
   int i, *p;

public:
   // Movement of an object inside a copy constructor.
   A(const A& a) : i(a.i), p(a.p)
   {
     a.p = nullptr; // pointer invalidated.
   }

   ~A() { delete p; }
   // Deleting NULL, 0 or nullptr (address 0x0) is safe. 
};

Ok, but if I move an object, the source object becomes useless, no? Of course, but in certain situations that's very useful. The most evident one is when I call a function with an anonymous object (temporal, rvalue object, ..., you can call it with different names):

void heavyFunction(HeavyType());

In that situation, an anonymous object is created, next copied to the function parameter, and afterwards deleted. So, here it is better to move the object, because you don't need the anonymous object and you can save time and memory.

This leads to the concept of an "rvalue" reference. They exist in C++11 only to detect if the received object is anonymous or not. I think you do already know that an "lvalue" is an assignable entity (the left part of the = operator), so you need a named reference to an object to be capable to act as an lvalue. A rvalue is exactly the opposite, an object with no named references. Because of that, anonymous object and rvalue are synonyms. So:

class A
{
   int i, *p;

public:
   // Copy
   A(const A& a) : i(a.i), p(new int(*a.p)) {}

   // Movement (&& means "rvalue reference to")
   A(A&& a) : i(a.i), p(a.p)
   {
      a.p = nullptr;
   }

   ~A() { delete p; }
};

In this case, when an object of type A should be "copied", the compiler creates a lvalue reference or a rvalue reference according to if the passed object is named or not. When not, your move-constructor is called and you know the object is temporal and you can move its dynamic objects instead of copying them, saving space and memory.

It is important to remember that "static" objects are always copied. There's no ways to "move" a static object (object in stack and not on heap). So, the distinction "move"/ "copy" when an object has no dynamic members (directly or indirectly) is irrelevant.

If your object is complex and the destructor has other secondary effects, like calling to a library's function, calling to other global functions or whatever it is, perhaps is better to signal a movement with a flag:

class Heavy
{
   bool b_moved;
   // staff

public:
   A(const A& a) { /* definition */ }
   A(A&& a) : // initialization list
   {
      a.b_moved = true;
   }

   ~A() { if (!b_moved) /* destruct object */ }
};

So, your code is shorter (you don't need to do a nullptr assignment for each dynamic member) and more general.

Other typical question: what is the difference between A&& and const A&&? Of course, in the first case, you can modify the object and in the second not, but, practical meaning? In the second case, you can't modify it, so you have no ways to invalidate the object (except with a mutable flag or something like that), and there is no practical difference to a copy constructor.

And what is perfect forwarding? It is important to know that a "rvalue reference" is a reference to a named object in the "caller's scope". But in the actual scope, a rvalue reference is a name to an object, so, it acts as a named object. If you pass an rvalue reference to another function, you are passing a named object, so, the object isn't received like a temporal object.

void some_function(A&& a)
{
   other_function(a);
}

The object a would be copied to the actual parameter of other_function. If you want the object a continues being treated as a temporary object, you should use the std::move function:

other_function(std::move(a));

With this line, std::move will cast a to an rvalue and other_function will receive the object as a unnamed object. Of course, if other_function has not specific overloading to work with unnamed objects, this distinction is not important.

Is that perfect forwarding? Not, but we are very close. Perfect forwarding is only useful to work with templates, with the purpose to say: if I need to pass an object to another function, I need that if I receive a named object, the object is passed as a named object, and when not, I want to pass it like a unnamed object:

template<typename T>
void some_function(T&& a)
{
   other_function(std::forward<T>(a));
}

That's the signature of a prototypical function that uses perfect forwarding, implemented in C++11 by means of std::forward. This function exploits some rules of template instantiation:

 `A& && == A&`
 `A&& && == A&&`

So, if T is a lvalue reference to A (T = A&), a also (A& && => A&). If T is a rvalue reference to A, a also (A&& && => A&&). In both cases, a is a named object in the actual scope, but T contains the information of its "reference type" from the caller scope's point of view. This information (T) is passed as template parameter to forward and 'a' is moved or not according to the type of T.

廻憶裏菂餘溫 2024-09-13 15:48:18

如果您确实对移动语义的良好、深入的解释感兴趣,我强烈建议您阅读有关它们的原始论文,“向 C++ 语言添加移动语义支持的提案。”

它非常易于阅读,并且为他们提供的好处。还有其他关于移动语义的最新论文可以在 WG21 网站,但这可能是最直接的,因为它从顶层视图处理事物,并且没有深入了解具体的语言细节。

If you are really interested in a good, in-depth explanation of move semantics, I'd highly recommend reading the original paper on them, "A Proposal to Add Move Semantics Support to the C++ Language."

It's very accessible and easy to read and it makes an excellent case for the benefits that they offer. There are other more recent and up to date papers about move semantics available on the WG21 website, but this one is probably the most straightforward since it approaches things from a top-level view and doesn't get very much into the gritty language details.

等风也等你 2024-09-13 15:48:18

这就像复制语义,但不必复制所有数据,而是从被“移动”的对象中窃取数据。

It's like copy semantics, but instead of having to duplicate all of the data you get to steal the data from the object being "moved" from.

罪歌 2024-09-13 15:48:18

你知道复制语义是什么意思吗?这意味着您拥有可复制的类型,对于用户定义的类型,您可以明确地编写复制构造函数并定义它。赋值运算符或编译器隐式生成它们。这将制作一份副本。

移动语义基本上是一个用户定义的类型,其构造函数采用右值引用(使用 && (是的两个 & 符号)的新引用类型),它是非常量的,这称为移动构造函数,同样适用赋值运算符。那么移动构造函数是做什么的,它不是从源参数复制内存,而是将内存从源“移动”到目标。

你什么时候想这样做? std::vector 就是一个例子,假设您创建了一个临时 std::vector 并从函数返回它说:

std::vector<foo> get_foos();

当函数返回时,您将获得复制构造函数的开销,如果(并且在 C 中会如此) ++0x) std::vector 有一个移动构造函数,而不是复制它,只需设置它的指针并将动态分配的内存“移动”到新实例。这有点像 std::auto_ptr 的所有权转移语义。

You know what a copy semantics means right? it means you have types which are copyable, for user-defined types you define this either buy explicitly writing a copy constructor & assignment operator or the compiler generates them implicitly. This will do a copy.

Move semantics is basically a user-defined type with constructor that takes an r-value reference (new type of reference using && (yes two ampersands)) which is non-const, this is called a move constructor, same goes for assignment operator. So what does a move constructor do, well instead of copying memory from it's source argument it 'moves' memory from the source to the destination.

When would you want to do that? well std::vector is an example, say you created a temporary std::vector and you return it from a function say:

std::vector<foo> get_foos();

You're going to have overhead from the copy constructor when the function returns, if (and it will in C++0x) std::vector has a move constructor instead of copying it can just set it's pointers and 'move' dynamically allocated memory to the new instance. It's kind of like transfer-of-ownership semantics with std::auto_ptr.

她说她爱他 2024-09-13 15:48:18

我写这篇文章是为了确保我正确理解它。

创建移动语义是为了避免不必要的大型对象复制。 Bjarne Stroustrup 在他的《C++ 编程语言》一书中使用了两个默认情况下发生不必要复制的示例:一是交换两个大对象,二是从方法返回一个大对象。

交换两个大对象通常涉及将第一个对象复制到临时对象、将第二个对象复制到第一个对象、以及将临时对象复制到第二个对象。对于内置类型,这非常快,但对于大型对象,这三个副本可能需要大量时间。 “移动分配”允许程序员覆盖默认的复制行为,而是交换对对象的引用,这意味着根本没有复制,并且交换操作要快得多。可以通过调用 std::move() 方法来调用移动赋值。

默认情况下,从方法返回对象涉及在调用者可以访问的位置创建本地对象及其关联数据的副本(因为调用者无法访问本地对象,并且本地对象会在方法完成时消失)。当返回内置类型时,此操作非常快,但如果返回大对象,则可能需要很长时间。移动构造函数允许程序员覆盖此默认行为,而是通过将返回给调用者的对象指向与本地对象关联的堆数据来“重用”与本地对象关联的堆数据。因此不需要复制。

在不允许创建本地对象(即堆栈上的对象)的语言中,不会发生这些类型的问题,因为所有对象都在堆上分配并且始终通过引用访问。

I'm writing this to make sure I understand it properly.

Move semantics were created to avoid the unnecessary copying of large objects. Bjarne Stroustrup in his book "The C++ Programming Language" uses two examples where unnecessary copying occurs by default: one, the swapping of two large objects, and two, the returning of a large object from a method.

Swapping two large objects usually involves copying the first object to a temporary object, copying the second object to the first object, and copying the temporary object to the second object. For a built-in type, this is very fast, but for large objects these three copies could take a large amount of time. A "move assignment" allows the programmer to override the default copy behavior and instead swap references to the objects, which means that there is no copying at all and the swap operation is much faster. The move assignment can be invoked by calling the std::move() method.

Returning an object from a method by default involves making a copy of the local object and its associated data in a location which is accessible to the caller (because the local object is not accessible to the caller and disappears when the method finishes). When a built-in type is being returned, this operation is very fast, but if a large object is being returned, this could take a long time. The move constructor allows the programmer to override this default behavior and instead "reuse" the heap data associated with the local object by pointing the object being returned to the caller to heap data associated with the local object. Thus no copying is required.

In languages which do not allow the creation of local objects (that is, objects on the stack) these types of problems do not occur as all objects are allocated on the heap and are always accessed by reference.

留一抹残留的笑 2024-09-13 15:48:18

为了说明对移动语义的需求,让我们考虑一下这个没有移动语义的示例:

下面是一个函数,它接受一个 T 类型的对象并返回一个相同类型 的对象>T

T f(T o) { return o; }
  //^^^ new object constructed

上面的函数使用按值调用,这意味着当调用此函数时,必须构造一个对象才能供该函数使用。
因为函数也是按值返回,所以为返回值构造了另一个新对象:

T b = f(a);
  //^ new object constructed

构造了两个个新对象,其中一个是临时对象,仅用于函数的持续时间。

当从返回值创建新对象时,将调用复制构造函数将临时对象的内容复制到新对象b。函数完成后,函数中使用的临时对象超出范围并被销毁。


现在,让我们考虑一下复制构造函数的作用。

它必须首先初始化对象,然后将所有相关数据从旧对象复制到新对象。
根据类的不同,也许它是一个包含大量数据的容器,那么它可以代表大量的时间内存使用

// Copy constructor
T::T(T &old) {
    copy_data(m_a, old.m_a);
    copy_data(m_b, old.m_b);
    copy_data(m_c, old.m_c);
}

使用移动语义现在可以实现通过简单地移动数据而不是复制来减少大部分工作的不愉快。

// Move constructor
T::T(T &&old) noexcept {
    m_a = std::move(old.m_a);
    m_b = std::move(old.m_b);
    m_c = std::move(old.m_c);
}

移动数据涉及将数据与新对象重新关联。并且根本不会发生任何复制。

这是通过右值引用来完成的。
rvalue 引用的工作方式与 lvalue 引用非常相似,但有一个重要区别:
右值引用可以移动,而左值引用则不能移动。

来自 cppreference.com

为了使强大的异常保证成为可能,用户定义的移动构造函数不应抛出异常。事实上,当容器元素需要重新定位时,标准容器通常依赖 std::move_if_noexcept 在移动和复制之间进行选择。
如果同时提供了复制构造函数和移动构造函数,如果参数是右值(纯右值,例如无名临时值或 xvalue,例如 std::move 的结果),则重载解析会选择移动构造函数,如果参数是左值(命名对象或返回左值引用的函数/运算符)。如果仅提供复制构造函数,则所有参数类别都会选择它(只要它接受对 const 的引用,因为右值可以绑定到 const 引用),这使得在移动不可用时复制成为移动的后备。
在许多情况下,移动构造函数会被优化,即使它们会产生明显的副作用,请参阅复制省略。
当构造函数采用右值引用作为参数时,称为“移动构造函数”。它没有义务移动任何东西,类不需要移动资源,并且“移动构造函数”可能无法移动资源,就像在允许的(但可能不明智的)情况下,参数是const 右值引用 (const T&&)。

To illustrate the need for move semantics, let's consider this example without move semantics:

Here's a function that takes an object of type T and returns an object of the same type T:

T f(T o) { return o; }
  //^^^ new object constructed

The above function uses call by value which means that when this function is called an object must be constructed to be used by the function.
Because the function also returns by value, another new object is constructed for the return value:

T b = f(a);
  //^ new object constructed

Two new objects have been constructed, one of which is a temporary object that's only used for the duration of the function.

When the new object is created from the return value, the copy constructor is called to copy the contents of the temporary object to the new object b. After the function completes, the temporary object used in the function goes out of scope and is destroyed.


Now, let's consider what a copy constructor does.

It must first initialize the object, then copy all the relevant data from the old object to the new one.
Depending on the class, maybe its a container with very much data, then that could represent much time and memory usage

// Copy constructor
T::T(T &old) {
    copy_data(m_a, old.m_a);
    copy_data(m_b, old.m_b);
    copy_data(m_c, old.m_c);
}

With move semantics it's now possible to make most of this work less unpleasant by simply moving the data rather than copying.

// Move constructor
T::T(T &&old) noexcept {
    m_a = std::move(old.m_a);
    m_b = std::move(old.m_b);
    m_c = std::move(old.m_c);
}

Moving the data involves re-associating the data with the new object. And no copy takes place at all.

This is accomplished with an rvalue reference.
An rvalue reference works pretty much like an lvalue reference with one important difference:
an rvalue reference can be moved and an lvalue cannot.

From cppreference.com:

To make strong exception guarantee possible, user-defined move constructors should not throw exceptions. In fact, standard containers typically rely on std::move_if_noexcept to choose between move and copy when container elements need to be relocated.
If both copy and move constructors are provided, overload resolution selects the move constructor if the argument is an rvalue (either a prvalue such as a nameless temporary or an xvalue such as the result of std::move), and selects the copy constructor if the argument is an lvalue (named object or a function/operator returning lvalue reference). If only the copy constructor is provided, all argument categories select it (as long as it takes a reference to const, since rvalues can bind to const references), which makes copying the fallback for moving, when moving is unavailable.
In many situations, move constructors are optimized out even if they would produce observable side-effects, see copy elision.
A constructor is called a 'move constructor' when it takes an rvalue reference as a parameter. It is not obligated to move anything, the class is not required to have a resource to be moved and a 'move constructor' may not be able to move a resource as in the allowable (but maybe not sensible) case where the parameter is a const rvalue reference (const T&&).

似狗非友 2024-09-13 15:48:18

移动语义是编程语言中的一项功能,允许将数据从一个对象有效传输到另一个对象,而无需不必要的复制或分配内存。它允许开发人员将一个对象的资源(例如内存)移动到另一个对象中,而不是创建副本,从而优化程序的性能。这对于资源密集型操作特别有用,例如处理大型数据集或在资源受限的环境中管理内存。大多数面向对象的编程语言(例如 C++、Java 和 Python)都支持移动语义。

Move semantics is a feature in programming languages that allows for the efficient transfer of data from one object to another without unnecessary copying or allocation of memory. It optimizes the performance of programs by allowing developers to move the resources (such as memory) of one object into another object, instead of creating a copy. This is particularly useful for resource-intensive operations, such as handling large data sets or managing memory in resource-constrained environments. Move semantics is supported by most object-oriented programming languages, such as C++, Java, and Python.

帝王念 2024-09-13 15:48:18

这是 Bjarne 所著《C++ 编程语言》一书中的答案斯特鲁斯特鲁普。如果您不想观看该视频,可以查看下面的文本:

考虑此片段。从运算符+返回涉及将结果从局部变量res复制到调用者可以访问它的某个地方。

Vector operator+(const Vector& a, const Vector& b)
{
    if (a.size()!=b.size())
        throw Vector_siz e_mismatch{};
    Vector res(a.size());
        for (int i=0; i!=a.size(); ++i)
            res[i]=a[i]+b[i];
    return res;
}

我们并不是真的想要一份副本;我们只是想要一份副本。我们只是想从函数中获取结果。所以我们需要移动 Vector 而不是复制它。 如下:

class Vector {
    // ...
    Vector(const Vector& a); // copy constructor
    Vector& operator=(const Vector& a); // copy assignment
    Vector(Vector&& a); // move constructor
    Vector& operator=(Vector&& a); // move assignment
};

Vector::Vector(Vector&& a)
    :elem{a.elem}, // "grab the elements" from a
    sz{a.sz}
{
    a.elem = nullptr; // now a has no elements
    a.sz = 0;
}

我们可以定义移动构造函数 表示“右值引用”,是我们可以绑定右值的引用。 “rvalue”旨在补充“lvalue”,其大致意思是“可以出现在赋值左侧的东西”。因此,右值大致意味着“无法分配给的值”,例如函数调用返回的整数,以及向量的operator+() 中的res 局部变量。

现在,语句 return res; 将不会复制!

Here's an answer from the book "The C++ Programming Language" by Bjarne Stroustrup. If you don't want to see the video, you can see the text below:

Consider this snippet. Returning from an operator+ involves copying the result out of the local variable res and into someplace where the caller can access it.

Vector operator+(const Vector& a, const Vector& b)
{
    if (a.size()!=b.size())
        throw Vector_siz e_mismatch{};
    Vector res(a.size());
        for (int i=0; i!=a.size(); ++i)
            res[i]=a[i]+b[i];
    return res;
}

We didn’t really want a copy; we just wanted to get the result out of a function. So we need to move a Vector rather than to copy it. We can define move constructor as follows:

class Vector {
    // ...
    Vector(const Vector& a); // copy constructor
    Vector& operator=(const Vector& a); // copy assignment
    Vector(Vector&& a); // move constructor
    Vector& operator=(Vector&& a); // move assignment
};

Vector::Vector(Vector&& a)
    :elem{a.elem}, // "grab the elements" from a
    sz{a.sz}
{
    a.elem = nullptr; // now a has no elements
    a.sz = 0;
}

The && means "rvalue reference" and is a reference to which we can bind an rvalue. "rvalue"’ is intended to complement "lvalue" which roughly means "something that can appear on the left-hand side of an assignment." So an rvalue means roughly "a value that you can’t assign to", such as an integer returned by a function call, and the res local variable in operator+() for Vectors.

Now, the statement return res; will not copy!

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文