CまたはC ++で構造体を返すのは安全ですか?


82

私が理解しているのは、これを行うべきではないということですが、このようなことを行う例を見たことがあると思います(コードは必ずしも構文的に正しいとは限りませんが、アイデアはそこにあります)

typedef struct{
    int a,b;
}mystruct;

そして、ここに関数があります

mystruct func(int c, int d){
    mystruct retval;
    retval.a = c;
    retval.b = d;
    return retval;
}

このようなことをしたい場合は、mallocされた構造体へのポインターを常に返す必要があることを理解しましたが、このようなことを行う例を見たことがあると確信しています。これは正しいです?個人的には、常にmallocされた構造体へのポインターを返すか、関数を参照してパスを実行し、そこで値を変更します。(私の理解では、関数のスコープが終了すると、構造を割り当てるために使用されたスタックはすべて上書きされる可能性があります)。

質問に2番目の部分を追加しましょう:これはコンパイラによって異なりますか?もしそうなら、デスクトップ用のコンパイラの最新バージョン(gcc、g ++、Visual Studio)の動作はどうなりますか?

問題についての考え?


33
「私が理解していることは、これは行われるべきではないということです」と誰が言いますか?私はいつもそれをやっています。また、C ++ではtypedefは不要であり、「C / C ++」のようなものは存在しないことに注意してください。
PlasmaHH 2012年

4
質問はc ++を対象としていないようです。
キャプテンキリン2012年

4
@PlasmaHH周りの大きな構造をコピーするのは非効率的です。そのため、構造体に値で返す前に、特に構造体に高価なコピーコンストラクターがあり、コンパイラーが戻り値の最適化に長けていない場合は、注意してよく考える必要があります。私は最近、あるプログラマーがどこでも値で返すことに決めたいくつかの大きな構造のコピーコンストラクターにかなりの時間を費やしていたアプリを最適化しました。この非効率性により、購入する必要のある追加のデータセンターハードウェアに約800,000ドルのコストがかかりました。
Crashworks 2012年

8
@Crashworks:おめでとうございます。あなたの上司があなたに昇給してくれたことを願っています。
PlasmaHH 2012年

6
@Crashworks:考えずに常に値で返すのは悪いことですが、それが自然な状況では、通常、コピーを作成する必要のない安全な代替手段がないため、値で返すことが最善の解決策です。ヒープを割り当てる必要はありません。多くの場合であってもそこにいないだろうこと、それが可能だときにおよびC ++ 11にジャンプする必要があります良いコンパイラのコピーの省略を使用して、コピー、移動のセマンティクスは、より深いコピーの排除することができます。値で返す以外のことを行うと、両方のメカニズムが正しく機能しません。
左回り2012年

回答:


75

それは完全に安全であり、そうすることは間違いではありません。また、コンパイラによって異なりません。

通常、(あなたの例のように)構造体が大きすぎない場合、このアプローチは、mallocされた構造体を返すよりも優れていると主張します(mallocコストのかかる操作です)。


3
フィールドの1つがchar *だったとしても、それでも安全でしょうか?これで、構造体にポインタがあります
jzepeda 2012年

3
@ user963258実際には、コピーコンストラクタとデストラクタの実装方法によって異なります。
Luchian Grigore 2012年

2
@PabloSantaCruzそれは難しい質問です。それが試験の質問であった場合、所有権を考慮する必要がある場合、審査官は回答として「いいえ」を期待する可能性があります。
キャプテンキリン2012年

2
@CaptainGiraffe:本当です。OPはこれを明確にしておらず、彼/彼女の例は基本的にCであったので、私はそれがC ++の質問というよりもCの質問であると思いました。
パブロサンタクルス

2
@Kos一部のコンパイラにはNRVOがありませんか?何年から?また、注意:C ++ 11では、NRVOがない場合でも、代わりに移動セマンティクスが呼び出されます。
デビッド

70

それは完全に安全です。

あなたは価値によって戻ってきています。未定義の動作につながるのは、参照によって戻ってきた場合です。

//safe
mystruct func(int c, int d){
    mystruct retval;
    retval.a = c;
    retval.b = d;
    return retval;
}

