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


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()関数を提供した場合、ベクターを新しいスタイルのforループで使用して、3つの座標x、y、zをループすることができます(間違いなく、より「便利な」例を作成できます。 Vectorを文字列などで置き換える):

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ループの定義を変更することにより、言語は改善されますか?

何らかの理由で、以前に尋ねられた非常によく似た質問を思い出しましたが、それが何と呼ばれていたのか忘れました。
Pubby

これは言語の欠陥だと思います。テンポラリの寿命はforループの本体全体に拡張されるのではなく、forループのセットアップにのみ拡張されます。影響を受けるのは範囲構文だけではなく、古典的な構文でも同様です。私の意見では、initステートメントの一時的な寿命は、ループの寿命全体に及ぶはずです。
edA-qa mort-ora-y

1
@ edA-qamort-ora-y:私はここに潜んでいるわずかな言語の欠陥があることに同意する傾向がありますが、それは特に、一時を参照に直接バインドするときはいつでもライフタイム延長が暗黙的に発生するという事実であると思いますが、他の状況-これは一時的な寿命の根本的な問題に対する中途半端な解決策のように見えますが、それはより良い解決策が何であるかが明らかであると言うことではありません。おそらく、テンポラリーを構築するときの明示的な「ライフタイムエクステンション」構文は、現在のブロックの終わりまでそれを持続させます-どう思いますか?
ndkrempel

@ edA-qamort-ora-y:...これは、一時を参照にバインドするのと同じことですが、インラインで(式内で)「ライフタイムエクステンション」が発生していることを読者に明示するという利点があります。 、別の宣言を要求するのではなく)、一時ファイルに名前を付ける必要はありません。
ndkrempel

回答:


64

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

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

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

悪い点は、関数から返された一時変数への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();

しかし、あなたはまた不正確なことをすることができなくなります。

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

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


1
これをより「おかしな」ものにする可能性があるのは、新しいforループが参照バインディングがカバーの下で行われているという事実を構文的に隠していることです。つまり、上記の「ちょうど悪い」例よりも露骨ではありません。そのため、新しいforループのためだけに、追加のライフタイム拡張ルールを提案するのはもっともらしいようです。
ndkrempel

1
@ndkrempel:はい。ただし、これを修正するために言語機能を提案する場合(したがって、少なくとも2017年まで待たなければならない場合)、それがより包括的で、一時的な拡張機能の問題をどこでも解決できるものであるとよいと思います
Nicol Bolas、2012年

3
+1。最後のアプローチでは、というよりdelete:あなたは戻っ右辺値があること代替動作を提供することができますVector normalize() && { normalize(); return std::move(*this); }(私はへの呼び出しと信じているnormalize関数内は左辺値過負荷に派遣する、誰かがそれを確認してください:)
デビッド・ロドリゲス- dribeas

3
これ&/ &&メソッドの資格を見たことがありません。これはC ++ 11からのものですか、これは(おそらく広く普及している)独自のコンパイラ拡張機能ですか?興味深い可能性を与えます。
Christian Rau、

1
@ChristianRau:これはC ++ 11の新機能であり、ある意味で「this」を修飾しているという点で、C ++ 03の非静的メンバー関数の「const」および「volatile」修飾に類似しています。ただし、g ++ 4.7.0ではサポートされていません。
ndkrempel

25

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

これは言語の制限ではなく、コードの問題です。式Vector{1., 2., 3.}は一時変数を作成しnormalizeますが、関数は左辺値参照を返します。式は左辺値であるため、コンパイラーはオブジェクトが生きていると想定しますが、それは一時オブジェクトへの参照であるため、完全な式が評価された後にオブジェクトが停止するため、ぶら下がり参照が残ります。

現在、現在のオブジェクトへの参照ではなく、値によって新しいオブジェクトを返すように設計を変更した場合、問題は発生せず、コードは期待どおりに動作します。


1
constこの場合、参照はオブジェクトの寿命を延ばしますか?
David Stone

5
これはnormalize()、既存のオブジェクトの変更関数としての明らかに望ましいセマンティクスを壊します。したがって問題。一時的なものは反復の特定の目的に使用された場合に「延長された寿命」を持ち、それ以外の場合はそうではないことは、紛らわしい誤解だと思います。
アンディロス

2
@AndyRoss:なぜ?任意の R値の基準(または一時的に結合しているがconst&)、その寿命が延長しました。
Nicol Bolas、2012年

2
@ndkrempel:それでも、範囲ベースのforループの制限ではなく、参照にバインドすると同じ問題が発生します:Vector & r = Vector{1.,2.,3.}.normalize();。あなたのデザインにはその制限があり、それはあなたが値で返すことをいとわないことです(多くの状況ではそれは理にかなっているかもしれません、そしてそれは右辺値参照moveでもっとそうです)、さもなければあなたは問題の場所で対処する必要があります呼び出し:適切な変数を作成し、それをforループで使用します。また、式があることに注意してくださいVector v = Vector{1., 2., 3.}.normalize().negate();作成2つのオブジェクト...
dribeas -デビッド・ロドリゲスの

1
@DavidRodríguez-dribeas:const-referenceへのバインドの問題はこれです:T const& f(T const&);完全に問題ありません。T const& t = f(T());完全に元気です。そして、別のTUでそれを発見しT const& f(T const& t) { return t; }て泣きます... operator+値を操作する場合は、より安全です。その後、コンパイラはコピーを最適化する場合があります(速度が必要ですか?値で渡す)が、それはおまけです。私が許可する一時的なバインディングはr値参照へのバインディングのみですが、関数は安全のために値を返し、エリシオンのコピー/セマンティクスの移動に依存する必要があります。
Matthieu M.

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移動操作で行うことができます。


ありがとう。いつものように、多くの選択肢があります。たとえば、Vectorが1000個のdoubleの配列(ヒープが割り当てられていない)である場合、提案が実行できない可能性があります。効率、使いやすさ、使いやすさのトレードオフ。
ndkrempel

2
はい、しかし、とにかく、スタック上にサイズが>≈100の構造体があることはめったにありません。
leftaroundabout
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.