ポインタを渡すのではなく、Cで構造体を値で渡すことの欠点はありますか?


157

ポインタを渡すのではなく、Cで構造体を値で渡すことの欠点はありますか?

構造体が大きい場合、明らかに大量のデータをコピーするパフォーマンスの側面がありますが、構造体が小さい場合、基本的には、いくつかの値を関数に渡すことと同じです。

戻り値として使用すると、さらに興味深いかもしれません。Cには関数からの戻り値が1つしかありませんが、多くの場合、いくつか必要です。簡単な解決策は、それらを構造体に入れて返すことです。

これには賛成または反対の理由がありますか?

ここで私が話していることは誰にとっても明白ではないかもしれないので、簡単な例を挙げます。

Cでプログラミングしている場合は、遅かれ早かれ次のような関数の記述を開始します。

void examine_data(const char *ptr, size_t len)
{
    ...
}

char *p = ...;
size_t l = ...;
examine_data(p, l);

これは問題ではありません。唯一の問題は、すべての関数で同じ規則を使用するために、パラメーターの順序を同僚に同意する必要があることです。

しかし、同じ種類の情報を返したい場合はどうなりますか?通常は次のようになります。

char *get_data(size_t *len);
{
    ...
    *len = ...datalen...;
    return ...data...;
}
size_t len;
char *p = get_data(&len);

これは正常に機能しますが、はるかに問題があります。戻り値は戻り値ですが、この実装ではそうではありません。上記から、関数get_dataがlenが指すものを見ることが許可されていないことを知る方法はありません。そして、値がそのポインタを通じて実際に返されることをコンパイラにチェックさせるものは何もありません。だから来月、誰かがコードを正しく理解せずにコードを変更すると(彼はドキュメントを読んでいないため)、だれにも気付かれずにコードが壊れるか、ランダムにクラッシュし始めます。

だから、私が提案する解決策は単純な構造です

struct blob { char *ptr; size_t len; }

例は次のように書き直すことができます。

void examine_data(const struct blob data)
{
    ... use data.tr and data.len ...
}

struct blob = { .ptr = ..., .len = ... };
examine_data(blob);

struct blob get_data(void);
{
    ...
    return (struct blob){ .ptr = ...data..., .len = ...len... };
}
struct blob data = get_data();

どういうわけか、ほとんどの人は本能的にexamine_dataにstruct blobへのポインターを取得させると思いますが、その理由はわかりません。それはまだポインタと整数を取得します、それらが一緒に行くことはちょうどより明確です。そして、get_dataの場合、長さの入力値がなく、返された長さがなければならないため、前に説明した方法で失敗することはありません。


それが価値があるものについてvoid examine data(const struct blob)は、間違っています。
Chris Lutz、

ありがとう、変数名を含むように変更しました。
dkagedal 2011

1
「上記から、関数get_dataがlenが指すものを見ることが許可されていないことを伝える方法はありません。そして、値がそのポインターを通じて実際に返されることをコンパイラーにチェックさせるものはありません。」-これは私にはまったく意味がありません(おそらく、最後の2行が関数の外に表示されているため、例が無効なコードであるためです)。詳しく説明してもらえますか?
Adam Spiers 2013

2
関数の下の2行は、関数の呼び出し方法を示すためにあります。関数のシグネチャは、実装がポインタにのみ書き込む必要があるという事実にヒントを与えません。また、コンパイラーは、値がポインターに書き込まれていることを検証する必要があることを知る方法がないため、戻り値のメカニズムはドキュメントでのみ説明できます。
dkagedal 2013

1
Cでこれを頻繁に行わない主な理由は歴史的なものです。C89より前のバージョンでは、構造体を値で渡したり返したりすることができなかったため、C89よりも前から存在し、論理的にそれを行うべきであるすべてのシステムインターフェイスは、gettimeofday代わりにポインターを使用します。
zwol

回答:


202

小さな構造体(たとえば、ポイント、四角形)の場合、値渡しは完全に許容されます。しかし、速度とは別に、大きな構造体を値で渡したり戻したりすることに注意する必要があるもう1つの理由があります。それは、スタックスペースです。

