【发布时间】:2026-02-11 18:50:01
【问题描述】:
当我阅读一些文章时,右值引用和移动语义通常是一起描述的。但是,据我了解,右值引用只是对右值的引用,与移动语义无关。甚至可以在不使用右值引用的情况下实现移动语义。所以问题是,为什么移动 constructor/operator= 使用右值引用?只是为了让代码更容易编写吗?
【问题讨论】:
当我阅读一些文章时,右值引用和移动语义通常是一起描述的。但是,据我了解,右值引用只是对右值的引用,与移动语义无关。甚至可以在不使用右值引用的情况下实现移动语义。所以问题是,为什么移动 constructor/operator= 使用右值引用?只是为了让代码更容易编写吗?
【问题讨论】:
考虑问题。我们要支持两种基本的移动操作:移动“构造”和移动“赋值”。我在那里使用引号是因为我们不一定要使用构造函数或移动赋值运算符来实现它们;我们可以用别的东西。
移动“构造”意味着通过从现有对象转移内容来创建新对象,这样删除旧对象不会释放新对象中现在使用的资源。移动“分配”意味着获取一个预先存在的对象并从现有对象中转移内容,这样删除旧对象并不会释放新对象中现在使用的资源。
好的,这些就是我们想要做的操作。那么,怎么做呢?
采取行动“建设”。虽然我们没有必须通过构造函数调用来实现这一点,但我们真的想要这样做。我们不想强迫人们进行两阶段移动构建,即使它背后有一些神奇的函数调用。所以我们希望能够将运动作为构造函数来实现。好的,好的。
这里的问题1:构造函数没有名字。因此,您只能根据参数类型和重载分辨率来区分它们。我们知道T 类型的对象的移动构造函数必须将T 类型的对象作为参数。而且由于它只需要一个参数,因此它看起来就像一个复制构造函数。
好的,所以现在我们需要一些方法来满足重载。我们可以引入一些标准库类型,std::move_ref。它就像std::reference_wrapper,但它是一个独特的类型。因此,您可以说移动构造函数是采用std::move_ref<T> 的构造函数。好的,好的:问题解决了。
只是没有;我们现在遇到了新问题。考虑这段代码:
std::string MakeAString() { return std::string("foo"); }
std::string data = MakeAString();
忽略省略,C++11 的表达式值类别规则规定从函数按值返回的类型是纯右值。因此,它将自动被移动构造函数/赋值运算符尽可能使用。不需要std::move 之类的。
要按照您的方式进行操作,您需要这样做:
std::string MakeAString() { return std::move(std::string("foo")); }
std::string data = std::move(MakeAString());
这两个std::move 调用都需要避免复制。您必须移出临时值并进入返回值,然后移出返回值并进入data(同样,忽略省略)。
如果您认为这只是一个小烦恼,请考虑一下右值引用还能给我们带来什么:完美转发。如果没有特殊的引用折叠规则,您将无法编写一个正确的转发函数来完美地转发复制和移动语义。 std::move_ref 将是真正的 C++ 类型;你不能像使用右值引用那样随意地使用规则,比如引用折叠到它上面。
归根结底,您需要某种语言结构,而不仅仅是一种库类型。通过使其成为一种新的引用,您可以为可以绑定到该引用的内容(以及不能绑定的内容)定义新规则。您还可以定义特殊的引用折叠规则,使完美转发成为可能。
【讨论】:
连接是从 rvalue 移动是安全的(因为(在没有强制转换的情况下)rvalues 指的是在其生命周期结束时的对象),因此可以通过从被引用对象中窃取/移动来安全地实现采用 rvalue 引用的构造函数。
从 C++ 语言的角度来看,这是连接的结束,但标准库通过不断地从 lvalues 复制和从 rvalues 构造来进一步扩展此连接 移动,并通过提供辅助函数(例如std::move),可以直接选择是移动还是复制特定对象(通过更改导致复制的表达式中对象的值类别/移动)。
移动语义可以在没有 rvalue-references 的情况下实现,但它会不那么整洁。需要解决一些问题:
如何通过非常量引用捕获右值?
如何区分复制的构造函数和移动的构造函数?
如何确保在任何可以安全优化的地方使用移动?
如何编写适用于可移动和可复制对象的通用代码?
【讨论】: