web-dev-qa-db-ja.com

これはC ++ 11 forループの既知の落とし穴ですか?

いくつかのメンバー関数で3つのdoubleを保持するための構造体があると想像してみましょう。

struct Vector {
  double x, y, z;
  // ...
  Vector &negate() {
    x = -x; y = -y; z = -z;
    return *this;
  }
  Vector &normalize() {
     double s = 1./sqrt(x*x+y*y+z*z);
     x *= s; y *= s; z *= s;
     return *this;
  }
  // ...
};

これは簡単にするために少し工夫されていますが、同様のコードがそこにあることに同意すると確信しています。これらのメソッドを使用すると、たとえば次のように便利にチェーンできます。

Vector v = ...;
v.normalize().negate();

あるいは:

Vector v = Vector{1., 2., 3.}.normalize().negate();

ここで、begin()関数とend()関数を提供した場合、Vectorを新しいスタイルのforループで使用できます。たとえば、3つの座標x、y、zをループします(間違いなく、より「便利な」例を作成できます)。ベクトルをたとえば文字列に置き換えることによって):

Vector v = ...;
for (double x : v) { ... }

私たちもできる:

Vector v = ...;
for (double x : v.normalize().negate()) { ... }

そしてまた:

for (double x : Vector{1., 2., 3.}) { ... }

ただし、次の(私には思えます)が壊れています:

for (double x : Vector{1., 2., 3.}.normalize()) { ... }

前の2つの使用法の論理的な組み合わせのように見えますが、この最後の使用法は、前の2つは完全に問題ないのに、ダングリング参照を作成すると思います。

  • これは正しく、広く感謝されていますか?
  • 上記のどの部分が「悪い」部分であり、避けるべきですか?
  • For-expressionで構築された一時的なものがループの期間中に存在するように、範囲ベースのforループの定義を変更することによって、言語が改善されますか?
89
ndkrempel

これは正しく、広く感謝されていますか?

はい、あなたの物事の理解は正しいです。

上記のどの部分が「悪い」部分であり、避けるべきですか?

悪い部分は、関数から返された一時的なものへのl値参照を取得し、それをr値参照にバインドすることです。これと同じくらい悪いです:

_auto &&t = Vector{1., 2., 3.}.normalize();
_

コンパイラはnormalizeからの戻り値がそれを参照していることを認識していないため、一時的な_Vector{1., 2., 3.}_の存続期間を延長することはできません。

For-expressionで構築された一時的なものがループの期間中に存在するように、範囲ベースのforループの定義を変更することによって、言語が改善されますか?

これは、C++の動作とは非常に矛盾します。

一時的なチェーン式や、式のさまざまな遅延評価方法を使用する人々によって行われる特定の落とし穴を防ぐことができますか?はい。ただし、特殊なケースのコンパイラコードが必要になるだけでなく、other式の構成で機能しない理由についても混乱します。

はるかに合理的な解決策は、関数の戻り値が常にthisへの参照であることをコンパイラーに通知する方法です。したがって、戻り値が一時的な拡張構造にバインドされている場合は、正しい一時を延長します。ただし、これは言語レベルのソリューションです。

現在(コンパイラがサポートしている場合)、一時的にnormalizecannotが呼び出されるようにすることができます。

_struct Vector {
  double x, y, z;
  // ...
  Vector &normalize() & {
     double s = 1./sqrt(x*x+y*y+z*z);
     x *= s; y *= s; z *= s;
     return *this;
  }
  Vector &normalize() && = delete;
};
_

これにより、Vector{1., 2., 3.}.normalize()でコンパイルエラーが発生しますが、v.normalize()は正常に機能します。明らかに、次のような正しいことを行うことはできません。

_Vector t = Vector{1., 2., 3.}.normalize();
_

しかし、あなたはまた、間違ったことをすることができなくなります。

または、コメントで提案されているように、右辺値参照バージョンが参照ではなく値を返すようにすることができます。

_struct Vector {
  double x, y, z;
  // ...
  Vector &normalize() & {
     double s = 1./sqrt(x*x+y*y+z*z);
     x *= s; y *= s; z *= s;
     return *this;
  }
  Vector normalize() && {
     Vector ret = *this;
     ret.normalize();
     return ret;
  }
};
_

Vectorが実際に移動するリソースを持つタイプである場合は、代わりにVector ret = std::move(*this);を使用できます。名前付きの戻り値の最適化により、パフォーマンスの観点からこれが合理的に最適化されます。

64
Nicol Bolas

for(double x:Vector {1.、2.、3。}。normalize()){...}

これは言語の制限ではありませんが、コードの問題です。表現 Vector{1., 2., 3.}は一時的なものを作成しますが、normalize関数はlvalue-referenceを返します。式はlvalueであるため、コンパイラはオブジェクトが存続していると想定しますが、これは一時的なものへの参照であるため、完全な式が評価された後にオブジェクトが終了するため、ぶら下がりリファレンス。

これで、現在のオブジェクトへの参照ではなく値で新しいオブジェクトを返すようにデザインを変更した場合、問題は発生せず、コードは期待どおりに機能します。

私見、2番目の例はすでに欠陥があります。修飾演算子が*thisを返すことは、あなたが述べたように便利です:それは修飾語の連鎖を可能にします。 can変更の結果を単に渡すために使用されますが、これを行うと、見落とされがちなため、エラーが発生しやすくなります。私が次のようなものを見たら

Vector v{1., 2., 3.};
auto foo = somefunction1(v, 17);
auto bar = somefunction2(true, v, 2, foo);
auto baz = somefunction3(bar.quun(v), 93.2, v.qwarv(foo));

関数が副作用としてvを変更することを自動的に疑うことはありません。もちろん、それらはcouldですが、混乱を招きます。したがって、このようなものを作成する場合は、vが一定に保たれるようにします。あなたの例では、無料の関数を追加します

auto normalized(Vector v) -> Vector {return v.normalize();}
auto negated(Vector v) -> Vector {return v.negate();}

その後、ループを記述します

for( double x : negated(normalized(v)) ) { ... }

そして

for( double x : normalized(Vector{1., 2., 3}) ) { ... }

これはIMOの読みやすさであり、安全です。もちろん、追加のコピーが必要ですが、ヒープに割り当てられたデータの場合、これは安価なC++ 11移動操作で実行できる可能性があります。

4
leftaroundabout