多くのCプログラミングは組み込みシステム向けであり、メモリは非常に貴重であり、スタックサイズはKBまたはバイト単位で測定される場合があります...構造体を値で渡すか返す場合、それらの構造体のコピーが配置されますスタック、このサイトの名前が付けられた状況を引き起こす可能性があります...

スタックが過剰に使用されているように見えるアプリケーションを見つけた場合、値で渡される構造体は、私が最初に探すものの1つです。


2
「構造体を値で渡したり返したりする場合、それらの構造体のコピーがスタックに配置されます」私は、そうしたツールチェーンをすべてブレインデッドと呼んでいます。そう、多くの人がそうするのは悲しいことですが、C標準が求めるものではありません。健全なコンパイラーは、それをすべて最適化します。
モニカを

1
頻繁に行われません。その理由@KubaOberこれは、次のとおりです。stackoverflow.com/questions/552134/...
ロディ

1
小さな構造体と大きな構造体を分離する決定的な行はありますか?
ジョシー・トンプソン

63

言及されていないこれを行わない理由の1つは、これがバイナリ互換性が重要な問題を引き起こす可能性があることです。

使用するコンパイラに応じて、コンパイラオプション/実装に応じて、スタックまたはレジスタを介して構造を渡すことができます。

見る: http //gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html

-fpcc-struct-return

-freg-struct-return

2つのコンパイラが一致しない場合、問題が発生する可能性があります。言うまでもなく、これを行わない主な理由が示されているのは、スタックの消費とパフォーマンスの理由です。


4
これは私が探していた答えのようなものでした。
dkagedal 2008年

2
確かに、しかしそれらのオプションは値渡しとは関係ありません。それらは完全に異なるものである構造体を返すことに関連しています。参照によって物を返すことは、通常、両足で自分を撃つ確実な方法です。int &bar() { int f; int &j(f); return j;};
Roddy

19

本当に組み立てた土地に深く掘るために、1つのニーズを、この質問に答えます:

(次の例では、x86_64でgccを使用しています。MSVC、ARMなどの他のアーキテクチャを追加することはどなたでも可能です。)

プログラム例を見てみましょう:

// foo.c

typedef struct
{
    double x, y;
} point;

void give_two_doubles(double * x, double * y)
{
    *x = 1.0;
    *y = 2.0;
}

point give_point()
{
    point a = {1.0, 2.0};
    return a;
}

int main()
{
    return 0;
}

完全な最適化でコンパイルする

gcc -Wall -O3 foo.c -o foo

アセンブリを見てください:

objdump -d foo | vim -

これは私たちが得るものです:

0000000000400480 <give_two_doubles>:
    400480: 48 ba 00 00 00 00 00    mov    $0x3ff0000000000000,%rdx
    400487: 00 f0 3f 
    40048a: 48 b8 00 00 00 00 00    mov    $0x4000000000000000,%rax
    400491: 00 00 40 
    400494: 48 89 17                mov    %rdx,(%rdi)
    400497: 48 89 06                mov    %rax,(%rsi)
    40049a: c3                      retq   
    40049b: 0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)

00000000004004a0 <give_point>:
    4004a0: 66 0f 28 05 28 01 00    movapd 0x128(%rip),%xmm0
    4004a7: 00 
    4004a8: 66 0f 29 44 24 e8       movapd %xmm0,-0x18(%rsp)
    4004ae: f2 0f 10 05 12 01 00    movsd  0x112(%rip),%xmm0
    4004b5: 00 
    4004b6: f2 0f 10 4c 24 f0       movsd  -0x10(%rsp),%xmm1
    4004bc: c3                      retq   
    4004bd: 0f 1f 00                nopl   (%rax)

noplパッドを除くと、give_two_doubles()27バイトでgive_point()29バイトです。一方、give_point()生成される命令は、give_two_doubles()

