3

const「ネストされた」方法で一時オブジェクトを参照にバインドする次のコードを検討してください。

#include <iostream>

std::string foo()
{
    return "abc";
}

std::string goo()
{
    const std::string & a = foo();
    return a;
}

int main()
{
    // Is a temporary allocated on the heap to support this, even for a moment?
    const std::string & b = goo();
}

この「ネストされた」構造をサポートするために、コンパイラがメモリストレージに関して何をしなければならないかを理解しようとしています。

への呼び出しのfoo()場合、メモリの割り当ては簡単だと思います。関数が終了すると、 a のストレージstd::stringがスタックに割り当てられます。foo()

しかし、によって参照されるオブジェクトのストレージをサポートするために、コンパイラは何をしなければならないのbでしょうか? 関数のスタックgooは巻き戻し、参照先のスタック上のオブジェクトに「置換」する必要がありますが、bのスタックを巻き戻すためにgoo、コンパイラーはヒープ上にオブジェクトのコピーを一時的に作成する必要があります (コピーする前に)。別の場所のスタックに戻します)?

それとも、一瞬でもヒープにストレージを割り当てずに、コンパイラがこの構造の要件を達成することは可能ですか?

または、スタックまたはヒープで追加の割り当てを行わずに、コンパイラが によって参照されるオブジェクトと同じストレージの場所を使用することさえ可能ですか?ba

4

5 に答える 5

7

あなたが考慮に入れなかった中間ステップがあると思います。それは、 にバインドbするaのではなく、 のコピーにバインドするということですa。そして、これは派手な記憶の悪ふざけによるものではありません!

goo値によって返されるため、その値は、すべての通常のメカニズムに従って、内部の完全な式のスコープ内で使用できます。のスタック フレームか、別の場所にあるか、または (この不自然なケースでは) 完全に最適化されている可能性がありますmainmain

ここでの唯一の魔法は、 が ref-to-であるため(ほぼ即時に破棄されるのではなく) 、スコープから外れるまで のスコープ内に保持されることです。mainbbconst

それで、ヒープは何らかの方法でそれに入るでしょうか?まあ、もしあなたがヒープを持っているなら、いいえ。あなたが無料の店を意味するなら、それでも、いいえ。

于 2012-12-05T19:18:08.520 に答える
4

理論的には、goo(さらに言えfooば)値によって返されるため、によって参照される変数のコピーが返されますa(そしてスタックに置かれます)。このコピーの有効期間は、のスコープが終了するbまで、によって延長されます。b

あなたが見逃している主な点は、 valueで返すことだと思います。つまり、afterfooまたはgooreturn の場合、それらの内部にあるものには何の違いもありませんconst。参照にバインドする一時的な文字列が残ります。

実際には、すべてが最適化される可能性が高くなります。

于 2012-12-05T19:15:58.100 に答える
4

以下は、C++ 標準でコンパイラがコードを再構築できるものの例です。フルNRVOを使用しています。newややあいまいな C++ 機能であるplacement の使用に注意してください。ポインターを渡すnewと、フリー ストアではなく、そこに結果が構築されます。

#include <iostream>

void __foo(void* __construct_std_string_at)
{
  new(__construct_std_string_at)std::string("abc");
}

void __goo(void* __construct_std_string_at)
{
  __foo(__construct_std_string_at);
}

int main()
{
  unsigned char __buff[sizeof(std::string)];
  // Is a temporary allocated on the heap to support this, even for a moment?
  __goo(&__buff[0]);
  const std::string & b = *reinterpret_cast<std::string*>(&__buff[0]);
  // ... more code here using b I assume
  // end of scope destructor:
  reinterpret_cast<std::string*>(&__buff[0])->~std::string();
}

で NRVO をブロックすると、次gooのようになります。

#include <iostream>

void __foo(void* __construct_std_string_at)
{
  new(__construct_std_string_at)std::string("abc");
}

void __goo(void* __construct_std_string_at)
{
  unsigned char __buff[sizeof(std::string)];
  __foo(&__buff[0]);
  std::string & a = *reinterpret_cast<std::string*>(&__buff[0]);
  new(__construct_std_string_at)std::string(a);
  // end of scope destructor:
  reinterpret_cast<std::string*>(&__buff[0])->~std::string();
}

int main()
{
  unsigned char __buff[sizeof(std::string)];
  // Is a temporary allocated on the heap to support this, even for a moment?
  __goo(&__buff[0]);
  const std::string & b = *reinterpret_cast<std::string*>(&__buff[0]);
  // ... more code here using b I assume
  // end of scope destructor:
  reinterpret_cast<std::string*>(&__buff[0])->~std::string();
}

基本的に、コンパイラは参照の有効期間を認識しています。したがって、変数の実際のインスタンスを格納する「匿名変数」を作成し、それへの参照を作成できます。

また、関数を呼び出すときに、戻り値が格納されるバッファーへのポインターを効果的に (暗黙的に) 渡すことにも注意しました。したがって、呼び出された関数は、呼び出し元のスコープ内で「その場で」オブジェクトを構築します。

NRVO を使用すると、呼び出された関数スコープ内の名前付き変数は、実際には呼び出し関数内で「戻り値が移動する場所」に構築されるため、簡単に戻ることができます。それがなければ、すべてをローカルで行う必要があります。その後、return ステートメントで、placement new と同等のものを使用して、戻り値を戻り値への暗黙のポインターにコピーします。

ライフタイムはすべて簡単に証明可能であり、スタック順に並べ替えられるため、ヒープ (別名フリー ストア) で何もする必要はありません。

元の署名foogoo予想される署名は、外部リンクがあるため、誰も使用していないことが判明したときに破棄されるまで、まだ存在している必要があります。

で始まるすべての変数と関数__は、説明のためだけに存在します。コンパイラ/実行環境には、赤血球に名前を付ける必要があるのと同様に、名前付き変数を含める必要はありません。(理論的には、__は予約済みであるため、コンパイル前にそのような変換パスを実行したコンパイラはおそらく合法であり、実際にそれらの変数名を使用してコンパイルに失敗した場合、それはコンパイラのせいではなくあなたのせいになりますが...それはかなりハッキーなコンパイラです。;) )

于 2012-12-05T19:30:21.923 に答える
3

いいえ、有効期間延長のための動的割り当てはありません。一般的な実装は、次のコード変換と同等です。

std::string goo()
{
    std::string __compiler_generated_tmp = foo();
    const std::string & a = __compiler_generated_tmp;
    return a;
}

有効期間は、参照が生きている間のみ延長され、現在のスコープの最後に発生する C++ 有効期間規則によって延長されるため、動的割り当ては必要ありません。名前のない (__compiler_generated_tmp上記のコードの) 変数をスコープに配置することで、通常の有効期間ルールが適用され、期待どおりに動作します。

于 2012-12-05T19:19:32.437 に答える
1

ではstd::string goo()、std::string は値によって返されます。

コンパイラは、main() でこの関数を呼び出していることを確認すると、戻り値が std::string であることを認識し、メインのスタックに std::string 用のスペースを割り当てます。

goo() が戻ると、agoo() 内の参照は無効になりますが、 std::stringa参照は main() のスタックに予約されたスペースにコピーされます

このような状況では、いくつかの最適化が可能です。1 つのコンパイラで何ができるかについては、こちらを参照してください。

于 2012-12-05T19:41:09.843 に答える