web-dev-qa-db-ja.com

strcpy()の戻り値

標準Cライブラリの多くの関数、特に文字列操作用の関数、特にstrcpy()は、次のプロトタイプを共有しています。

char *the_function (char *destination, ...)

これらの関数の戻り値は、実際には提供されたdestinationと同じです。冗長なものの戻り値を無駄にするのはなぜですか?そのような関数が無効であるか、何か有用なものを返す方が理にかなっています。

これがなぜであるかについての私の唯一の推測は、次のように、関数呼び出しを別の式にネストする方が簡単で便利であるということです。

printf("%s\n", strcpy(dst, src));

このイディオムを正当化する他の賢明な理由はありますか?

27

エヴァンが指摘したように、次のようなことをすることが可能です

_char* s = strcpy(malloc(10), "test");
_

例えばヘルパー変数を使用せずに、malloc()edメモリに値を割り当てます。

(この例は最良の例ではありません。メモリ不足の状態でクラッシュしますが、アイデアは明らかです)

21
nothrow

私はあなたの推測が正しいと信じています、それは電話を入れ子にするのをより簡単にします。

5
Evan Teran

char *stpcpy(char *dest, const char *src); 文字列のendへのポインタを返し、POSIX.1-の一部です。 2008 。それ以前は、1992年以来GNU libc拡張機能でした。1986年にLatticeCAmigaDOSに最初に登場した場合。

_gcc -O3_は、場合によってはstrcpy + strcatを最適化してstpcpyまたはstrlen +インラインコピーを使用します。以下を参照してください。


Cの標準ライブラリは非常に初期に設計されており、_str*_関数が最適に設計されていないと主張するのは非常に簡単です。 I/O関数は間違いなく非常に早い段階で、Cがプリプロセッサを搭載する前の1972年に設計されました。これは なぜfopen(3)はモード文字列を使用するのですか? Unix open(2) のようなフラグビットマップの代わりに。

MikeLeskの「ポータブルI/Oパッケージ」に含まれている関数のリストを見つけることができなかったので、現在の形式のstrcpyがそこまでさかのぼるかどうか、またはそれらの関数が後で追加されたかどうかはわかりません。 。 (私が見つけた唯一の本当の情報源は デニスリッチーの広く知られているCの歴史の記事 です。これは優れていますが、そのの深さではありません。 t実際のI/Oパッケージ自体のドキュメントまたはソースコードを見つけます。)

それらは、1978年に K&R初版 、現在の形で表示されます。


関数は、呼び出し元にとって有用である可能性がある場合は、計算結果を破棄するのではなく、返す必要があります。文字列の終わりへのポインタ、または整数の長さのいずれかとして。 (ポインターは自然です。)

@Rが言うように:

これらの関数が終了ヌルバイトへのポインタを返すことを願っています(これにより、多くのO(n)操作がO(1)に削減されます)

例えばループ内でstrcat(bigstr, newstr[i])を呼び出して、多くの短い(O(1)長)文字列から長い文字列を作成するのは、およそO(n^2)の複雑さですが、strlen/memcpyは各文字を2回しか調べません。 (strlenで1回、memcpyで1回)。

ANSI C標準ライブラリのみを使用すると、すべての文字を1回だけ効率的に調べる方法はありません。一度に1バイトずつループを手動で作成することもできますが、数バイトより長い文字列の場合、最新のHWで現在のコンパイラ(検索ループを自動ベクトル化しない)で各文字を2回調べるよりも悪いです。与えられた効率的なlibc提供のSIMDstrlenとmemcpy。 length = sprintf(bigstr, "%s", newstr[i]); bigstr+=length;を使用することもできますが、sprintf()はそのフォーマット文字列を解析する必要があり、高速ではありません

差分位置を返すstrcmpまたはmemcmpのバージョンすらありません。それが必要な場合は、 Pythonで文字列比較が非常に高速なのはなぜですか? :コンパイルされたループで実行できるものよりも高速に実行される最適化されたライブラリ関数(手がない場合) -関心のあるすべてのターゲットプラットフォーム用に最適化されたasm)。これを使用して、近づくと通常のループにフォールバックする前に、異なるバイトに近づくことができます。

Cの文字列ライブラリは、暗黙の長さの文字列の終わりを見つけるだけでなく、操作のO(n)コスト、およびstrcpyの動作を考慮せずに設計されたようです。間違いなく唯一の例ではありません。

基本的に、暗黙の長さの文字列を不透明なオブジェクト全体として扱い、検索または追加後に常に先頭または末尾または内部の位置へのポインタを返します。


歴史の当て推量

