当参数是转发而不是移动构造时,在参数列表中使用std::move是否安全?

5
尝试为std :: string_view和std :: string在std :: unordered_set中提供解决方案,我正在尝试用 std :: unordered_map > 替换 std :: unordered_set (值是 std :: unique_ptr ,因为小字符串优化意味着 string 的基础数据的地址不总是作为 std :: move 的结果传输)。

我的原始测试代码(省略头文件)似乎有效:

using namespace std::literals;

int main(int argc, char **argv) {
    std::unordered_map<std::string_view, std::unique_ptr<std::string>> mymap;

    for (int i = 1; i < argc; ++i) {
        auto to_insert = std::make_unique<std::string>(argv[i]);
        
        mymap.try_emplace(*to_insert, std::move(to_insert));
    }

    for (auto&& entry : mymap) {
        std::cout << entry.first << ": " << entry.second << std::endl;
    }

    std::cout << std::boolalpha << "\"this\" in map? " << (mymap.count("this") == 1) << std::endl;
    std::cout << std::boolalpha << "\"this\"s in map? " << (mymap.count("this"s) == 1) << std::endl;
    std::cout << std::boolalpha << "\"this\"sv in map? " << (mymap.count("this"sv) == 1) << std::endl;
    return EXIT_SUCCESS;
}

我使用 g++ 7.2.0 进行编译,编译命令为 g++ -O3 -std=c++17 -Wall -Wextra -Werror -flto -pedantic test_string_view.cpp -o test_string_view,没有收到任何警告,然后运行程序,得到以下输出:
$ test_string_view this is a test this is a second test
second: second
test: test
a: a
this: this
is: is
"this" in map? true
"this"s in map? true
"this"sv in map? true

这正是我所预期的。

我的主要关注点在于是否:

        mymap.try_emplace(*to_insert, std::move(to_insert));

已定义行为。 *to_insert 依赖于 to_insert 不被清空(通过移动构造存储在 map 中的 std::unique_ptr)直到 string_view 构造完成。将考虑两个定义 try_emplace

try_emplace(const key_type& k, Args&&... args);

并且

try_emplace(key_type&& k, Args&&... args);

我不确定会选择哪个,但无论如何,似乎在调用try_emplace时,key_type都会被构建,而将mapped_type(“value”,尽管映射似乎使用value_type来引用组合键/值对)的参数转发,而不立即使用,这使代码定义。我的理解正确吗,还是这是未定义的行为?
我的担忧是其他类似的构造似乎明显是未定义的,但仍然可以工作,例如:
mymap.insert(std::make_pair<std::string_view,
                            std::unique_ptr<std::string>>(*to_insert,
                                                          std::move(to_insert)));

这段代码可以产生预期的输出,而类似的结构如下:

mymap.insert(std::make_pair(std::string_view(*to_insert),
                            std::unique_ptr<std::string>(std::move(to_insert))));

在运行时触发了Segmentation fault,尽管它们都没有引起任何警告,并且两个构造似乎同样无序(工作的insert中的无序隐式转换,在导致segfault的insert中的无序显式转换),所以我不想说“try_emplace对我有用,所以没问题。”

请注意,虽然这个问题与C++11: std::move() call on arguments' list类似,但它并不完全相同(这是使此处的std::make_pair不安全的原因,但不一定适用于try_emplace的基于转发的行为);在那个问题中,接收参数的函数接收std::unique_ptr,立即触发构造,而try_emplace接收用于转发而不是std::unique_ptr的参数,因此虽然std::move已经“发生”了(但还没有做任何事情),我认为我们是安全的,因为std::unique_ptr是“稍后”构造的。

1个回答

4
是的,你对try_emplace的调用是完全安全的。 std :: move实际上不会移动任何东西,它只是将传递的变量转换为xvalue。无论参数以什么顺序初始化,永远不会移动任何内容,因为参数都是引用。 引用直接绑定到对象,不调用任何构造函数。
如果您查看第二个片段,您将看到std :: make_pair 也按引用获取其参数,在这种情况下,除非在构造函数体中进行了移动,否则也不会进行移动。
但是,您的第三个片段确实存在UB问题。差异微妙,但是如果评估make_pair的参数是从左到右进行的,则会使用已移动的to_insert值来初始化临时创建的std :: unique_ptr对象。这意味着现在,to_insert为null,因为实际上执行了移动操作,因为您正在显式构造一个执行移动操作的对象。

4
虽然这是正确的,但我仍然建议您非常小心。您必须知道所调用函数的签名才能确定所有参数都是引用,以便确保安全。如果作者随后更改参数为按值传递,则这将不再安全。对于一个完全不了解函数签名的人来说,foo.frobnicate(*bar, std::move(bar)) 会引起警觉。 - Justin
第二个片段仍在使用std::make_pair,而不是直接使用std::pair构造函数,它只是明确指定了模板类型(触发隐式构造),而不是显式构造,然后传递。那样不同样不安全吗?即使通过通用引用接收,它也必须构造正确的类型,只是将构造时间从“调用前”移动到“调用时”,但两者都是无序的,对吧?在“加载”参数并将其转换为所需类型之间没有序列点,对吧? - ShadowRanger
@ShadowRanger 噢,不过这没有关系。是的,参数需要初始化,但因为它们是引用,如果忽略函数体,就不会发生移动。实际上,如果你用与make_pair相同签名但空函数体的函数替换它,就不会进行移动操作。虽然没有更多的序列点,但是每个参数都会被初始化,然后是下一个参数...没有交错或类似的情况。 - Rakete1111
@Rakete1111:啊,我看到了我的错误。通常情况下,当需要转换时,通过引用接收仍然需要构造。但在这种情况下,只有第一个参数需要转换,而不是第二个;它立即从*to_insert构造string_view,但只是将xvalue引用传递给to_insert,而不清空它;unique_ptr已经是正确的类型。就像try_emplace一样,string_view 在调用std::make_pair时立即创建的,而unique_ptr直到后来才被移动构造。谢谢! - ShadowRanger

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