//undefined behavior
mystruct& func(int c, int d){
    mystruct retval;
    retval.a = c;
    retval.b = d;
    return retval;
}

スニペットの動作は完全に有効であり、定義されています。コンパイラによって異なりません。大丈夫です!

個人的には、mallocされた構造体へのポインタを常に返します。

あなたはすべきではありません。可能な場合は、動的に割り当てられたメモリを避ける必要があります。

または、関数を参照してパスを実行し、そこで値を変更します。

このオプションは完全に有効です。それは選択の問題です。一般に、元の構造体を変更しながら、関数から何か他のものを返したい場合は、これを行います。

私の理解では、関数のスコープが終了すると、構造を割り当てるために使用されたスタックはすべて上書きされる可能性があります

これは間違っています。つまり、それは一種の正しいことですが、関数内で作成した構造体のコピーを返します。理論的には。実際には、RVOは発生する可能性があり、発生する可能性があります。戻り値の最適化について読んでください。これはretval、関数が終了するとスコープ外に見えるが、余分なコピーを防ぐために、実際には呼び出し元のコンテキストでビルドされる可能性があることを意味します。これは、コンパイラーが自由に実装できる最適化です。


3
RVOに言及するための+1。この重要な最適化により、STLコンテナーなど、高価なコピーコンストラクターを使用するオブジェクトでこのパターンを実際に実行できます。
コス2012年

1
コンパイラーは戻り値の最適化を自由に実行できますが、それが実行される保証はありません。これはあなたが信頼できるものではなく、ただ希望するものです。
Watcom 2013年

-1は、「可能な場合は動的に割り当てられたメモリを回避する」ためのものです。これは初心者くさいルールやコードで頻繁に結果になりがちLARGEの単純なポインタは、多くの時間を節約することができたときにデータの量が返される(そして物事が遅くなぜ彼らはパズル)。正しいルールは速度に基づいて、リターン構造やポインタであります、使用法、および明快
ロイド・サージェント

9

mystruct関数内のオブジェクトの存続期間は、関数を終了すると実際に終了します。ただし、returnステートメントでオブジェクトを値で渡します。これは、オブジェクトが関数から呼び出し元の関数にコピーされることを意味します。元のオブジェクトはなくなりましたが、コピーは存続しています。


8

structCでaを返すのが安全であるだけでなく(またはclassC ++で、struct-sは実際にclassはデフォルトのpublic:メンバーで-esです)、多くのソフトウェアがそれを行っています。

もちろん、classC ++でを返すとき、言語はいくつかのデストラクタまたは移動コンストラクタが呼び出されることを指定しますが、これがコンパイラによって最適化される可能性がある場合が多くあります。

さらに、Linux x86-64 ABIstruct2つのスカラー(たとえば、ポインター、またはlong)値を持つをレジスター(%rax%rdx)を介して返すことを指定しているため、非常に高速で効率的です。したがって、その特定のケースでは、struct他のことを行うよりも、そのような2スカラーフィールドを返す方がおそらく高速です(たとえば、引数として渡されるポインターにそれらを格納する)。

そのような2スカラー場を返すことは、それを-ingしてポインタを返すstructよりもはるかに高速ですmalloc


5

これは完全に合法ですが、構造体が大きい場合は、速度とスタックサイズという2つの要素を考慮する必要があります。


1
戻り値の最適化を聞いたことがありますか?
Luchian Grigore 2012年

はい。ただし、構造体を値で返すのが一般的であり、コンパイラーがRVOを実行できない場合があります。
ebutusov 2012年

3
いくつかのプロファイリングを行った後、余分なコピーについてのみ心配すると思います。
Luchian Grigore 2012年

4

構造体型は、関数によって返される値の型にすることができます。コンパイラは構造体のコピーを作成し、関数内のローカル構造体ではなくそのコピーを返すため、安全です。

typedef struct{
    int a,b;
}mystruct;

mystruct func(int c, int d){
    mystruct retval;
    cout << "func:" <<&retval<< endl;
    retval.a = c;
    retval.b = d;
    return retval;
}

int main()
{
    cout << "main:" <<&(func(1,2))<< endl;


    system("pause");
}

4

安全性は、構造体自体がどのように実装されたかによって異なります。似たようなものを実装しているときにこの質問に出くわしました。これが潜在的な問題です。

