将正在移动的对象的成员作为参数传递是否安全

Is it safe to pass as argument a member of an object which is moving

本文关键字:参数传递 是否 安全 对象 移动 成员      更新时间:2023-10-16
#include <iostream>
#include <string>
#include <map>
struct A {
    int n { 42 };
    std::string s { "ciao" };
};
int main() {
    A a;
    std::map<std::string, A> m;
    std::cout << "a.s: " << a.s << std::endl; // print: "a.s: ciao"
    m.emplace(a.s, std::move(a)); // a.s is a member of a, moved in the same line
    std::cout << "in map: " << m.count("ciao") << std::endl; // print: "in map: 1"
    std::cout << "a.s: " << a.s << std::endl; // print: "a.s: " (as expected, it has been moved)
}

"移动"对象的成员作为参数传递是否安全?在这种情况下,emplace似乎有效:地图具有预期的键。

有趣。我认为这是安全的,原因错综复杂。(为了记录,我也认为这是非常糟糕的风格 - 显式副本在这里不花钱,因为它将被移动到地图中。

首先,实际的函数调用不是问题。 std::move仅将a强制转换为右值引用,右值引用只是引用;a 不会立即移动。 emplace_back将其参数转发给std::pair<std::string, A>的构造函数,这就是事情变得有趣的地方。

那么,使用std::pair的哪个构造函数呢?它有相当多的,但有两个是相关的:

pair(const T1& x, const T2& y);
template<class U, class V> pair(U&& x, U&&y);

(参见标准中的 20.3.2(,其中 T1T2std::pair 的模板参数。根据 13.3,我们最终以 U == const T1&V == T2 进入后者,这很直观(否则实际上不可能进入std::pair(。这给我们留下了一个形式的构造函数

pair(const T1& x, T2 &&y) : first(std::forward(x)), second(std::forward(y)) { }

根据 20.3.2 (6-8(。

那么,这安全吗?有用的是,std::pair详细定义,包括内存布局。它特别指出,

T1 first;
T2 second;

按此顺序出现,因此first将在second之前初始化。这意味着在您的特定情况下,字符串将在移走之前被复制,并且您是安全的。

但是,如果您以相反的方式执行此操作:

m.emplace(std::move(A.s), A); // huh?

。然后你会得到有趣的效果。

调用 m.emplace(a.s, std::move(a)); 时发生的情况是,您将 l 值引用传递给 a.s,将 r 值引用传递给 emplace 函数a。这是否安全取决于该函数的实现。如果它首先从第二个参数中剔除胆量,然后尝试使用第一个参数,您可能会遇到问题。如果它首先使用第一个参数,然后从第二个参数中剔除胆量,没问题。由于这是一个标准的库函数,你不能依赖实现,所以我们不能断定代码是安全的。

为了安全起见,您可以确保在将字符串的内容移出a之前创建字符串的副本。

m.emplace(std::string(a.s), std::move(a));

现在,您将 r 值引用传递给 a.s 的临时副本,并将 r 值引用传递给 emplace 函数a。因此,在这种情况下它是安全的,因为std::map::emplace采用一组通用引用。如果你对一个函数做同样的事情,它按值接受第二个参数,这是不安全的,因为移动构造函数可以在复制a.s之前被调用(因为函数参数的计算顺序未指定(。知道这一点,这段代码可能太聪明了,无法维护。

因此,最清晰的代码是这样的:

auto key = a.s;
m.emplace(std::move(key), std::move(a));
相关文章: