8

関数の戻り値をコピーまたは移動するのは呼び出し元ですか、それとも呼び出し先ですか? たとえば、このようにキューの pop() 関数を実装したい場合

template <typename T> 
class queue
{
    std::deque<T> d;
public:
    // ... //
    T pop()
    {
        // Creates a variable whose destructor removes the first
        // element of the queue if no exception is thrown. 
        auto guard = ScopeSuccessGuard( [=]{ d.pop_front(); } );
        return d.front();
    }
}

フロント要素をコピーした後にスコープガードのデストラクタが呼び出されますか?

編集:フォローアップの質問: 行は

auto item = q.pop();

今すぐ例外安全性を強くしますか?

4

2 に答える 2

9

戻り値は、ローカル変数がスコープ外になる前にコピーされます。コピー/移動は、一時的な場所 (スタックまたはレジスター) への場合もあれば、呼び出し元自身のバッファーまたは優先レジスターへの直接の場合もあります。これは最適化/インライン化の問題です。

一時的な場所が関係する場合、コンパイラは呼び出し元と呼び出し先の間で作業を分割する必要があり、OS およびバイナリ オブジェクト/実行可能形式固有の戻り値 (およびもちろん関数パラメーター) の規則がいくつかあります。あるコンパイラでコンパイルされたライブラリ/オブジェクトは、通常、別のコンパイラで引き続き使用できます。

ラインは...

auto item = q.pop();

...非常に例外的に安全ですか?

pop_front()can'tと仮定するとthrow、興味深いケースは一時的な場所が返される場合で、関数が返された後、そこから値が呼び出し元のバッファーに再度コピーされます。あなたはそれに対して十分に保護していないように私には思えます。省略 (呼び出し元の結果バッファー/レジスターで戻り値を直接構築する呼び出し先) は許可されていますが、必須ではありません。

これを調べるために、次のコードを書きました。

#include <iostream>

struct X
{
    X() { std::cout << "X::X(this " << (void*)this << ")\n"; }
    X(const X& rhs) { std::cout << "X::X(const X&, " << (void*)&rhs
                                << ", this " << (void*)this << ")\n"; }
    ~X() { std::cout << "X::~X(this " << (void*)this << ")\n"; }

    X& operator=(const X& rhs)
    { std::cout << "X::operator=(const X& " << (void*)&rhs
                << ", this " << (void*)this << ")\n"; return *this; }
};

struct Y
{
    Y() { std::cout << "Y::Y(this " << (void*)this << ")\n"; }
    ~Y() { std::cout << "Y::~Y(this " << (void*)this << ")\n"; }
};

X f()
{
   Y y;
   std::cout << "f() creating an X...\n";
   X x;
   std::cout << "f() return x...\n";
   return x;
};

int main()
{
    std::cout << "creating X in main...\n";
    X x;
    std::cout << "x = f(); main...\n";
    x = f();
}

でコンパイルするとg++ -fno-elide-constructors、私の出力(追加のコメント付き)は次のとおりです。

creating X in main...
X::X(this 0x22cd50)
x = f(); main...
Y::Y(this 0x22cc90)
f() creating an X...
X::X(this 0x22cc80)
f() return x...
X::X(const X&, 0x22cc80, this 0x22cd40)   // copy-construct temporary
X::~X(this 0x22cc80)   // f-local x leaves scope
Y::~Y(this 0x22cc90)
X::operator=(const X& 0x22cd40, this 0x22cd50)  // from temporary to main's x
X::~X(this 0x22cd40)
X::~X(this 0x22cd50)

明らかに、割り当てf()はスコープを残した後に発生しました。例外は、スコープ ガード (ここでは Y で表されます) が破壊された後です。

main にX x = f();orが含まれている場合も同様のことが起こりますが、 -local 変数X x(f());の破棄後に呼び出されるのはコピー コンストラクターです。f()

(あるコンパイラの動作は、標準が機能するために何かを必要とするかどうかを判断するための根拠として不十分な場合があることを理解していますが、逆の場合はかなり信頼性が高くなります。動作しない場合、そのコンパイラは壊れています-これは比較的まれです- または、標準はそれを必要としません. ここでは、コンパイラの動作は、標準の要件に関する私の印象に逸話的な重みを追加するために使用されています.)

好奇心旺盛な人のための面倒な詳細: 通常、一方向でしか呼び出せないコードがあると便利というわけではありませんが、安全かもしれないのは です。標準では、有効期間が延長された一時的なものを、追加のコピーがない関数にコピーされた一時的なものにする必要があります。ほんの少しの価値があります-それは私のプログラムで「機能」し、興味深いことに、戻り値を省略した場合に使用されるのと同じスタックの場所を一時的に占有します。const X& x = f();constf()-f-no-elide-constructorsオプションは最適化を無効にするのではなく、ペシミゼーションを追加するために邪魔になりません: 関数を呼び出す前に一時用に追加のスタックスペースを残してから、そこからコピーして一時を破棄するための余分なコードを追加してから、スタックポインターを再調整します... .

于 2013-07-02T09:00:06.880 に答える
6

戻り値のコピーは呼び出し先によって行われ、デストラクタが呼び出される前に作成する必要があります。そうしないと、ローカルに構築された変数の値/内容を返すことができないためです。

標準の関連セクションは次のとおりです。セクション12.4、ポイント11(デストラクタ)

デストラクタは暗黙的に呼び出されます

  • オブジェクトが作成されたブロックが終了するとき (6.7)、自動保存期間 (3.7.3) を持つ構築されたオブジェクトの場合

「破壊の前に帰還が起こる」と書かれている場所を見つけようとしていましたが、[何かが欠けていない限り]私が望むほど明確に述べられていません.

于 2013-07-02T09:00:39.543 に答える