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

时间:2018-08-08 17:09:25

标签: c++ c++17 move-semantics string-view

尝试提供对std::string_view and std::string in std::unordered_set的解决方案,我正在尝试用std::unordered_set<std::string>替换std::unordered_map<std::string_view, std::unique_ptr<std::string>>(值是std::unique_ptr<std::string>,因为小的字符串优化将意味着由于stringstd::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直到构造std::unique_ptr之后才被清空(通过移动构造存储在映射中的string_view)。将考虑的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来引用组合的键/值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,尽管它们都未发出任何警告,而且两种结构似乎都没有顺序(工作insert中没有顺序的隐式转换,在段错误{{ 1}}),所以我不想说“ insert对我有用,所以没关系。”

请注意,尽管此问题与C++11: std::move() call on arguments' list类似,但并非完全相同(这可能会使try_emplace在此不安全,但不一定适用于基于std::make_pair的转发行为);在该问题中,接收参数的函数接收try_emplace,立即触发构造,而std::unique_ptr接收转发参数,而不是try_emplace,因此std::unique_ptr已“发生“(但是什么也没做),我认为我们很安全,因为std::move的构造是“以后”。

1 个答案:

答案 0 :(得分:4)

是的,您拨给try_emplace的电话非常安全。 std::move实际上不移动任何东西,它只是将传递的变量强制转换为xvalue。无论参数以什么顺序初始化,都不会移动任何内容,因为参数都是引用。引用直接绑定到对象,它们不调用任何构造函数。

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

您的第三个代码段确实存在UB问题。区别是细微的,但是如果make_pair的参数从左到右求值,则临时std::unique_ptr对象将使用从to_insert的值移动来初始化。这意味着现在to_insert为null,因为实际上发生了移动,因为您正在显式构造一个实际执行移动的对象。