興味深いのは、コンパイラーがmovより高速なSSE2バリアントmovapdとに最適化できたことmovsdです。さらに、give_two_doubles()実際にメモリとの間でデータを移動するため、処理が遅くなります。

明らかに、これの多くは組み込み環境には適用できない可能性があります(現在、Cの活躍の場はほとんどの場合です)。私は組み立てウィザードではないので、どんなコメントでも歓迎します!


6
大きな違いを示したり、予測が難しいジャンプの数など、より興味深い側面を数えたりできない限り、命令の数を数えることはそれほど興味深いことではありません。実際のパフォーマンスプロパティは、命令の数よりもはるかに微妙です。 。
dkagedal 2010

6
@dkagedal:はい。振り返ってみると、私自身の答えは非常に貧弱に書かれていると思います。命令の数にはあまり重点を置きませんでしたが(その印象を与えたのはどうも:Pです)、実際のポイントは、構造体を値で渡す方が、小さい型の参照で渡すよりも望ましいということです。とにかく、値による受け渡しがより簡単であり(ライフタイムジャグリングがなく、誰かがデータを変更したり、const常に変更することを心配する必要がない)、値渡しによるコピーでパフォーマンスが大幅に低下するわけではないことがわかりました。 、多くの人が信じているかもしれないことに反して。
kizzx2

15

単純な解決策は、エラーコードを戻り値として返し、その他はすべて関数内のパラメーターとして返します。
このパラメーターはもちろん構造体にすることができますが、値によってこれを渡す特定の利点はなく、ポインターを送信しただけです。
構造体を値で渡すのは危険です。渡す内容を十分に注意する必要があります。Cにはコピーコンストラクターがないため、構造体パラメーターの1つがポインターである場合、ポインターの値がコピーされるため、非常にわかりにくく、難しい場合があります。維持する。

答えを完成させるためだけに(Roddyへの完全なクレジット)スタックの使用は、構造体を値で渡さないもう1つの理由です。スタックオーバーフローのデバッグは本当のPITAだと思います。

コメントを再生:

構造体をポインタで渡すことは、一部のエンティティがこのオブジェクトの所有権を持ち、何をいつ解放する必要があるかについて完全な知識を持っていることを意味します。値で構造体を渡すと、構造体の内部データ(別の構造体へのポインタなど)への非表示の参照が作成されます。これを維持するのは困難です(可能ですが、なぜですか?)。


6
しかし、ポインタを渡すことは、構造体にポインタを置いたからといって「危険」ではないので、私はそれを購入しません。
dkagedal 2008年

ポインタを含む構造をコピーすることの大きなポイント。この点はあまり明白ではないかもしれません。彼が何を指しているのかわからない場合は、ディープコピーとシャローコピーを検索してください。
zooropa 2009

1
C関数の規則の1つは、入力パラメーターの前に出力パラメーターを最初にリストすることです。たとえば、int func(char * out、char * in);
zooropa 2009

たとえばgetaddrinfo()が出力パラメーターを最後に置く方法のようですか?:-)慣習には何千ものセットがあり、好きなように選択できます。
dkagedal 2013

10

ここで人々がこれまでに言及し忘れたことの1つ(または見落としました)は、構造体には通常パディングがあるということです。

struct {
  short a;
  char b;
  short c;
  char d;
}

charはすべて1バイト、shortはすべて2バイトです。構造体の大きさはどれくらいですか?いいえ、6バイトではありません。少なくとも、より一般的に使用されているシステムではそうではありません。ほとんどのシステムでは8になります。問題は、配置が一定ではなく、システムに依存するため、同じ構造体でも、配置が異なり、システムによってサイズが異なります。

そのパディングはスタックをさらに食い尽くすだけでなく、システムがどのようにパディングし、アプリにあるすべての構造体を見てサイズを計算しないかを事前に予測できないという不確実性も追加しますそれのための。ポインタを渡すと、予測可能なスペースが必要になります。不確実性はありません。ポインターのサイズはシステムにとって既知であり、構造体がどのように見えるかに関係なく、常に等しく、ポインターのサイズは常に整列され、パディングを必要としない方法で選択されます。


