2016-11-15 13 views
12

今日、生ポインタをstd::unique_ptrに変更するために一部のコードをリファクタリングしていたのに、order of evaluationのバグが原因でセグメンテーションフォルトが発生しました。C++ 17の式の評価順序とstd :: move

古いコードは、次のような何かをした:

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)が移動された変数boomを参照解除します。

これに対する可能な修正がaddへの呼び出しでそれを移動する前Foo::nameのハンドルを取得するために、次のとおりです。

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++上記2つの提案されたソリューションのどちらかである、とされていない場合、より良い代替手段がありますか?

  • P0145R3 - Refining Expression Evaluation Order for Idiomatic C++のためにC++ 17に変更があります。これにより、上記の解決策のいずれかが変わるか、落とし穴が防止されますか?

+1

私は、 'add'関数をリファクタリングするか、' std :: unique_ptr 'オブジェクトを取るオーバーロードを追加するという"慣用的 "な解決策を言います。 –

+0

@Someprogrammerdudeちょうど、 'process'から' add'に名前のハンドルを取得するという要件を変更するでしょうか?またはP0145R3はこの問題に対処していますか? –

+1

IMOできるだけスタックの遠くまでこれらの詳細をプッシュするのが一般的には良い考えです。結局のところ、抽象化はOOとC++の主要な部分の1つです。 –

答えて

6

私にとっては、これらの2つの提案は悪く見えます。どちらの場合でも、あなたはあなたのFooオブジェクトに移動を許可しています。それは、あなたがもはやその状態について何も仮定することができなくなることを意味します。最初の引数(オブジェクトへの参照または文字列への参照)が処理される前に、関数addの中で割り当てを解除することができます。はい、それは現在の実装では機能しますが、誰かがaddなどの実装に触れるとすぐに破損する可能性があります。

安全な方法:

  • 文字列のコピーを作成し、add
  • の最初の引数として、それが唯一のタイプFooの単一の引数を取り、内部Foo::nameを抽出し、あなたのaddメソッドをリファクタリングすることを渡しますこのメソッドを引数として取りません。しかし、あなたはまだaddメソッド内に同じ問題があります。第二のアプローチで

あなたが最初の(デフォルトの値を持つ)新しいマップエントリを作成し、それへの変更可能な参照を取得し、その後値を割り当てることによって、評価順序の問題を回避することができる必要があります:

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

マップの実装がエントリへの可変参照を取得することをサポートしているかどうかは不明ですが、多くの場合、動作する必要があります。

私はもう一度考えてみると、 "名前をコピーする"アプローチに行くかもしれません。とにかくマップキーのためにコピーする必要があります。手動でコピーすると、キーのために移動できます。オーバーヘッドはありません。

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

編集:コメントで指摘

としては、評価順序はここで保証されているので、直接add関数内_foo_map[f->name] = std::move(f)を割り当てることが可能であるべきです。

+0

名前*をコピーすることは本当に必要ですか? '[]'と '='は 'operator []'と 'operator ='の関数の文法的砂糖です。したがって、 'map [f-> name] = std :: move(foo)'は 'map.operator [](f-> name).operator =(std :: move(foo))'正しいでしょうか?そして、P0145R3では、これらのいくつかの関数呼び出しの評価順序が正しく定義されていますか? –

+0

私はその変換と標準について詳しくは考えていないので、100%確実ではありません。それがあなたの言うとおりであれば、手動停止は避けることができます。しかし、地図内で新しいエントリがキーと値のペアとして格納されるため、文字列は遅かれ早かれコピーされます。キーのタイプがstd :: stringの場合、キーは文字列のコピーです。 – Matthias247

+4

'f'が最初にr値参照に変換されても、移動しないので、' _foo_map [f-> name] = std :: move(f) 'を実行することはできます。 '_foo_map [f-> name]' – Altainia

1

あなたはaddが不要なコピー/移動(f->nameをゴミ同じコピー/移動)を回避する、参照することによりfを取る作ることができます:

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]が評価されなければなりませんだからnamefに応じて何かを指していても問題ありません。

+0

に項目を追加する慣用的な方法です'f'が消費されました。すでに 'add(f-> name、std :: move(f))'という行は悪いので、修正する必要があります。 – cmaster

+0

@cmaster 'f'は' add '内の 'operator ='への呼び出しまで消費されません。 'std :: move'は何も消費しません。そして、 'name'が読み込まれた後であることが保証されます。なぜなら、' operator = 'はオペランドが評価されるまで呼び出すことができないからです。 –

+0

問題は、 'add()'が2つのもつれた引数で呼び出されるということです.1つの変更(移動構築/割り当てによる消費)は、他方の状態の変更につながります(参照は無効になります)。これは決して良いことではなく、コードをメンテナンス可能にしたい場合は、すべてのコストをかけて回避する必要があります。 – cmaster