一時的なものへの言及をベースに、この範囲を誰が責めるのでしょうか?


15

次のコードは一見無害に見えます。ユーザーはこの関数bar()を使用して、いくつかのライブラリ機能と対話します。(これはbar()、一時的でない値または同様のものへの参照を返したので、長い間働いていたかもしれません。)しかし、今では単にの新しいインスタンスを返していますBB繰り返しa()処理可能な型のオブジェクトへの参照を返す関数がありますABによって返される一時オブジェクトはbar()、反復が開始される前に破棄されるため、ユーザーはこのオブジェクトを照会すると、セグメンテーション違反につながります。

誰(ライブラリーまたはユーザー)がこれを責めるかは私は優柔不断です。すべてのライブラリが提供するクラスは私にはきれいに見えますが、確かに他の多くのコードとは何も違いはありません(メンバーへの参照を返す、スタックインスタンスを返すなど)。ユーザーは何も悪いことをしていないように見えます。彼は、オブジェクトの存続期間に関して何もせずにオブジェクトを繰り返し処理しているだけです。

(関連する質問は次のとおりです:ループヘッダー内の複数のチェーンされた呼び出しによって取得されるものに対してコードが「範囲ベースの反復」ではないという一般的なルールを確立する必要があります。右辺値?)

#include <algorithm>
#include <iostream>

// "Library code"
struct A
{
    A():
        v{0,1,2}
    {
        std::cout << "A()" << std::endl;
    }

    ~A()
    {
        std::cout << "~A()" << std::endl;
    }

    int * begin()
    {
        return &v[0];
    }

    int * end()
    {
        return &v[3];
    }

    int v[3];
};

struct B
{
    A m_a;

    A & a()
    {
        return m_a;
    }
};

B bar()
{
    return B();
}

// User code
int main()
{
    for( auto i : bar().a() )
    {
        std::cout << i << std::endl;
    }
}

6
誰を責めるかを考えたとき、次のステップは何でしょうか?彼/彼女に叫ぶ?
JensG 14年

7
いいえ、なぜですか?実際、この「プログラム」を開発する思考プロセスが将来この問題を回避できなかった場所を知りたいと思っています。
hllnll 14年

これは右辺値や範囲ベースのforループとは関係ありませんが、ユーザーはオブジェクトの有効期間を正しく理解していません。
ジェームズ14年

サイトリマーク:これは、欠陥ではないため閉鎖されたCWG 900です。議事録には議論が含まれているかもしれません。
dyp 14年

7
これは誰のせいですか?何よりもまず、Bjarne StroustrupとDennis Ritchieです。
メイソンウィーラー14年

回答:


14

基本的な問題は、C ++の言語機能の組み合わせ(またはその欠如)であると思います。ライブラリコードとクライアントコードの両方が合理的です(問題が明らかではないという事実によって証明されるように)。一時的Bなものの寿命が(ループの最後まで)適切に延長されていれば、問題はありません。

一時的な生活を十分に長くすることは、もはや困難です。むしろアドホックな「ループの終わりまでの範囲ベースのライブの範囲の作成に関わるすべての一時的」でさえ、副作用はありません。値によってオブジェクトにB::a()依存しない範囲を返す場合を考えてくださいB。その後、一時Bファイルはすぐに破棄できます。ライフタイムの延長が必要なケースを正確に特定できたとしても、これらのケースはプログラマーには明らかではないため、その効果(デストラクタはずっと後に呼ばれる)は驚くべきものであり、おそらく同様に微妙なバグのソースです。

そのようなナンセンスを単に検出して禁止し、プログラマにbar()ローカル変数に明示的に昇格させることを強制する方が望ましいでしょう。これはC ++ 11では不可能であり、注釈が必要なため、おそらく不可能になるでしょう。Rustはこれを行います。署名は.a()次のとおりです。

fn a<'x>(bar: &'x B) -> &'x A { bar.a }
// If we make it as explicit as possible, or
fn a(&self) -> &A { self.a }
// if we make it a method and rely on lifetime elision.

ここでは'x、リソースが利用可能である時間の期間のシンボル名で生涯変数または領域が、あります。率直に言って、生涯を説明するのは難しいです-またはまだ最良の説明を理解していません-したがって、私はこの例に必要な最小限に制限し、傾斜した読者に公式文書を参照します

ボローチェッカーはbar().a()、ループが実行されている限り、結果が生きる必要があることに気付くでしょう。ライフタイムの制約として、次のように'x記述します'loop <= 'x。また、メソッド呼び出しの受信者がbar()一時的なものであることに気付くでしょう。2つのポインターは同じ存続期間に関連付けられているため'x <= 'temp、別の制約です。

これら2つの制約は矛盾しています!'loop <= 'x <= 'tempbutが必要です'temp <= 'loop。これは問題をかなり正確にキャプチャします。要件が競合するため、バグのあるコードは拒否されます。これはコンパイル時のチェックであり、Rustコードは通常、同等のC ++コードと同じマシンコードになるため、ランタイム費用を支払う必要がないことに注意してください。

