2

私の実際の問題はもっと複雑で、ここでそれを再現するための短い具体的な例を示すことは非常に難しいようです. したがって、関連する可能性のある別の小さな例をここに投稿しています。その議論は、実際の問題にも役立つ場合があります。

// A: works fine (prints '2')
cout << std::get <0>(std::get <1>(
    std::forward_as_tuple(3, std::forward_as_tuple(2, 0)))
) << endl;

// B: fine in Clang, segmentation fault in GCC with -Os
auto x = std::forward_as_tuple(3, std::forward_as_tuple(2, 0));
cout << std::get <0>(std::get <1>(x)) << endl;

実際の問題には が含まれていないstd::tupleため、例を独立させるために、カスタムの最小限の大まかな同等物を次に示します。

template <typename A, typename B>
struct node { A a; B b; };

template <typename... A>
node <A&&...> make(A&&... a)
{
    return node <A&&...>{std::forward <A>(a)...};
}

template <typename N>
auto fst(N&& n)
-> decltype((std::forward <N>(n).a))
    { return std::forward <N>(n).a; }

template <typename N>
auto snd(N&& n)
-> decltype((std::forward <N>(n).b))
    { return std::forward <N>(n).b; }

これらの定義を考えると、まったく同じ動作になります。

// A: works fine (prints '2')
cout << fst(snd(make(3, make(2, 0)))) << endl;

// B: fine in Clang, segmentation fault in GCC with -Os
auto z = make(3, make(2, 0));
cout << fst(snd(z)) << endl;

一般に、動作はコンパイラと最適化レベルに依存するようです。デバッグしても何もわかりませんでした。すべての場合において、すべてがインライン化および最適化されているように見えるため、問題の原因となっている特定のコード行を特定できません。

それらへの参照がある限り一時変数が存続することになっている場合 (関数本体内からローカル変数への参照を返さない場合)、上記のコードが問題を引き起こす可能性がある根本的な理由と、ケース A とB は異なるはずです。

私の実際の問題では、Clang と GCC の両方で、最適化レベルに関係なく、ワンライナー バージョン (ケース A) でもセグメンテーション エラーが発生するため、問題は非常に深刻です。

代わりに値または右辺値参照 (たとえばstd::make_tuple、またはnode <A...>カスタム バージョン) を使用すると、問題はなくなります。タプルがネストされていない場合も消えます。

しかし、上記のどれも役に立ちません。私が実装しているのは、ビュー用の一種の式テンプレートと、タプル、シーケンス、および組み合わせを含む多数の構造に対する遅延評価です。したがって、一時変数への右辺値参照が絶対に必要です。入れ子になったタプル、たとえば(a, (b, c))、入れ子になった操作を含む式、たとえば、すべてが正常に機能しますu + 2 * vが、両方ではありません。

上記のコードが有効かどうか、セグメンテーション違反が予想されるかどうか、それを回避する方法、コンパイラと最適化レベルで何が起こっているのかを理解するのに役立つコメントをいただければ幸いです。

4

1 に答える 1

1

ここで問題となるのは、「一時変数は、それらへの参照がある限り存続するはずである場合」という文です。これは限られた状況でのみ当てはまります。あなたのプログラムは、それらのケースの 1 つのデモンストレーションではありません。完全な式の最後で破棄される一時変数への参照を含むタプルを格納しています。このプログラムはそれを非常に明確に示しています ( Coliru のライブ コード):

struct foo {
    int value;
    foo(int v) : value(v) {
        std::cout << "foo(" << value << ")\n" << std::flush;
    }
    ~foo() {
        std::cout << "~foo(" << value << ")\n" << std::flush;
    }
    foo(const foo&) = delete;
    foo& operator = (const foo&) = delete;
    friend std::ostream& operator << (std::ostream& os,
                                      const foo& f) {
        os << f.value;
        return os;
    }
};

template <typename A, typename B>
struct node { A a; B b; };

template <typename... A>
node <A&&...> make(A&&... a)
{
    return node <A&&...>{std::forward <A>(a)...};
}

template <typename N>
auto fst(N&& n)
-> decltype((std::forward <N>(n).a))
    { return std::forward <N>(n).a; }

template <typename N>
auto snd(N&& n)
-> decltype((std::forward <N>(n).b))
    { return std::forward <N>(n).b; }

int main() {
    using namespace std;
    // A: works fine (prints '2')
    cout << fst(snd(make(foo(3), make(foo(2), foo(0))))) << endl;

    // B: fine in Clang, segmentation fault in GCC with -Os
    auto z = make(foo(3), make(foo(2), foo(0)));
    cout << "referencing: " << flush;
    cout << fst(snd(z)) << endl;
}

Aタプルに保存されている参照に同じ完全な式でBアクセスし、タプルを保存して後で参照にアクセスするため、動作が未定義であるため、正常に動作します。clangでコンパイルしてもクラッシュしない可能性がありますが、有効期間が終了した後にオブジェクトにアクセスするため、明らかに未定義の動作であることに注意してください。

