0

返される変数が関数のスコープ外になった場合の C++ の戻り値の最適化についてはかなりよく理解していますが、メンバー変数を返す場合はどうでしょうか。次のコードを検討してください。

#include <iostream>
#include <string>

class NamedObject {
 public:
  NamedObject(const char* name) : _name(name) {}
  std::string name() const {return _name;}

 private:
  std::string _name;
};

int main(int argc, char** argv) {
  NamedObject obj("name");
  std::cout << "name length before clear: " << obj.name().length() << std::endl;
  obj.name().clear();
  std::cout << "name length after clear: " << obj.name().length() << std::endl;
  return 0;
}

どの出力:

name length before clear: 4
name length after clear: 4

明らかに、 はobj.name().clear()一時コピーに作用しますが、 への呼び出しはobj.name.length()どうですか? std::string::length()メンバー関数であるconstため、文字列の状態を変更しないことが保証されています。したがって、コンパイラがメンバー変数をコピーせず、const メンバー関数の呼び出しに直接使用することを許可する必要があるのは妥当と思われます。最新の C++ コンパイラはこの最適化を行いますか? 作ってはいけない、または作れない理由はありますか?

編集:

明確にするために、標準の戻り値の最適化がここで機能するかどうかを尋ねているわけではありません。最初に質問した時点でなぜそうでないのか理解しました。返される値が関数の範囲外に出ないという理由だけで、RVO が通常定義されている方法はここでは機能しません。

私が求めているのは、呼び出し時にコンパイラーが呼び出しに副作用がないと判断できる場合、コピーをスキップできるかどうかです。つまり、あたかも

obj.name().length()

そうだった

obj._name.length()
4

5 に答える 5

6

関数は値で戻ります。name()つまり、すべての操作は一時的に実行されます。

したがって、コンパイラがメンバー変数をコピーせず、const メンバー関数の呼び出しに直接使用することを許可する必要があるのは妥当と思われます。

この仮定は、多くの理由で正しくありません。関数が宣言されconst いる場合、オブジェクトの状態を変更しないことをコンパイラーに伝えているため、コンパイラーはそれを検証するのに役立ちます。戻り値の型は、コンパイラが実行できるチェックの一部です。たとえば、戻り値の型を次のように変更するとします。

std::string& name() const { return _name; }

コンパイラは不平を言うでしょう: あなたname()は状態を変更しないと約束しましたが、の人がそれを行うことができる参照を提供しています. さらに、その関数のセマンティクスは、呼び出し元が変更できるコピーを提供することです。コピーが省略された場合 (省略は不可能ですが、議論のために)、コードを呼び出すと、ローカル コピーのように見えるものが変更され、実際にオブジェクトの状態が変更される可能性があります。

一般に、const であるアクセサーを提供する場合は、コピーではなくメンバーへの参照を返す必要があります。

私は、一時的な C++ の戻り値の最適化についてかなりよく理解しています [...] 最新の C++ コンパイラはこの最適化を行いますか? 作ってはいけない、または作れない理由はありますか?

戻り値の最適化が何であるかをよく理解していないか、2 番目の質問をしないと思います。これを例に取りましょう。ユーザーコードが次の場合:

std::string foo() {
   std::string result;
   result = "Hi";
   return result;
}
std::string x = foo();

上記のコードには、潜在的に 3 つの文字列があります: resultinside foo、戻り値 (これを と呼びましょう__ret) およびx、および適用可能な 2 つの最適化: NRVOおよび汎用の copy- elisionです。NRVOは、関数を処理するときにコンパイラによって実行される最適化であり、mergintでfoo構成され、それらを同じ場所に配置して単一のオブジェクトを作成します。最適化の 2 番目の部分は呼び出し側で実行する必要があり、ここでも 2 つのオブジェクトとの位置がマージされます。result__retx__ret

実際の実装では、2 つ目から始めます。呼び出し元 (ほとんどの呼び出し規則では) は、返されたオブジェクトのメモリの割り当てを担当します。最適化を行わない場合 (および一種の疑似コードの場合) は、呼び出し元で次のように処理されます。

[uninitialized] std::string __ret;
foo( [hidden arg] &__ret );          // Initializes __ret
std::string x = __ret;

ここで、コンパイラは一時的な__ret意志が初期化するためだけに有効であることを知っているためx、コードを次のように変換します。

[uninitialized] std::string x;
foo( [hidden arg] &x );             // Initializes x

そして、呼び出し元のコピーは省略されます。中のコピーfooも同様に省略されます。変換された (呼び出し規約に準拠するための) 関数は次のとおりです。

