今天在重构一些代码以更改指向 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
}
重构的代码会导致段错误,因为首先处理第二个参数 (std::move(f)
),然后处理第一个参数 (f->name
) 取消引用一个移动的变量,繁荣!
对此的可能解决方法是获取 Foo::name
的句柄 before 在调用 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));
}
这两种解决方案都需要额外的代码行,而且看起来不像最初(尽管是 UB)对 add
的调用那样可组合。
问题:
上面提出的 2 个解决方案中的任何一个惯用 C++,如果没有,是否有更好的替代方案?
我看到由于 P0145R3 - Refining Expression Evaluation Order for Idiomatic C++ 而在 C++17 中会有一些变化.这会改变上述任何解决方案/防止它们的陷阱吗?
最佳答案
对我来说,这 2 个提案看起来很糟糕。在其中任何一个中,您都将 Foo
对象随移动而放弃。这意味着你不能再对它的状态做出任何假设。在处理第一个参数(对字符串的引用或指向对象的指针)之前,它可以在 add
函数中被释放。是的,它可以在当前的实现中工作,但是一旦有人触及 add
的实现或其中更深层次的任何东西,它就会中断。
安全的方法:
- 复制字符串并将其作为第一个参数传递给
add
- 重构您的
add
方法,使其只接受Foo
类型的单个参数,并在方法中提取Foo::name
而不会不要把它当作论据。但是,您在add
方法中仍然遇到同样的问题。
在第二种方法中,您应该能够通过首先创建一个新的映射条目(具有默认值)并获取对其的可变引用并随后分配值来解决评估顺序问题:
auto& entry = _foo_map[f->name];
entry = std::move(f);
不确定您的 map 实现是否支持获取对条目的可变引用,但对于许多人来说它应该可以工作。
如果我再考虑一下,您可能还会选择“复制名称”的方法。无论如何都需要为映射键复制它。如果您手动复制它,您可以移动它作为 key ,没有开销。
std::string name = f->name;
_foo_map[std::move(name)] = std::move(f);
编辑:
正如评论中指出的那样,应该可以在 add
函数中直接分配 _foo_map[f->name] = std::move(f)
因为此处保证评估顺序。
关于C++17 表达式求值顺序和 std::move,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/40618581/