この使用法を安全にしたい場合は、左辺値への参照を格納するようにプログラムを簡単に変更できますが、右辺値はタプル自体に移動します ( Coliru のライブ デモ)。

template <typename... A>
node<A...> make(A&&... a)
{
    return node<A...>{std::forward <A>(a)...};
}

で置き換えるnode<A&&...>node<A...>がコツです。Aはユニバーサル参照であるため、 の実際の型はA左辺値引数の左辺値参照であり、右辺値引数の非参照型になります。参照の折りたたみルールは、この使用法と完全な転送に有利に機能します。

編集:このシナリオの一時オブジェクトの有効期間が参照の有効期間に延長されない理由については、C++ 11 12.2 一時オブジェクト [class.temporary] パラグラフ 4 を確認する必要があります。

完全式の終わりとは異なる時点で一時変数が破棄される状況が 2 つあります。最初のコンテキストは、配列の要素を初期化するために既定のコンストラクターが呼び出されるときです。コンストラクターに 1 つ以上の既定の引数がある場合、既定の引数で作成されたすべての一時的な要素の破棄は、次の配列要素 (存在する場合) の構築の前に順序付けられます。

そして、より複雑なパラグラフ 5:

2 番目のコンテキストは、参照がテンポラリにバインドされる場合です。参照がバインドされている一時オブジェクト、または参照がバインドされているサブオブジェクトの完全なオブジェクトである一時オブジェクトは、次の例外を除き、参照の存続期間中持続します。

  • コンストラクターの ctor-initializer (12.6.2) の参照メンバーへの一時的なバインドは、コンストラクターが終了するまで持続します。

  • 関数呼び出し (5.2.2) の参照パラメーターへの一時的なバインドは、呼び出しを含む完全な式が完了するまで持続します。

  • 関数 return ステートメント (6.6.3) の戻り値に一時的にバインドされているものの有効期間は延長されません。一時的なものは、return ステートメントの完全な式の最後で破棄されます。

  • new-initializer (5.3.4)の参照への一時的なバインドは、 new-initializer を含む完全な式が完了するまで持続します。[ 例:

struct S { int mi; const std::pair<int,int>& mp; };
S a { 1, {2,3} };
S* p = new S{ 1, {2,3} }; // Creates dangling reference

--end example ] [ 注: これによりダングリング参照が導入される可能性があり、実装はそのような場合に警告を発行することをお勧めします。—終わりのメモ]

参照にバインドされても有効期間が延長されないテンポラリーの破棄は、同じ完全式で以前に構築されたすべてのテンポラリーの破棄の前に順序付けられます。参照がバインドされている 2 つ以上の一時オブジェクトの有効期間が同じ時点で終了する場合、これらの一時オブジェクトはその時点で、構築の完了とは逆の順序で破棄されます。さらに、参照にバインドされた一時オブジェクトの破棄では、静的、スレッド、または自動ストレージ期間 (3.7.1、3.7.2、3.7.3) を持つオブジェクトの破棄の順序を考慮する必要があります。つまり、 がobj1一時オブジェクトと同じ保存期間を持ち、一時オブジェクトが作成される前に作成されたオブジェクトである場合、一時オブジェクトは が破棄される前obj1に破棄されます。もしもobj2一時オブジェクトと同じ保存期間を持つオブジェクトであり、一時オブジェクトが作成された後に作成され、一時オブジェクトは破棄後obj2に破棄されます。[ 例:

struct S {
  S();
  S(int);
  friend S operator+(const S&, const S&);
  ~S();
};
S obj1;
const S& cr = S(16)+S(23);
S obj2;

この式S(16) + S(23)は 3 つのテンポラリを作成します。最初のテンポラリT1は式 の結果を保持しS(16)、2 番目のテンポラリT2は式 S(23) の結果を保持し、3 番目のテンポラリT3はこれら 2 つの式の加算結果を保持します。次に、テンポラリT3が参照にバインドされますcr。またはが最初に作成されるかどうT1かは指定されていません。が の前に作成されるT2実装では、 が の前に破棄されることが保証されます。一時変数とは;の参照パラメータにバインドされています。これらの一時変数は、への呼び出しを含む完全な式の最後で破棄されます。一時的なT1T2T2T1T1T2operator+operator+T3参照にバインドされているは、 の存続期間の終わり、つまりプログラムの終了時にcr破棄されます。crさらに、T3が破棄される順序は、静的ストレージ期間を持つ他のオブジェクトの破棄順序を考慮に入れます。つまり、 はより前に構築され、 はよりobj1前に構築されるため、は T3 より前に破棄され、 は より前に破棄されることが保証されます。—終わりの例]T3T3obj2obj2T3obj1

「コンストラクターのctor-initializerの参照メンバーに」一時的にバインドしています。

于 2014-01-22T05:26:16.030 に答える