void foo( [hidden uninitialized] std::string* __ret ) {
   std::string result;
   result = "Hi";
   new (__ret) std::string( result );   // placement new: construct in place
   return;
}

この場合の最適化はまったく同じです。result返されたオブジェクトを初期化するためだけに存在するため、新しいオブジェクトを作成するのではなく、同じスペースを再利用できます。

void foo( [hidden uninitialized] std::string* __ret ) {
   new (__ret) std::string();
   (*__ret) = "Hi";
   return;
}

元の問題に戻ると、メンバー関数が呼び出される前にメンバー変数が存在するため、この最適化は適用できません。コンパイラは、メンバー属性と同じ場所に戻り値を配置できません。これは、その変数が__ret(呼び出し元によって提供された) アドレスではない既知の場所で既に有効であるためです。

過去にNRVOコピー省略について書きました。それらの記事を読むことに興味があるかもしれません。

于 2012-06-15T21:10:33.803 に答える
2

簡潔な答え:

length()インライン化またはコンパイラ固有の魔法を使用してコンパイルするときに、コンパイラがコピー コンストラクタとメソッドの実装を確認しない限り、mainそのコピーを最適化して取り除くことはできません。

長い答え:

通常、C++ 標準では、実行すべき最適化と実行すべきでない最適化を直接規定することはありません。実際、最適化とは、ほぼ定義上、適切に形成されたプログラムの動作を変更しないものです。

特定の の呼び出しobj.nameによって、オブザーバーがその存在を証明できないコピーが生成されることをコンパイラが証明できる場合、コンパイラはそのコピーを自由に省略できます。ほんの少しのインライン化でも同様のケースになる可能性があるため、このコピーの省略は理論的にはここで許可されています。印刷したり、その効果を使用したりしないためです。

ここで、よく見てみると、標準の 12.8 節には 4 つの追加の状況がリストされています (例外処理、nameケースの内部などの呼び出し先の戻り値、参照への一時的なバインドに関連する)。const簡単に参照できるようにこの投稿にリストしますが、呼び出しから一時的なものを受け取り、メソッドを呼び出すために使用されるケースと一致するものはありません。

したがって、これらの明示的な「例外」では、 の修飾子を検査しmainて通知するだけでコピーを最適化することはできません。constlength()

特定の基準が満たされると、オブジェクトのコピー/移動コンストラクタおよび/またはデストラクタに副作用がある場合でも、実装はクラス オブジェクトのコピー/移動構築を省略することができます。このような場合、実装は、省略されたコピー/移動操作のソースとターゲットを、同じオブジェクトを参照する 2 つの異なる方法として扱い、そのオブジェクトの破棄は、2 つのオブジェクトが削除された時点のいずれか遅い方の時点で発生します。最適化なしで破壊されました。コピー省略と呼ばれるこのコピー/移動操作の省略は、次の状況で許可されます (複数のコピーを排除するために組み合わせることができます)。

— クラスの戻り値の型を持つ関数の return ステートメントで、式が関数の戻り値の型と同じ cvunqualified 型を持つ非揮発性自動オブジェクト (関数または catch 節パラメーター以外) の名前である場合、自動オブジェクトを関数の戻り値に直接構築することにより、コピー/移動操作を省略できます

— throw 式で、オペランドが非 volatile 自動オブジェクト (関数または catch 句パラメーター以外) の名前であり、そのスコープが最も内側の try ブロックの末尾を超えて拡張されていない場合 (存在する場合) 1 つ)、オペランドから例外オブジェクト (15.1) へのコピー/移動操作は、自動オブジェクトを直接例外オブジェクトに構築することによって省略できます。

— 参照 (12.2) にバインドされていない一時クラス オブジェクトが同じ cv 非修飾型のクラス オブジェクトにコピー/移動される場合、一時オブジェクトを省略したコピー/移動の対象

— 例外ハンドラーの例外宣言 (条項 15) が、例外オブジェクト (15.1) と同じタイプ (cv 修飾を除く) のオブジェクトを宣言する場合、コピー/移動操作は、例外宣言を処理することによって省略できます。 exception-declaration によって宣言されたオブジェクトのコンストラクタとデストラクタの実行を除いて、プログラムの意味が変更されない場合は、例外オブジェクトのエイリアスとして。

于 2012-06-15T21:16:00.900 に答える
1

