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)调用那么可组合
问题:
上面提出的两个解决方案中的任何一个都是惯用的 C++,如果不是,是否有更好的选择?
我看到由于 P0145R3 - 细化表达式评估顺序,C++17 中出现了一些变化
地道的 C++。这会改变上述任何解决方案/防止它们的陷阱吗?
对我来说,这 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
的内容也没有问题。
今天重构一些代码以将原始指针更改为 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
.
问题:
上面提出的两个解决方案中的任何一个都是惯用的 C++,如果不是,是否有更好的选择?
我看到由于 P0145R3 - 细化表达式评估顺序,C++17 中出现了一些变化 地道的 C++。这会改变上述任何解决方案/防止它们的陷阱吗?
对我来说,这 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
的内容也没有问题。