それでも、これは言語に追加する大きな機能であり、すべてのコードで使用されている場合にのみ機能します。APIの設計にも影響があります(C ++では危険すぎる設計も実用的になりますが、他の設計は存続期間中はうまく動作しません)。悲しいかな、それはC ++(または実際に任意の言語)にさかのぼって追加することは実用的ではないことを意味します。要約すると、欠点は成功した言語が持つ慣性と、1983年のBjarneが過去30年間の研究とC ++の経験の教訓を取り入れる水晶玉と先見性を持っていなかったという事実にあります;-)

もちろん、これは将来の問題を回避する上でまったく役に立ちません(Rustに切り替えてC ++を二度と使用しない限り)。複数のチェーンされたメソッド呼び出しで長い式を避けることができます(かなり制限されており、生涯のトラブルをリモートで修正することすらありません)。または、コンパイラーの支援なしで、より規律のある所有ポリシーを採用することもできbarます。値によって返されることと、その結果が呼び出されたものB::a()より長くなってはならないことを明確に文書化します。寿命の長い参照の代わりに値で返すように関数を変更する場合、これが契約の変更であることを意識してください。それでもエラーが発生しやすくなりますが、発生した場合に原因を特定するプロセスを高速化できます。Ba()


14

C ++機能を使用してこの問題を解決できますか?

C ++ 11には、メンバー関数のref修飾子が追加されています。これにより、メンバー関数を呼び出すことができるクラスインスタンス(式)の値カテゴリを制限できます。例えば:

struct foo {
    void bar() & {} // lvalue-ref-qualified
};

foo& lvalue ();
foo  prvalue();

lvalue ().bar(); // OK
prvalue().bar(); // error

beginメンバー関数を呼び出すとき、ほとんどの場合、endメンバー関数(またはsize範囲のサイズを取得するにはのようなもの)も呼び出す必要があることを知っています。これには、2回対処する必要があるため、左辺値を操作する必要があります。したがって、これらのメンバー関数は左辺値参照修飾する必要があると主張できます。

ただし、これは根本的な問題であるエイリアシングを解決しない可能性があります。メンバ関数エイリアスオブジェクト、またはオブジェクトが管理するリソース。and を単一の関数で置き換える場合、右辺値で呼び出すことができる関数を提供する必要があります。beginendbeginendrange

struct foo {
    vector<int> arr;

    auto range() & // C++14 return type deduction for brevity
    { return std::make_pair(arr.begin(), arr.end()); }
};

for(auto const& e : foo().range()) // error

これは有効なユースケースかもしれませんが、上記の定義ではrange許可されていません。メンバー関数呼び出しの後で一時アドレスに対処できないため、コンテナ、つまり所有範囲を返す方が合理的かもしれません。

struct foo {
    vector<int> arr;

    auto range() &
    { return std::make_pair(arr.begin(), arr.end()); }

    auto range() &&
    { return std::move(arr); }
};

for(auto const& e : foo().range()) // OK

これをOPのケースに適用し、わずかなコードレビュー

struct B {
    A m_a;
    A & a() { return m_a; }
};

このメンバー関数は、式の値カテゴリを変更しますB()。prvalue B().a()ですが、左辺値です。一方、B().m_a右辺値です。それで、これを一貫させることから始めましょう。これを行うには2つの方法があります。

struct B {
    A m_a;
    A &  a() &  { return m_a; }

    A && a() && { return std::move(m_a); }
    // or
    A    a() && { return std::move(m_a); }
};

上記の2番目のバージョンは、OPの問題を修正します。

さらに、Bのメンバー関数を制限できます。

struct A {
    // [...]

    int * begin() & { return &v[0]; }
    int * end  () & { return &v[3]; }

    int v[3];
};

:範囲ベースのforループの後の式の結果は参照変数にバインドされるため、これはOPのコードに影響を与えません。そして、この変数は(その関数beginおよびendメンバー関数にアクセスするために使用される式として)左辺値です。

もちろん、問題は、デフォルトのルールが「右辺値のメンバー関数のエイリアスは、すべてのリソースを所有するオブジェクトを返すべきであるかどうか、正当な理由がない限り」であるかどうかです。返されるエイリアスは合法的に使用できますが、あなたが経験している方法では危険です。一時的な「親」の寿命を延ばすために使用することはできません。

// using the OP's definition of `struct B`,
// or version 1, `A && a() &&;`

A&&      a = B().a(); // bug: binds directly, dangling reference
A const& a = B().a(); // bug: same as above
A        a = B().a(); // OK

A&&      a = B().m_a; // OK: extends the lifetime of the temporary

C ++ 2aでは、この(または同様の)問題を次のように回避することになっていると思います。

for( B b = bar(); auto i : b.a() )

OPの代わりに

for( auto i : bar().a() )

回避策bは、forループのブロック全体がライフタイムであることを手動で指定します。

この初期ステートメントを導入した提案

ライブデモ


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.