コンパイラは、値を返すときに、いくつかの操作を実行します(おそらく他の操作もあります)。

  1. コピーコンストラクターを呼び出しますmystruct(const mystruct&)(コンパイラー自体によって割り当てられた関数の外部this一時変数です)func
  2. ~mystruct内部に割り当てられた変数のデストラクタを呼び出しますfunc
  3. mystruct::operator=戻り値が他の何かに割り当てられている場合に呼び出す=
  4. ~mystructコンパイラが使用する一時変数でデストラクタを呼び出します

さて、場合は、mystructここに記載されているような、単純なようであることは、すべての罰金ですが、それはポインタ(のように持っている場合char*)、またはより複雑なメモリ管理を、それはすべての方法に依存しmystruct::operator=mystruct(const mystruct&)、および~mystruct実装されています。したがって、複雑なデータ構造を値として返す場合は注意が必要です。


c ++ 11より前にのみ当てはまります。
ビョルンサンディン

4

行ったように構造体を返すことは完全に安全です。

ただし、このステートメントに基づくと、関数のスコープが終了すると、構造体の割り当てに使用されたスタックはすべて上書きできると理解しているため、構造体のメンバーのいずれかが動的に割り当てられたシナリオのみを想像します( malloc'edまたはnew'ed)。この場合、RVOがないと、動的に割り当てられたメンバーは破棄され、返されるコピーには、ガベージを指すメンバーが含まれます。


スタックは、コピー操作のために一時的にのみ使用されます。通常、スタックは呼び出しの前に予約され、呼び出された関数は返されるデータをスタックに配置し、呼び出し元はこのデータをスタックからプルして、割り当てられた場所に格納します。だから、そこに心配はありません。
Thomas Tempelmann 2012年

3

私もsftrabbitに同意します。Lifeは実際に終了し、スタック領域はクリアされますが、コンパイラはすべてのデータをレジスタまたはその他の方法で確実に取得できるほど賢いです。

確認のための簡単な例を以下に示します(Mingwコンパイラアセンブリから取得)

_func:
    push    ebp
    mov ebp, esp
    sub esp, 16
    mov eax, DWORD PTR [ebp+8]
    mov DWORD PTR [ebp-8], eax
    mov eax, DWORD PTR [ebp+12]
    mov DWORD PTR [ebp-4], eax
    mov eax, DWORD PTR [ebp-8]
    mov edx, DWORD PTR [ebp-4]
    leave
    ret

bの値がedxを介して送信されていることがわかります。デフォルトのeaxにはaの値が含まれています。


2

構造を返すことは安全ではありません。私は自分でそれを行うのが大好きですが、誰かが後で返された構造にコピーコンストラクターを追加すると、コピーコンストラクターが呼び出されます。これは予期しないことであり、コードを壊す可能性があります。このバグを見つけるのは非常に困難です。

もっと詳しく答えましたが、モデレーターは気に入らなかった。だから、あなたの費用で、私のヒントは短いです。


「構造物を返却することは安全ではありません。[…]コピーコンストラクターが呼び出されます。」–安全非効率には違いがあります。構造体を返すことは間違いなく安全です。それでも、最初に構造体が呼び出し元のスタックに作成されるため、コピーctorの呼び出しはコンパイラーによって省略される可能性があります。
PHG

2

質問に2番目の部分を追加しましょう:これはコンパイラによって異なりますか?

私が自分の痛みを発見したように、確かにそうです:http//sourceforge.net/p/mingw-w64/mailman/message/33176880/

私はwin32(MinGW)でgccを使用して、構造体を返すCOMインターフェイスを呼び出していました。MSはGNUとは異なる方法で処理するため、私の(gcc)プログラムはスタックが破壊されてクラッシュしました。

ここではMSの方が高いかもしれませんが、私が気にしているのは、Windows上で構築するためのMSとGNU間のABI互換性だけです。

含まれている場合、デスクトップ用の最新バージョンのコンパイラ(gcc、g ++、およびVisual Studio)の動作はどうなりますか。

MSがどのようにそれを行っているように見えるかについてのいくつかのメッセージをWineメーリングリストで見つけることができます。


