1. ホーム
  2. c++

[解決済み] C++11のforループの落とし穴はこれか?

2023-04-30 11:20:02

質問

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()関数があれば、新しいスタイルのforループでVectorを使い、例えば3つの座標x、y、zをループすることができます(Vectorを例えばStringに置き換えれば、よりquot; useful"な例を構築できることは間違いないでしょう)。

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式で構築されたテンポラリーがループの間存在するようにすれば、言語は改善されますか。

どのように解決するのですか?

<ブロッククオート

これは正しく、広く評価されているのでしょうか?

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

<ブロッククオート

上記のうち、避けるべき"bad"の部分はどこでしょうか?

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

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();

しかし、正しくないこともできなくなります。

あるいは、コメントで提案されているように、rvalue参照版が参照ではなく、値を返すようにすることもできます。

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); を使うことができます。名前付き戻り値の最適化により、これはパフォーマンスの点で合理的に最適化されています。