虽然今天重构了一些代码以将原始指针更改为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
之前获取句柄{/ 1}} 在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
那样可组合。
问题:
上面提到的两个解决方案中的任何一个惯用 C ++,如果没有,是否有更好的选择?
由于P0145R3 - Refining Expression Evaluation Order for Idiomatic C++,我看到C ++ 17出现了变化。这会改变上述任何一种解决方案/防止陷阱吗?
答案 0 :(得分:6)
对我来说,这两个提案看起来很糟糕。在其中任何一个中,你都要移动你的Foo
对象。这意味着在此之后你不能再对它的状态作出任何假设。在处理第一个参数(对字符串或指向对象的指针)之前,可以在add
函数内部释放它。是的,它可以在当前的实现中工作,但只要有人触及add
的实现或更深入的任何内容,它就会崩溃。
安全的方式:
add
add
方法,它只需要一个类型为Foo
的参数,并在方法中提取Foo::name
,并且不将其作为参数。但是,add
方法中仍然存在相同的问题。在第二种方法中,您应该能够通过首先创建一个新的映射条目(使用默认值)并获取对它的可变引用并在之后分配值来解决评估顺序问题:
auto& entry = _foo_map[f->name];
entry = std::move(f);
不确定您的地图实现是否支持获取对条目的可变引用,但对于许多应该可以工作。
如果我再想一想,你也可以选择“复制名称”的方法。无论如何都需要为地图键复制它。如果你手动复制它,你可以移动它以获得密钥,没有开销。
std::string name = f->name;
_foo_map[std::move(name)] = std::move(f);
修改强>
正如评论中所指出的那样,应该可以直接在_foo_map[f->name] = std::move(f)
函数中分配add
,因为这里保证了评估顺序。
答案 1 :(得分:1)
您可以add
通过引用获取f
,这可以避免不必要的复制/移动(同样的副本/移动会导致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));
}
foo_map[name]
必须在operator=
被调用之前进行评估,因此,即使name
引用取决于f
的内容,也没有问题。