C++11移动语义和重新赋值

4

这段代码可以编译,但是我只是开始学习C++11,无法理解背后发生了什么。

void func(int&& var)
{
    int var2(var);
}

void main()
{

    int var1 = 22;
    func(move(var1));
}

我的猜测是:move(var1)返回var1的右值(可能是其数据),而func函数使用var1的右值初始化var2。但是为什么在调用func之后,var1仍然有效呢?它的临时值不应该已重新分配给var2而无效了吗?

5
int类型转移只是复制。 - Tony The Lion
std::move函数必须保持参数有效。这意味着它并不需要实际移动所有类型。像int这样的基本类型将不会被移动,因为那样会使它们无效。 - Some programmer dude
1
虽然在答案中提到int将是一个副本,但我认为类型移动的方式不正确。 int && var是对rvalue的引用(使用std :: move转换),但是var(按名称)就像lvalue一样(因为现在可以通过名称引用它),并且int var2(var);无论如何都不会将var移动到var2中,而是会复制(将调用复制构造函数)。尝试“移动”的正确方法是int 'var2(std :: move(var));' - Abhijit-K
int main,并且去掉 using namespace std;,这样会使代码更清晰 :) - David Rodríguez - dribeas
它不应该有无效值,因为它的临时值已被重新分配给var2吗?不是的,标准要求从中移动的对象处于有效的未确定状态(即它不强制规定状态是什么,但必须是有效的)。 - David Rodríguez - dribeas
可能是What can I do with a moved-from object?的重复问题。 - Nicol Bolas
5个回答

6
这里有几个问题。
首先,你正在使用一个 int。对于一个 int 来说,复制和移动的速度是一样快的。因此,将移动实现为复制是完全合理的。
其次,移动构造(或赋值)不需要改变被移动对象的值。在代码中继续将其视为有用值是逻辑错误,但不要求该值变得无用。
其次,你的代码并没有真正地移动任何东西。仅仅使用 ::std::move 并不会导致任何东西被移动。它只是一种将 lvalue 转换为 rvalue 的好方法,以便可以移动某些东西。在你的情况下,var 有一个名称,所以它是一个 lvalue。你在 func 中初始化的 var2 实际上是一个复制,而不是一个移动。如果你写成 int var2(move(var));,那么它就是一个移动操作,此时 main 中的 var1 可能会失效。
再次强调,::std::move 函数只是为了向阅读你的代码的人发出信号,表示可能会发生移动,并且不能保证变量的值。它并不会实际移动任何东西。
下面是你的代码的标记版本:
// func requires a temporary argument, something that can be moved from
void func(int&& var)
{
    int var2(var); // Doesn't actually call int(int &&), calls int(int &)
    // int var2(move(var));  // Would actually call int(int &&) and a move would
                             // happen then at this point and potentially alter the
                             // value of what var is referencing (which is var1
                             // in this code).
}

void main()
{
    int var1 = 22;
    func(move(var1)); // Tell people that var1's value may be unspecified after
                      // Also, turn var1 into a temporary to satisfy func's
                      // signature.
}

因为你的代码不会移动任何东西,这里有一个版本可以确保移动某些东西到其他地方:

#include <vector>
#include <utility>

using ::std;

// Still require an rvalue (aka a temporary) argument.
void func(vector<int>&& var)
{
   // But, of course, inside the function var has a name, and is thus an lvalue.
   // So use ::std::move again to turn it into an rvalue.
   vector<int> var2(move(var));
   // Poof, now whetever var was referencing no longer has the value it used to.
   // Whatever value it had has been moved into var2.
}

int main()
{
   vector<int> var1 = { 32, 23, 66, 12 };
   func(move(var1)); // After this call, var1's value may no longer be useful.
   // And, in fact, with this code, it will likely end up being an empty
   // vector<int>.
}

当然,这种写法很愚蠢。有时候需要指定一个参数是临时的。通常情况下,如果你有一个版本的函数接受引用,而另一个版本接受右值引用,那么就需要指定。但一般情况下不需要这样做。因此,下面是惯用的编写代码的方法:

#include <vector>
#include <utility>

using ::std;

// You just want a local variable that you can mess with without altering the
// value in the caller. If the caller wants to move, that should be the caller's
// choice, not yours.
void func(vector<int> var)
{
   // You could create var2 here and move var into it, but since var is a
   // local variable already, why bother?
}

int main()
{
   vector<int> var1 = { 32, 23, 66, 12 };
   func(move(var1));  // The move now does actually happen right here when
                      // constructing the argument to func and var1's value
                      // will change.
}

当然,在那段代码中给var1命名有点儿愚蠢。实际上,它应该这样写:
   func(vector<int>{ {32, 23, 66, 12} });

那么你只是构建一个临时向量并将其传递给 func。不需要明确使用 move