PDP-11 の初期のCでは、strcpywhile(*dst++ = *src++) {}よりも効率的ではなかったと思います(おそらくそのように実装されていました)。

実際、 K&R初版(101ページ) は、strcpyの実装を示しており、次のように述べています。

これは一見不可解に思えるかもしれませんが、表記上の利便性はかなりのものであり、Cプログラムで頻繁に見られる以外の理由がなければ、イディオムを習得する必要があります。

これは、dstまたはsrcの最終値が必要な場合に、プログラマーが独自のループを作成することを完全に期待していることを意味します。したがって、手作業で最適化されたasmライブラリ関数用のより便利なAPIを公開するには手遅れになるまで、標準ライブラリAPIを再設計する必要性を認識していなかったのかもしれません。


しかし、dstの元の値を返すことには意味がありますか?

strcpy(dst, src)dstを返すことは、_x=y_がxに評価することに類似しています。したがって、strcpyは文字列代入演算子のように機能します。

他の回答が指摘しているように、これによりfoo( strcpy(buf,input) );のようにネストが可能になります。初期のコンピュータは非常にメモリに制約がありました。 ソースコードをコンパクトに保つ​​ことは一般的な方法でした。パンチカードと遅い端末がおそらくこれの要因でした。歴史的なコーディング標準やスタイルガイド、または1行にまとめるには多すぎると考えられていたものがわかりません。

無愛想な古いコンパイラもおそらく要因でした。最新の最適化コンパイラでは、char *tmp = foo();/bar(tmp);bar(foo());より遅くはありませんが、_gcc -O0_を使用します。非常に初期のコンパイラが変数を完全に最適化できるかどうかはわかりませんが(スタックスペースを予約しないで)、単純なケースでは少なくとも変数をレジスタに保持できることを願っています(意図的にスピル/リロードする最新の_gcc -O0_とは異なります)。一貫したデバッグのためのすべて)。つまり、_gcc -O0_は、一貫性のあるデバッグを目的とした反最適化であるため、古いコンパイラには適していません。


コンパイラによって生成されたasmの動機の可能性

C文字列ライブラリの一般的なAPI設計の効率性に注意が払われていないことを考えると、これはありそうにないかもしれません。しかし、おそらくコードサイズの利点がありました。 (初期のコンピューターでは、コードサイズはCPU時間よりも厳しい制限でした)。

初期のCコンパイラーの品質についてはよくわかりませんが、PDP-11のような単純な直交アーキテクチャーであっても、最適化がうまくいかなかったのは間違いありません。

関数呼び出しの後に文字列ポインタが必要になるのは一般的です。 asmレベルでは、おそらくあなた(コンパイラー)は呼び出しの前にそれをレジスターに持っています。呼び出し規約に応じて、スタックにプッシュするか、呼び出し規約が最初の引数を指定する右側のレジスタにコピーします。 (つまり、strcpyがそれを期待している場所)。または、事前に計画している場合は、呼び出し規約の正しいレジスタにポインタがすでにあります。

しかし、関数呼び出しは、すべての引数受け渡しレジスタを含むいくつかのレジスタをクローバーします。 (したがって、関数がレジスターで引数を取得すると、スクラッチレジスターにコピーする代わりに、そこでインクリメントすることができます。)

したがって、呼び出し元として、関数呼び出し全体で何かを保持するためのcode-genオプションには次のものが含まれます。

  • ローカルスタックメモリに保存/再ロードします。 (または、最新のコピーがまだメモリに残っている場合は、リロードするだけです)。
  • 関数全体の開始/終了時に呼び出し保存レジスターを保存/復元し、関数呼び出しの前にそれらのレジスターの1つへのポインターをコピーします。
  • この関数は、レジスタ内の値を返します。 (もちろん、これは、Cソースが入力変数の戻り値の代わりにを使用するように記述されている場合にのみ機能します。たとえば、ネストしていない場合はdst = strcpy(dst, src);それ)。

すべてのアーキテクチャでのすべての呼び出し規約レジスタ内の戻りポインタサイズの戻り値を知っているので、ライブラリ関数に1つの追加命令があると、その戻り値を使用するすべての呼び出し元のコードサイズを節約できます。

strcpy(すでにレジスターにある)の戻り値を使用することで、コンパイラーに呼び出しの周りのポインターを呼び出し保存レジスターに保存させたり、スタックにスピルさせたりするよりも、プリミティブな初期Cコンパイラーからより良いasmを取得した可能性があります。これはまだ当てはまるかもしれません。