2
そうですが、パディングは存在し、構造体を値または参照で渡すことに依存していません。
イリヤ

2
@dkagedal:「異なるシステムで異なるサイズ」のどの部分を理解できませんでしたか?それがあなたのシステムでそのようになっているからといって、あなたはそれが他のものと同じでなければならないことを仮定します-それがまさにあなたが値で渡すべきではない理由です。システムでも失敗するようにサンプルを変更しました。
Mecki

2
構造体パディングに関するMeckiのコメントは、スタックサイズが問題になる可能性がある組み込みシステムに特に関連していると思います。
zooropa 2009

1
引数の裏側は、構造体が単純な構造体(いくつかのプリミティブ型を含む)である場合、値で渡すとコンパイラーがレジスターを使用してそれを操作できるようになると思います-ポインターを使用する場合、結果は遅いメモリ。それはかなり低レベルになり、これらのヒントのいずれかが重要である場合、ターゲットアーキテクチャにかなり依存します。
kizzx2

1
構造体が小さいか、CPUに多くのレジスターがある(そしてIntel CPUにはない)場合を除いて、データはスタックに残り、それもメモリーであり、他のメモリーと同じくらい高速/低速です。一方、ポインターは常に小さく、単なるポインターであり、頻繁に使用されると、ポインター自体は通常は常にレジスターになります。
メッキー2010

9

あなたの質問はかなりうまくまとめられていると思います。

値で構造体を渡すもう1つの利点は、メモリの所有権が明示的であることです。構造体がヒープからのものであるかどうか、誰がそれを解放する責任を持っているかは不思議ではありません。


9

(大きすぎない)構造体をパラメーターとして、および戻り値として渡すことは、完全に正当な手法です。もちろん、構造体がPODタイプであるか、コピーのセマンティクスが明確に指定されていることに注意する必要があります。

更新:申し訳ありませんが、私はC ++の考え方を身につけていました。Cで関数から構造体を返すことが合法でなかった時期を思い出しますが、それはおそらくそれ以降変更されています。使用する予定のすべてのコンパイラがこのプラクティスをサポートしている限り、それは有効であると私は言います。


私の質問はC ++ではなくCに関するものであったことに注意してください。
dkagedal 2008年

関数から構造体を返すことは有効ではありません:)
Ilya

1
関数からデータを返すためのエラーコードとパラメーターとしてreturnを使用するというllyaの提案が好きです。
zooropa 2009

8

ここに誰も言及していないものがあります:

void examine_data(const char *c, size_t l)
{
    c[0] = 'l'; // compiler error
}

void examine_data(const struct blob blob)
{
    blob.ptr[0] = 'l'; // perfectly legal, quite likely to blow up at runtime
}

aのメンバーはですが、そのメンバーが(のように)ポインターである場合は、実際に望んでいるconst structものconstではありません。もちろん、これは意図のドキュメントであり、これに違反する人はだれでも悪いコードを書いている(そうである)と考えることもできますが、一部の人(特に、クラッシュ)。char *char *constconst char *const

代わりにaを作成してstruct const_blob { const char *c; size_t l }それを使用することもできますが、それはやや厄介です- typedefポインターのingで私が持っているのと同じ命名方式の問題に陥ります。したがって、ほとんどの人は、2つのパラメーター(または、この場合は文字列ライブラリーを使用する可能性が高い)を使用することに固執します。


はい、それは完全に合法であり、あなたが時々したいことでもあります。しかし、それらが指すポインターをconstに指すようにできないのは、構造体ソリューションの制限であることには同意します。
dkagedal 2011

struct const_blob解決策の厄介な落とし穴は、「indirect-const-ness」だけconst_blobが異なるメンバーがあったとしても、厳密なエイリアシングルールの目的で、aへのblob型は異なると見なされることです。コードがキャスト場合、結果として、に、いずれかのタイプを使用して、基礎となる構造への次の書き込みは静かに任意の使用は、(通常は無害であり得るが、致命的であり得る)未定義の動作を起動するように、他のタイプの既存のポインタを無効にします。struct blob*struct const_blob*blob*const_blob*
スーパーキャット2015年

