ポインターを渡すのではなく、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の場合、長さの入力値がなく、返される長さが必要なので、前に説明した方法で混乱させることはできません。
値で渡す小さな構造体(ポイント、四角形など)の場合、完全に受け入れられます。しかし、速度とは別に、値によって大きな構造体を慎重に渡したり戻したりする必要があるもう1つの理由があります。それは、スタックスペースです。
多くのCプログラミングは組み込みシステム用であり、メモリは非常に貴重であり、スタックサイズはKBまたはバイト単位で測定される場合があります。スタック、潜在的に このサイト の名前が付けられている状況を引き起こす...
過度にスタックを使用していると思われるアプリケーションを見つけた場合、値で渡される構造体は最初に探すものの1つです。
言及されていないこれを行わない理由の1つは、これによりバイナリ互換性が重要になる問題が発生する可能性があることです。
使用するコンパイラに応じて、コンパイラのオプション/実装に応じて、スタックまたはレジスタを介して構造体を渡すことができます
参照: http://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html
-fpcc-struct-return
-freg-struct-return
2つのコンパイラーが一致しない場合、事態が爆発する可能性があります。言うまでもなく、これを行わない主な理由は、スタックの消費とパフォーマンスの理由です。
この質問に実際に答えるには、アセンブリーの土地を深く掘り下げる必要があります:
(次の例では、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の競争の場がほとんどの場合です)。私はアセンブリウィザードではないので、コメントは歓迎します!
簡単な解決策は、エラーコードを戻り値として返し、それ以外のすべてを関数のパラメーターとして返すことです。
このパラメーターはもちろん構造体ですが、ポインターを送信するだけで、これを値で渡す特別な利点はありません。
値による構造の受け渡しは危険です。渡すものは非常に注意する必要があります。Cにはコピーコンストラクタがないことに注意してください。構造パラメータの1つがポインタ値である場合、ポインタ値がコピーされる可能性があります非常に混乱し、保守が困難です。
答え( Roddy への完全なクレジット)を完了するために、スタックの使用は値によって構造を渡さないもう1つの理由です。
コメントしてリプレイ:
構造体をポインターで渡すことは、一部のエンティティがこのオブジェクトの所有権を持ち、何をいつリリースすべきかを完全に把握していることを意味します。構造体を値で渡すと、構造体の内部データへの隠された参照(別の構造体へのポインターなど)が作成され、維持するのが難しくなります(可能ですが、なぜですか?)。
(大きすぎない)構造体を、パラメーターとしても戻り値としても、値で渡すことは完全に正当な手法です。もちろん、構造体がPOD型であるか、コピーのセマンティクスが適切に指定されていることに注意する必要があります。
更新:申し訳ありませんが、私はC++の思考を制限していました。 Cで関数から構造体を返すことは合法ではなかった時期を思い出しますが、それはおそらくその後変更されました。使用する予定のすべてのコンパイラがこのプラクティスをサポートしている限り、それはまだ有効だと思います。
あなたの質問は物事をかなりうまくまとめていると思います。
構造体を値で渡すことのもう1つの利点は、メモリの所有権が明示的であることです。構造体がヒープからのものであるかどうか、そしてそれを解放する責任を誰が持っているかについて不思議はありません。
ここで人々がこれまで言及するのを忘れていた(または見落としていた)ことの1つは、構造体に通常パディングがあることです!
struct {
short a;
char b;
short c;
char d;
}
すべてのcharは1バイト、すべてのshortは2バイトです。構造体の大きさは?いいえ、6バイトではありません。少なくとも一般的に使用されるシステムではそうではありません。ほとんどのシステムでは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
}
const struct
のメンバーはconst
ですが、そのメンバーが(char *
のような)ポインターの場合、char *const
ではなくconst char *
になります。もちろん、const
は意図の文書であり、これに違反する人は誰でも悪いコードを書いていると仮定できますが(それはそうです)、それは一部の人(特に4時間を費やした人)には十分ではありませんクラッシュの原因を追跡します)。
代わりにstruct const_blob { const char *c; size_t l }
を作成して使用することもできますが、それはやや面倒です-typedef
ingポインターで使用しているのと同じ命名体系の問題になります。したがって、ほとんどの人は、2つのパラメーターだけを使用します(または、この場合は文字列ライブラリーを使用します)。
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 |
+---------------------------+
構造体を値で渡す利点の1つは、最適化コンパイラがコードをより最適化できることです。