C++17 表达式求值顺序和 std::move

C++17 expression evaluation order and std::move

今天重构一些代码以将原始指针更改为 std::unique_ptr,我遇到了由于 order of evaluation 错误导致的分段错误。

旧代码执行如下操作:

void add(const std::string& name, Foo* f)
{
    _foo_map[name] = f;
}

void process(Foo* f)
{
    add(f->name, f);
}

要使用的代码的第一次、天真、重构std::unique_ptr:

void add(const std::string& name, std::unique_ptr<Foo> f)
{
    _foo_map[name] = std::move(f);
}

void process(std::unique_ptr<Foo> f)
{
    add(f->name, std::move(f)); // segmentation-fault on f->name
}

重构代码导致分段错误,因为第 2 个参数 (std::move(f)) 首先被处理,然后第 1 个参数 (f->name) 取消引用移出的变量,砰!

可能的解决方法是在 Foo::name 之前获取句柄,然后 将其移动到 add:

void process(std::unique_ptr<Foo> f)
{
    const std::string& name = f->name;
    add(name, std::move(f));
}

或者也许:

void process(std::unique_ptr<Foo> f)
{
    Foo* fp = f.get();
    add(fp->name, std::move(f));
}

这两种解决方案都需要额外的代码行,并且看起来不像对 add.

的原始(尽管是 UB)调用那么可组合

问题:

对我来说,这 2 个提案看起来很糟糕。在其中任何一个中,您都在移动 Foo 对象。这意味着您不能再对其之后的状态做出任何假设。在处理第一个参数(对字符串的引用或指向对象的指针)之前,它可以在 add 函数内部被释放。是的,它可以在当前的实现中工作,但一旦有人触及 add 的实现或其中更深层次的任何内容,它就会崩溃。

安全方法:

  • 复制字符串并将其作为第一个参数传递给add
  • 重构您的 add 方法,使其仅接受类型为 Foo 的单个参数,并在方法内部提取 Foo::name 并且不将其作为参数。但是你在 add 方法中仍然有同样的问题。

在第二种方法中,您应该能够通过首先创建一个新的映射条目(具有默认值)并获取对其的可变引用并随后分配值来解决评估顺序问题:

auto& entry = _foo_map[f->name];
entry = std::move(f);

不确定您的地图实现是否支持获取对条目的可变引用,但对许多人来说应该可以。

如果我再考虑一下,您也可以选择 "copy the name" 方法。无论如何都需要为地图键复制它。如果您手动复制它,您可以为密钥移动它,没有开销。

std::string name = f->name;
_foo_map[std::move(name)] = std::move(f);

编辑:

正如评论中指出的那样,应该可以在 add 函数中直接分配 _foo_map[f->name] = std::move(f),因为此处保证了评估顺序。

您可以让 add 通过引用引用 f,这避免了不必要的 copy/move(相同的 copy/move 会破坏 f->name):

void add(const std::string& name, std::unique_ptr<Foo> && f)
{
    _foo_map[name] = std::move(f);
}

void process(std::unique_ptr<Foo> f)
{
    add(f->name, std::move(f));
}

必须在调用 operator= 之前评估 foo_map[name],因此即使 name 引用依赖于 f 的内容也没有问题。