web-dev-qa-db-ja.com

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

ポインターを渡すのではなく、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の場合、長さの入力値がなく、返される長さが必要なので、前に説明した方法で混乱させることはできません。

151
dkagedal

値で渡す小さな構造体(ポイント、四角形など)の場合、完全に受け入れられます。しかし、速度とは別に、値によって大きな構造体を慎重に渡したり戻したりする必要があるもう1つの理由があります。それは、スタックスペースです。

多くのCプログラミングは組み込みシステム用であり、メモリは非常に貴重であり、スタックサイズはKBまたはバイト単位で測定される場合があります。スタック、潜在的に このサイト の名前が付けられている状況を引き起こす...

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

190
Roddy

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

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

参照: http://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html

-fpcc-struct-return

-freg-struct-return

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

64
tonylo

この質問に実際に答えるには、アセンブリーの土地を深く掘り下げる必要があります:

(次の例では、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()より1つ少ない命令を生成します

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

どうやらこれの多くは組み込み環境では適用できない可能性があります(これは、今日ではCの競争の場がほとんどの場合です)。私はアセンブリウィザードではないので、コメントは歓迎します!

20
kizzx2

簡単な解決策は、エラーコードを戻り値として返し、それ以外のすべてを関数のパラメーターとして返すことです。
このパラメーターはもちろん構造体ですが、ポインターを送信するだけで、これを値で渡す特別な利点はありません。
値による構造の受け渡しは危険です。渡すものは非常に注意する必要があります。Cにはコピーコンストラクタがないことに注意してください。構造パラメータの1つがポインタ値である場合、ポインタ値がコピーされる可能性があります非常に混乱し、保守が困難です。

答え( Roddy への完全なクレジット)を完了するために、スタックの使用は値によって構造を渡さないもう1つの理由です。

コメントしてリプレイ:

構造体をポインターで渡すことは、一部のエンティティがこのオブジェクトの所有権を持ち、何をいつリリースすべきかを完全に把握していることを意味します。構造体を値で渡すと、構造体の内部データへの隠された参照(別の構造体へのポインターなど)が作成され、維持するのが難しくなります(可能ですが、なぜですか?)。

14
Ilya

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

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

9
Greg Hewgill

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

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

9
Darron

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

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

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

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

9
Mecki

誰も言及しなかったことがあります:

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
}

const structのメンバーはconstですが、そのメンバーが(char *のような)ポインターの場合、char *constではなくconst char *になります。もちろん、constは意図の文書であり、これに違反する人は誰でも悪いコードを書いていると仮定できますが(それはそうです)、それは一部の人(特に4時間を費やした人)には十分ではありませんクラッシュの原因を追跡します)。

代わりにstruct const_blob { const char *c; size_t l }を作成して使用することもできますが、それはやや面倒です-typedefingポインターで使用しているのと同じ命名体系の問題になります。したがって、ほとんどの人は、2つのパラメーターだけを使用します(または、この場合は文字列ライブラリーを使用します)。

8
Chris Lutz

http://www.drpaulcarter.com/pcasm/ のPC Assembly Tutorialの150ページには、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                 |
        +---------------------------+
5
Jingguo Yao

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

0
Vad