コンパイラーがどのような最適化を行うかを知る最良の方法は、コンパイラーが生成するアセンブリーを調べて、コンパイラーが実際に何を行うかを正確に確認することです。特定のコンパイラがすべての状況でどのような最適化を行うかどうかを予測することは非常に困難であり、ほとんどの人は通常、悲観的すぎるか、楽観的すぎます。

一方、コンパイラの出力を調べるだけで、当て推量を必要とせずに、コンパイラが何をするかを正確に確認できます。

Visual Studioでは、プロジェクトのプロパティ-> C / C++->出力ファイル->アセンブラー出力->「ソースコードを使用したアセンブリ」を設定するか、/ Fasをソースコードに指定するだけで、ソースコードとインターリーブされたアセンブリの便利な出力を取得できます。コマンドライン-Sを使用してアセンブリを出力するようにGCCに指示できますが、それはアセンブリの行をソースの行と相関させません。そのためには、objdumpを使用するか、バージョンで機能する場合は-fverbose-asmコマンドラインオプションを使用する必要があります。

たとえば、コードのブロックの1つ(MSVCのフルリリースでコンパイルされたもの)は次のとおりです。

; 23   :    obj.name().clear();

    lea ecx, DWORD PTR _obj$[esp+92]
    push    ecx
    lea esi, DWORD PTR $T23719[esp+96]
    call    ?name@NamedObject@@QBE?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@XZ ; NamedObject::name
    mov DWORD PTR [eax+16], ebx
    cmp DWORD PTR [eax+20], edi
    jb  SHORT $LN70@main
    mov eax, DWORD PTR [eax]
$LN70@main:
    mov BYTE PTR [eax], bl
    mov ebx, DWORD PTR __imp_??3@YAXPAX@Z
    cmp DWORD PTR $T23719[esp+112], edi
    jb  SHORT $LN84@main
    mov edx, DWORD PTR $T23719[esp+92]
    push    edx
    call    ebx
    add esp, 4
$LN84@main:

; 24   :    std::cout << "name length after clear: " << obj.name().length() << std::endl;

    lea eax, DWORD PTR _obj$[esp+92]
    push    eax
    lea esi, DWORD PTR $T23720[esp+96]
    call    ?name@NamedObject@@QBE?AV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@XZ ; NamedObject::name
    mov BYTE PTR __$EHRec$[esp+100], 2
    mov ecx, DWORD PTR __imp_?endl@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@1@AAV21@@Z
    mov eax, DWORD PTR [eax+16]
    mov edx, DWORD PTR __imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A
    push    ecx
    push    eax
    push    OFFSET ??_C@_0BK@PFKLDML@name?5length?5after?5clear?3?5?$AA@
    push    edx
    call    ??$?6U?$char_traits@D@std@@@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@0@AAV10@PBD@Z ; std::operator<<<std::char_traits<char> >
    add esp, 8
    mov ecx, eax
    call    DWORD PTR __imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@I@Z
    mov ecx, eax
    call    DWORD PTR __imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@P6AAAV01@AAV01@@Z@Z
    cmp DWORD PTR $T23720[esp+112], edi
    jb  SHORT $LN108@main
    mov eax, DWORD PTR $T23720[esp+92]
    push    eax
    call    ebx
    add esp, 4

( undname.exeを使用してMSVCシンボル名の装飾を解除できます)ご覧のとおり、この場合、NamedObject::name().clear()の前と.length()の前の両方で関数を呼び出します。

于 2012-06-15T20:59:25.800 に答える
1

constメンバー関数であるため、文字列の状態を変更しないことが保証されています

それは真実ではない。std::stringはデータ メンバーを持つことができ、任意の関数はオフまたはその任意のメンバーをmutableキャストできます。constthis

于 2012-06-15T20:39:33.533 に答える
0

戻り値の最適化とは、関数に対してローカルなスコープを持つ一時オブジェクトまたはオブジェクトを削除し、削除されるオブジェクトを戻りオブジェクトのエイリアスとして使用することにより、returnステートメントの暗黙的なコピーを削除することです。

明らかに、これは、関数がreturnステートメントで使用されているオブジェクトを構築している場合にのみ適用されます。返されるオブジェクトがすでに存在する場合、追加のオブジェクトは作成されないため、返されるオブジェクトを返されるオブジェクトにコピーする必要があります。関数内に削除できる他のオブジェクト構築はありません。

上記のすべてにかかわらず、コンパイラーは、適合プログラムによって動作の違いが観察されない限り、適切と思われる最適化を行うことができます。これにより、あらゆる(観察不可能な)可能性があります。

于 2012-06-15T20:51:33.943 に答える