5

PCアセンブリチュートリアルのページ150(http://www.drpaulcarter.com/pcasm/)には、Cが関数が構造体を返すことができるようにする方法についての明確な説明があります。

Cでは、構造体タイプを関数の戻り値として使用することもできます。明らかに、構造はEAXレジスターに戻すことができません。コンパイラーが異なれば、この状況の扱いも異なります。コンパイラが使用する一般的な解決策は、構造体ポインタをパラメータとして取る関数として内部的に関数を書き換えることです。ポインターは、呼び出されたルーチンの外部で定義された構造に戻り値を入れるために使用されます。

次のCコードを使用して、上記のステートメントを確認します。

struct person {
    int no;
    int age;
};

struct person create() {
    struct person jingguo = { .no = 1, .age = 2};
    return jingguo;
}

int main(int argc, const char *argv[]) {
    struct person result;
    result = create();
    return 0;
}

"gcc -S"を使用して、このCコードのアセンブリを生成します。

    .file   "foo.c"
    .text
.globl create
    .type   create, @function
create:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $16, %esp
    movl    8(%ebp), %ecx
    movl    $1, -8(%ebp)
    movl    $2, -4(%ebp)
    movl    -8(%ebp), %eax
    movl    -4(%ebp), %edx
    movl    %eax, (%ecx)
    movl    %edx, 4(%ecx)
    movl    %ecx, %eax
    leave
    ret $4
    .size   create, .-create
.globl main
    .type   main, @function
main:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $20, %esp
    leal    -8(%ebp), %eax
    movl    %eax, (%esp)
    call    create
    subl    $4, %esp
    movl    $0, %eax
    leave
    ret
    .size   main, .-main
    .ident  "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
    .section    .note.GNU-stack,"",@progbits

createを呼び出す前のスタック:

        +---------------------------+
ebp     | saved ebp                 |
        +---------------------------+
ebp-4   | age part of struct person | 
        +---------------------------+
ebp-8   | no part of struct person  |
        +---------------------------+        
ebp-12  |                           |
        +---------------------------+
ebp-16  |                           |
        +---------------------------+
ebp-20  | ebp-8 (address)           |
        +---------------------------+

createを呼び出した直後のスタック:

        +---------------------------+
        | ebp-8 (address)           |
        +---------------------------+
        | return address            |
        +---------------------------+
ebp,esp | saved ebp                 |
        +---------------------------+

2
ここには2つの問題があります。最も明白なのは、これが「Cが関数に構造体を返すことをどのように許可するか」をまったく記述していないことです。これは、32ビットx86ハードウェアでそれを行う方法を説明するだけです。これは、レジスタの数などを見ると、最も制限されたアーキテクチャの1つです。2番目の問題は、Cコンパイラが値を返すコードを生成する方法です。 ABIによって指示されます(エクスポートされない関数またはインライン関数を除く)。ちなみに、インライン化された関数は、おそらく構造体を返すことが最も役立つ場所の1つです。
dkagedal、2011年

訂正ありがとうございます。呼び出し規約の詳細については、en.wikipedia.org / wiki / Calling_conventionが参考になります。
Jingguo Yao

@dkagedal:重要なのは、x86がこのように処理を行うだけでなく、あらゆるプラットフォームのコンパイラが、構造体以外の戻り値をサポートできるようにする「ユニバーサル」アプローチ(つまり、このアプローチ)が存在することです。スタックを爆破するほど巨大です。多くのプラットフォームのコンパイラは、いくつかの構造型戻り値を処理するために他のより効率的な手段を使用しますが、言語が構造戻り型をプラットフォームが最適に処理できるものに制限する必要はありません。
スーパーキャット2018

0

値で構造体を渡すことの1つの利点を指摘したいのは、最適化コンパイラがコードをより最適化できることです。

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