参照しているWineメーリングリストへのポインタを指定すると、さらに役立ちます。
ジョナサンレフラー2015年

構造体を返すことは問題ありません。COMはバイナリインターフェイスを指定します; 誰かがCOMを適切に実装しない場合、それはバグになります。
MM

1

注:この回答はc ++ 11以降にのみ適用されます。「C / C ++」のようなものはなく、異なる言語です。

いいえ、ローカルオブジェクトを値で返す危険はありません。そうすることをお勧めします。しかし、ここでのすべての答えに欠けている重要な点があると思います。他の多くの人は、構造体がコピーされているか、RVOを使用して直接配置されていると述べています。ただし、これは完全には正しくありません。ローカルオブジェクトを返すときに発生する可能性のあることを正確に説明しようと思います。

移動セマンティクス

c ++ 11以降、安全に盗むことができる一時オブジェクトへの参照である右辺値参照がありました。例として、std :: vectorには、ムーブコンストラクターとムーブ代入演算子があります。これらは両方とも一定の複雑さを持ち、移動元のベクトルのデータへのポインターをコピーするだけです。ここでは、移動セマンティクスについて詳しくは説明しません。

関数内でローカルに作成されたオブジェクトは一時的なものであり、関数が戻るとスコープ外になるため、返されたオブジェクトがc ++ 11以降でコピーされることはありません。移動コンストラクターは、返されるオブジェクトで呼び出されます(または、後で説明します)。つまり、大きなベクターのように、高価なコピーコンストラクターで安価な移動コンストラクターを使用してオブジェクトを返す場合、データの所有権のみがローカルオブジェクトから返されたオブジェクトに転送されます。これは安価です。

特定の例では、オブジェクトのコピーと移動に違いはないことに注意してください。構造体のデフォルトのmoveおよびcopyコンストラクターは、同じ操作になります。2つの整数をコピーします。ただし、構造体全体が64ビットCPUレジスタに収まるため、これは少なくとも他のソリューションよりも高速です(間違っている場合は訂正してください。CPUレジスタについてはあまり知りません)。

RVOとNRVO

RVOは戻り値の最適化を意味し、コンパイラが実行する数少ない最適化の1つであり、副作用が発生する可能性があります。c ++ 17以降、RVOが必要です。名前のないオブジェクトを返す場合、呼び出し元が戻り値を割り当てる場所に直接構築されます。コピーコンストラクターも移動コンストラクターも呼び出されません。RVOがないと、名前のないオブジェクトは最初にローカルで構築され、次に返されたアドレスで構築されて移動し、次にローカルの名前のないオブジェクトが破棄されます。

RVOが必要な場合(c ++ 17)または可能性が高い場合(c ++ 17より前)の例:

auto function(int a, int b) -> MyStruct {
    // ...
    return MyStruct{a, b};
}

NRVOは、名前付き戻り値の最適化を意味し、呼び出された関数にローカルな名前付きオブジェクトに対して実行されることを除いて、RVOと同じです。これはまだ標準(c ++ 20)によって保証されていませんが、多くのコンパイラはまだそれを行っています。名前付きのローカルオブジェクトを使用しても、返されるときに最悪の場合は移動されることに注意してください。

結論

値で返さないことを検討する必要がある唯一のケースは、名前が付けられた非常に大きい(スタックサイズのように)オブジェクトがある場合です。これは、NRVOがまだ保証されておらず(c ++ 20以降)、オブジェクトの移動でさえ遅いためです。私の推奨事項、およびCppコアガイドラインの推奨事項は、常に値でオブジェクトを返すことを優先することです(複数の戻り値がある場合は、struct(またはタプル)を使用します)。唯一の例外は、オブジェクトの移動にコストがかかる場合です。その場合は、非const参照パラメーターを使用してください。

C ++の関数から手動で解放する必要があるリソースを返すことは決して良い考えではありません。絶対にしないでください。少なくともstd :: unique_ptrを使用するか、リソースを解放するデストラクタRAII)を使用して独自の非ローカルまたはローカル構造体を作成し、そのインスタンスを返します。リソースに独自の移動セマンティクスがない場合は、移動コンストラクターと移動代入演算子を定義する(およびコピーコンストラクター/代入を削除する)こともお勧めします。


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