ところで、多くのISAでは、戻り値レジスタは最初の引数受け渡しレジスタではありません。また、ベース+インデックスアドレッシングモードを使用しない限り、strcpyがポインタインクリメントループのレジスタをコピーするために追加の命令が必要になります(そして別のレジスタを拘束します)。

PDP-11ツールチェーン 通常はある種のスタック引数呼び出し規約を使用 、常に引数をスタックにプッシュします。正常な呼び出し保存レジスタと呼び出しクローバーレジスタの数はわかりませんが、使用できるGPレジスタは5つまたは6つだけでした( R7はプログラムカウンタ、R6はスタックポインタ、R5はよく使用されるフレームポインタ )。つまり、32ビットx86に似ていますが、さらに窮屈です。

_char *bar(char *dst, const char *str1, const char *str2)
{
    //return strcat(strcat(strcpy(dst, str1), "separator"), str2);

    // more readable to modern eyes:
    dst = strcpy(dst, str1);
    dst = strcat(dst, "separator");
//    dst = strcat(dst, str2);

    return dst;  // simulates further use of dst
}

  # x86 32-bit gcc output, optimized for size (not speed)
  # gcc8.1 -Os  -fverbose-asm -m32
  # input args are on the stack, above the return address

    Push    ebp     #
    mov     ebp, esp  #,      Create a stack frame.

    sub     esp, 16   #,      This looks like a missed optimization, wasted insn
    Push    DWORD PTR [ebp+12]      # str1
    Push    DWORD PTR [ebp+8]       # dst
    call    strcpy  #
    add     esp, 16   #,

    mov     DWORD PTR [ebp+12], OFFSET FLAT:.LC0      # store new args over our incoming args
    mov     DWORD PTR [ebp+8], eax    #  EAX = dst.
    leave   
    jmp     strcat                  # optimized tailcall of the last strcat
_

これは、_dst =_を使用しないバージョンよりも大幅にコンパクトであり、代わりにstrcatの入力引数を再利用します。 (Godboltコンパイラエクスプローラの両方を参照してください 。)

_-O3_の出力は大きく異なります。戻り値を使用しないバージョンのgccは、stpcpy(テールへのポインターを返します)を使用し、次にmov- immediateを使用してリテラル文字列データを適切な場所に直接格納します。

ただし、残念ながら、dst = strcpy(dst, src) -O3バージョンは引き続き通常のstrcpyを使用し、strcatstrlen + mov- immediateとしてインライン化します。


CストリングにするかしないかCストリングにする

Cの暗黙の長さの文字列は、常に本質的に悪いわけではなく、興味深い利点があります(たとえば、接尾辞もコピーせずに有効な文字列です)。

ただし、C文字列ライブラリは効率的なコードを可能にするように設計されていません。これは、char- at-a-timeループは通常自動ベクトル化せず、ライブラリ関数は実行する必要のある作業の結果を破棄するためです。

gccとclangは、最初の反復の前に反復カウントがわかっていない限り、ループを自動ベクトル化することはありません。 for(int i=0; i<n ;i++)。 ICCは検索ループをベクトル化できますが、手書きのasmほどうまくいく可能性はまだありません。


strncpyなどは基本的に災害です。例えばstrncpyは、バッファサイズの制限に達した場合、終了する_'\0'_をコピーしません。大きな文字列の途中に書き込むように設計されているようですが、バッファオーバーフローを回避するためにnotではありません。最後にポインタを返さないということは、前後に_arr[n] = 0;_する必要があることを意味し、触れる必要のないメモリのページに触れる可能性があります。

snprintfのようないくつかの関数は使用可能であり、常にヌル終了します。どちらを行うかを覚えるのは難しく、間違ったことを覚えていると大きなリスクがあります。そのため、正確性が重要な場合は毎回チェックする必要があります。

ブルース・ドーソンが言うように: すでにstrncpyの使用をやめなさい! 。どうやら__snprintf_のようないくつかのMSVC拡張機能はさらに悪いです。

2
Peter Cordes

また、コーディングも非常に簡単です。

戻り値は通常、AXレジスタに残されます(必須ではありませんが、多くの場合そうです)。そして、関数の開始時に宛先がAXレジスタに格納されます。宛先を返すために、プログラマーは何もする必要がありません....まったく何もしません!値をそのままにしておきます。

プログラマーは関数をvoidとして宣言できます。しかし、その戻り値はすでに適切な場所にあり、返されるのを待っているだけであり、それを返すための追加の指示も必要ありません!どんなに小さな改善でも、場合によっては便利です。

1
abelenky

Fluent Interfaces と同じ概念。コードをより速く/読みやすくするだけです。

0
palswim