89

いくつかのメンバー関数で 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 ループで使用できます。たとえば、x、y、z の 3 つの座標をループすることができます (間違いなく、より多くの「有用な」例を作成できます)。 Vector を String などに置き換えることによって):

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 式で構築された一時変数がループ中に存在するように、範囲ベースの for ループの定義を変更することで、言語は改善されますか?
4

3 に答える 3

64

これは正しく、広く評価されていますか?

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

上記のうち、避けるべき「悪い」部分はどれですか?

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

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

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

for 式で構築された一時変数がループ中に存在するように、範囲ベースの for ループの定義を変更することによって、言語は改善されますか?

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

一時的に連鎖した式を使用したり、式のさまざまな遅延評価方法を使用したりする人々による特定の落とし穴を防ぐことができますか? はい。しかし、特殊なケースのコンパイラ コードが必要になるだけでなく、他の式構造では機能しない理由がわかりにくくなります。

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

現在(コンパイラがサポートしている場合)、一時的に呼び出す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() && = 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);。名前付きの戻り値の最適化により、これはパフォーマンスの観点から合理的に最適化されます。

于 2012-05-15T03:50:36.310 に答える
25

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

これは言語の制限ではなく、コードの問題です。式Vector{1., 2., 3.}は一時を作成しますが、normalize関数はlvalue-reference を返します。式はlvalueであるため、コンパイラはオブジェクトが有効であると想定しますが、これは一時オブジェクトへの参照であるため、完全な式が評価された後にオブジェクトが停止するため、ダングリング参照が残ります。

ここで、現在のオブジェクトへの参照ではなく値で新しいオブジェクトを返すように設計を変更しても、問題はなく、コードは期待どおりに機能します。

于 2012-05-15T03:24:48.373 に答える
4

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

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関数が副作用として変更されることを自動的に疑うことはありません。もちろん、可能ですが、混乱を招く可能性があります。ですから、このようなことを書くとしたら、それ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移動操作で実行できる可能性があります。

于 2012-05-15T13:34:09.237 に答える