std::move 在转发参数而不是移动构造时在参数列表中安全吗?

Is std::move safe in an arguments list when the argument is forwarded, not move constructed?

试图为 提供解决方案,我正在尝试将 std::unordered_set<std::string> 替换为 std::unordered_map<std::string_view, std::unique_ptr<std::string>>(值为 std::unique_ptr<std::string>,因为小字符串优化会意味着 string 的基础数据的地址不会因为 std::move) 而总是被传输。

我原来的测试代码,似乎有效,是(省略headers):

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 在构建 string_view 之前不会被清空(通过移动构造存储在地图中的 std::unique_ptr )。将考虑的 try_emplace 的两个定义是:

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

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

我不确定会选择哪个,但无论哪种方式,似乎 key_type 都将作为调用 try_emplace 的一部分构建,而 mapped_type 的参数](“值”,虽然地图似乎使用 value_type 来指代组合的 key/value pair)一起转发,而不是立即使用,这使得代码定义。我的解释是否正确,或者这是未定义的行为?

我担心的是,其他看起来绝对未定义的类似结构似乎仍然有效,例如:

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,尽管其中 none 发出任何类型的警告,并且两个构造似乎同样未排序(工作中未排序的隐式转换 insert,在段错误 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 是“稍后”构建的。

是的,您拨打 try_emplace 是绝对安全的。 std::move 实际上并没有移动任何东西,它只是将传递的变量转换为一个 xvalue。无论参数的初始化顺序如何,都不会移动任何内容,因为参数都是引用。引用直接绑定到对象,它们不调用任何构造函数。

如果您查看第二个代码段,您会发现 std::make_pair 也通过引用获取其参数,因此在这种情况下,除了在构造函数主体中外,也不会进行移动。

然而,您的第三个片段确实存在 UB 问题。差别很小,但是如果 make_pair 的参数是从左到右计算的,那么一个临时的 std::unique_ptr 对象将被初始化为 to_insert 的值。这意味着现在 to_insert 为空,因为移动确实发生了,因为您正在显式构建一个实际执行移动的对象。