确实,移动构造函数并不要求使被移动的对象无效。事实上,恰恰相反:该对象必须保持有效,以便至少可以被销毁。也就是说,“有效”在这里是一个模糊的术语,在明确定义之前,对其后果进行推理是没有意义的。 - Pete Becker
@PeteBecker:我应该用更好的词。我已经修复了。你是对的,它并不会“使值无效”。只是这个值不再一定有任何有用的东西。你唯一可以保证能够正常工作的操作就是销毁。 - Omnifarious
3
实际上,我并不反对你的说法;这只是标准委员会讨论的大问题的一部分,即当对象已被移动时,您拥有哪些保证。我提到它是为了引起人们对基本问题的关注:对象的移动构造函数必须至少确保可以销毁被移动的对象,在C++库类型的情况下,没有前置条件的成员函数仍然必须合理地工作。对于用户定义的类型,显然可析性很重要,其他任何内容都取决于设计需求。 - Pete Becker

3

对于int类型,没有什么需要移动的,因此会创建一个副本。对于具有移动构造函数或移动赋值运算符并且具有特定目的以移动底层资源的类型,将发生更改,否则将进行复制。

std::move会将左值转换为右值,以便可以将某些资源(堆上的对象或文件句柄)移动到其他对象中,但实际的移动是在移动构造函数或移动赋值运算符中发生的。

例如,考虑标准库中的向量,它具有移动复制构造函数和移动赋值运算符,可以执行一些显式工作以跨越移动资源。

话虽如此,我认为在函数func内部移动类型的方式不正确。int&& var是对rvalue的引用(通过std::move转换),然而var(按名称)就像是左值,int var2(var);无论如何都不会将var移动到var2中,它将是COPY。尝试移动的正确方法是使用int var2(std::move(var));,我的意思是如果类型具有可以移动资源的移动构造函数,您必须使用这样的方式。

void func(int&& var) //2. Var is a reference to rvalue however the by name the var is a lvalue and if passed as it would invoke copy constructor.
{
    int var2(std::move(var)); // 3. hence to invoke move constructor if it exists the correct attempt would be to case it to rvalue again as move constructors take rvalue. If the move constructor does not exists then copy is called. 
}

void main()
{

    int var1 = 22;
    func(move(var1)); //1. Cast the type to rvalue so that it can be passed to a move copy contructor or move assigment operator. 
}

2
表达式var1是lvalue。应用std::move(var1)会给你一个引用同一对象的rvalue。然后,您将此rvalue绑定到名为varint&&中。
表达式var也是lvalue。这是因为任何命名变量的表达式都是lvalue(即使其类型是rvalue引用)。然后,您使用var的值初始化var2
所以你所做的就是将var1的值复制到var2。根本没有移动任何东西。事实上,您甚至不能从像int这样的基本类型中移动。尝试从临时int初始化或赋值只会复制它的值,而不会移动任何内容。
即使您使用具有移动构造函数和赋值运算符的类型T,您的代码也不会移动任何内容。那是因为您唯一的非引用初始化是int var2(var);,但在这里var是lvalue。这意味着将使用复制构造函数。
var1移动到var2的最简单方法是这样做:
void func(T var)
{
    T var2(move(var));
}

void main()
{

    T var1(22);
    func(move(var1));
}

这将从var1移动到创建var,然后再从var移动到创建var2

可以用几乎相同的方式做到这一点,只需将var更改回右值引用即可,但我不建议这样做。你需要记录传递的右值将被内部移动使其无效。


1

move(var1) 返回 var1 的一个 r-value(可能是它的数据)

不,move(var1) 返回一个 rvalue 引用,指向 var1

func 函数正在使用 var1 的 r-value 初始化 var2

funcvar1 复制到 var2。如果 var1 是具有移动构造函数的类型,则将调用移动构造函数来初始化 var2。但它不是这样的。

但为什么在 func 调用后 var1 仍然有效?

因为复制一个 int 不会使其无效。

它不应该有一个无效值吗?

int 没有特殊的“无效值”。使用未初始化的对象具有未定义的行为,但此对象并未未初始化。


1
正如其他人所说,您的示例使用了原始类型,实质上会调用复制,因为在这里无法进行移动。但是,我已经创建了一个示例,在该示例中可以发生移动,使用了std::string。它的内部可以从下面移动出来,在这个示例中,您可以看到发生了这种情况。
#include<iostream>
#include<string>
#include<utility>

void func(std::string&& var)
{
    // var is an lvalue, so call std::move on it to make it an rvalue to invoke move ctor
    std::string var2(std::move(var));
    std::cout << "var2: " << var2 << std::endl;
}

int main()
{

    std::string var1 = "Tony";
    std::cout << "before call: " << var1 << std::endl;
    func(std::move(var1));
    std::cout << "after call: "  << var1 << std::endl;
}

输出:

before call: Tony
var2: Tony
after call: 

你可以看到,var1已经被移动,并且不再包含任何数据,但是根据标准它仍处于未指定的状态,并且仍可重用。

实时示例


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接