2016年2月19日
2016年、C言語はどう書くべきか (後編)
(2016-01-07)by Matt Stancliff
本記事は、原著者の許諾のもとに翻訳・掲載しております。
(前編はこちら: 2016年、C言語はどう書くべきか (前編) )
(編注:2020/08/18、いただいたフィードバックをもとに記事を修正いたしました。)
システム依存の型
まだ「32 bitのプラットフォームでは32 bitのlong型、64 bitのプラットフォームでは64 bitのlong型がいい」という不満があるようですね。
プラットフォームに依存する2つの異なるサイズを使うため、 故意に コードを難しくすることを考えたくなければ、システム依存の型のために long
を使おうとは思わないでしょう。
この状況では、プラットフォームのためにポインタ値を保持する整数型、 intptr_t
を使うべきです。
モダン32-bitプラットフォームでは、 intptr_t
は int32_t
です。
モダン64-bitプラットフォームでは、 intptr_t
は int64_t
です。
intptr_t
は uintptr_t
の形でもあります。
ポインタのオフセットを保持するためには、名が体を表す通りの ptrdiff_t
を使います。これは、ポインタの差分の値を格納するのに適した型です。
最大値ホルダー
システム内で使える全ての整数を保持できる整数型が必要な場合はどうしますか。
この場合、より小さな符号なし整数型を uint64_t
にキャストするなど、可能な限り最大の型を選びがちですが、さらに規則正しく、どんな値でもその他のあらゆる値を確実に保持させ得る方法があります。
全ての整数にとって最も安全なコンテナは intmax_t
(または uintmax_t
)です。あらゆる符号付き整数を精度を損ねることなく intmax_t
にキャストしたり、割り当てたりすることができます。さらに、どのような符号なし整数も uintmax_t
に精度を損ねることなくキャスト、割り当てすることも可能です。
その他の型
最も頻繁に使われるシステム依存の型は、 size_t
で、 stddef.h に定義されています。
size_t
は基本的に「最大の配列インデックスを保持できる整数」ですが、プログラム内の最大のメモリオフセットを保持が可能という意味もあります。
実際の用途としては、 size_t
は sizeof
オペレータの戻り値の型です。
いずれにしても、 size_t
は全てのモダンプラットフォーム上の uintptr_t
と同様になるよう、 実用的に 定義されます。そのため、32-bitプラットフォームでは、 size_t
は uint32_t
であり、64-bitプラットフォームでは、 size_t
は uint64_t
です。
他にも、 ssize_t
があり、これはライブラリ関数からの返り値として使われる符号付きの size_t
で、エラーの際に -1
を返します(注: ssize_t
はPOSIXであり、Windowsインターフェースには適用されません)。
では、あなたの関数パラメータ内で、任意のシステム依存型のサイズのための size_t
を使うべきでしょうか。技術的には、 size_t
は sizeof
の戻り値の型なので、バイト数を表したサイズ値を受け取る関数は全て size_t
に変換できます。
他の用法には次のものが含まれます。: size_t
はmallocに渡される引数の型であり、 ssize_t
は read()
と write()
の戻り値の型です( ssize_t
が存在せず、戻り値がただの int
であるWindowsの場合を除きます)。
出力の型
決して出力中に型をキャストしてはいけません。
常に、 inttypes.h で定義された適切な型指定子を使いましょう。
それには次のものが含まれますが、この限りではありません。
size_t
–%zu
ssize_t
–%zd
ptrdiff_t
–%td
- 未加工のポインタ値 –
%p
(モダンコンパイラでは16進で出力されます。つまり、最初にポインタを(void *)
にキャストします) int64_t
–"%" PRId64
uint64_t
–"%" PRIu64
- 64-bit型は
PRI[udixXo]64
スタイルのマクロで出力しなけれななりません。 - 理由:
- プラットフォームには、64-bit値が
long
であるものもあれば、long long
のものもあるため。 これらのマクロは、プラットフォームを問わず根底をなす適切なフォーマット仕様を提供します。 - これらのフォーマットマクロなしに、プラットフォームを横断する正しい文字列を明示することは実際、不可能です。なぜなら、型はあなた次第で変わるからです(そして、出力前に値をキャストすることは安全でも理論的でもないことを覚えておきましょう)。
- 64-bit型は
intptr_t
—"%" PRIdPTR
uintptr_t
—"%" PRIuPTR
intmax_t
—"%" PRIdMAX
uintmax_t
—"%" PRIuMAX
PRI*
フォーマット指定子に関する覚書:これらは マクロ であり、マクロはプラットフォーム固有の基盤上の適切なprintf型指定子まで範囲を広げられます。つまり、次のようにはできないということです。
printf("Local number: %PRIdPTR\n\n", someIntPtr);
しかし代わりに、マクロであるがゆえに、次の記述が可能です。
printf("Local number: %" PRIdPTR "\n\n", someIntPtr);
%
はフォーマット文字列リテラルの 中に 置きましょう。ただし、型指定子はフォーマット文字列リテラルの 外に 置きます。なぜなら、全ての隣接した文字列はプリプロセッサによって1つのまとまった最終的な文字列に連結されるためです。
C99では、どこででも変数を定義できる
ですから、次のようにしないことです。
void test(uint8_t input) {
uint32_t b;
if (input > 3) {
return;
}
b = input;
}
代わりにこうしましょう。
void test(uint8_t input) {
if (input > 3) {
return;
}
uint32_t b = input;
}
注意:緊密なループがある場合、イニシャライザの配置をテストしてください。時に、分散された宣言によって予期せぬスローダウンが起こることがあります。通常の非ファストパスコード(世界のほとんど全て)は、可能な限り明確にするのが大事です。また初期化のそばで型を定義することで、可読性は大きく向上します。
C99では、 for
ループでインラインにカウンタを定義できる
ですから、次のようにしてはいけません。
uint32_t i;
for (i = 0; i < 10; i++)
必ず以下の通りにしましょう。
for (uint32_t i = 0; i < 10; i++)
1つの例外:ループを出た後もカウンタ値を保つ必要がある場合はもちろん、ループ範囲内にカウンタを指定しないようにします。
モダンコンパイラが #pragma once
をサポート
よって、次のようにしてはいけません。
#ifndef PROJECT_HEADERNAME
#define PROJECT_HEADERNAME
.
.
.
#endif /* PROJECT_HEADERNAME */
ただ、以下のように記述します。
#pragma once
#pragma once
はコンパイラに、ヘッダを一度だけincludeさせるので、3行のヘッダガードを書く必要が なくなりました 。このプラグマはあらゆるプラットフォーム上のあらゆるコンパイラを広くサポートしていますので、手動のヘッダガード作成よりも推奨されます。
詳細は、 pragma once でサポートしているコンパイラのリストを参照してください。
C言語では自動割り当て配列の静的初期化が可能
ですから、次のようにしないことです。
uint32_t numbers[64];
memset(numbers, 0, sizeof(numbers));
代わりにこうしましょう。
uint32_t numbers[64] = {0};
C言語では自動割り当て構造体の静的初期化が可能
ですから、次のようにしてはいけません。
struct thing {
uint64_t index;
uint32_t counter;
};
struct thing localThing;
void initThing(void) {
memset(&localThing, 0, sizeof(localThing));
}
代わりにこうしましょう。
struct thing {
uint64_t index;
uint32_t counter;
};
struct thing localThing = {0};
重要なメモ: パディングを持つ構造体の場合は、 {0}
メソッドは余分なパディングのバイト数をゼロに初期化しません。例えば、 struct thing
には、ワードサイズでのインクリメントを考慮するために、 counter
の後に4バイトのパディングを持っています(64-bitプラットフォームの場合)。未使用のパディングも 含め 、構造体全体をゼロにする必要がある時は、 memset(&localThing, 0, sizeof(localThing))
を使いましょう。アクセス可能なコンテンツが 8 + 4 = 12 bytes
のみだったとしても、 sizeof(localThing) == 16 bytes
だからです。
既に割り当てられた構造体を再度初期化する場合は、後の代入のために、グローバルなゼロ構造体を宣言してください。
struct thing {
uint64_t index;
uint32_t counter;
};
static const struct thing localThingNull = {0};
.
.
.
struct thing localThing = {.counter = 3};
.
.
.
localThing = localThingNull;
幸運にも、C99(あるいはそれ以降)の環境がある場合は、グローバルな”ゼロ構造体”を保持する代わりに、複合リテラルが使えます(2001からの The New C: Compound Literals も参照してください)。
複合リテラルを使うことで、コンパイラで一時的に匿名構造体を作ってから、ターゲット値にコピーすることもできます。
localThing = (struct thing){0};
C99の可変長配列機能(Variable Length Alleys)(C11ではオプショナル)
下記のようにしてはいけません(配列が小さいことがわかっている、あるいは何かをすぐにテストしたい場合)。
uintmax_t arrayLength = strtoumax(argv[1], NULL, 10);
void *array[];
array = malloc(sizeof(*array) * arrayLength);
/* remember to free(array) when you're done using it */
代わりに次のようにします。
uintmax_t arrayLength = strtoumax(argv[1], NULL, 10);
void *array[arrayLength];
/* no need to free array */
重要: 可変長配列(VLA)は(たいてい)、通常の配列のようにまとめて割り当てられます。300万の要素を持つ通常の配列を静的に作るのでなければ、この構文で実行中に300万の要素の配列を作ろうとしないことです。これらはスケーラブルなpython/rubyの自動拡張リストではありません。実行時の配列の長さを明示し、その長さがスタックに対して大き過ぎる場合、あなたのプログラムは恐ろしいことを引き起こすでしょう(クラッシュやセキュリティ問題)。VLAは小さく、目的が1つだけの状況の場合は便利ですが、プロダクションソフトウェアで、そのスケールに頼るべきではありません。ある時は3要素の配列を使い、またある時は300万要素の配列が必要という場合は、当然、可変長配列機能を使ってはいけません。
VLAが動いているのに出くわした場合(あるいは、短い単発テストをしたい場合)にVLA構文を意識することは大事です。しかしそれはほとんど 危険なアンチパターン とされています。なぜなら、要素サイズの限度のチェックをしなかったり、空きスタックスペースのない間違った対象プラットフォームにいることを忘れたりするだけでプログラムはクラッシュし得るからです。
覚書:この状況では arrayLength
は合理的なサイズです(よって、数KB以下の場合、奇妙なプラットフォームではスタックは4KBで最大値を超えることがあります)。巨大な配列(100万のエントリなど)にスタックを割り当てることはできませんが、配列の要素数が限られていることを分かっていれば、 C99 VLA 機能の利用は、mallocを使って手動でヒープメモリをリクエストするよりもずっと容易です。
覚書 続き:上述の機能はユーザの入力をチェックすることはできません。そのためユーザは巨大なVLAを割り当てて、いとも簡単にプログラムを壊してしまいます。中には、VLAをアンチパターンと呼ぶところまで 行ってしまった人もいますが 、範囲を厳しくすれば、特定の状況では小さな勝利が得られます。
C99では非重複ポインタパラメータに注釈をつけられる
restrictキーワード ( __restrict
であることが多い)を見てください。
パラメータの型
関数が 任意の 入力データと長さの処理を受け付けるなら、パラメータの型は制限しないようにしましょう。
そこで、次のようには記述しません。
void processAddBytesOverflow(uint8_t *bytes, uint32_t len) {
for (uint32_t i = 0; i < len; i++) {
bytes[0] += bytes[i];
}
}
代わりにこのようにします。
void processAddBytesOverflow(void *input, uint32_t len) {
uint8_t *bytes = input;
for (uint32_t i = 0; i < len; i++) {
bytes[0] += bytes[i];
}
}
関数に対する入力の型は、パラメータでコードが何をするかではなく、コード内の インターフェース を表します。このコードへのインターフェースが意味するのは「バイト配列1つと長さ1つを受け入れよ」ということですから、呼び出しをuint8_tのみに制限しないほうがよいのです。ユーザは旧スタイルの char *
型の値やその他予期せぬ方法でデータを渡そうとするかもしれません。
入力の型を void *
として定義し、関数内で実際に使いたい型に再代入または再キャストすることで、その関数を使おうとするユーザはあなた自身のライブラリ 内 の抽象化について考えずに済みます。
この例において、アライメントの問題について指摘する読者がいましたが、ここでは1バイトの要素の入力にアクセスしているので、問題ありません。もし、そうではなく複数のバイト数に入力をキャストするなら、アライメントに気を配る必要があります。プラットフォームを横断するアライメントを扱う別の記述方法は、 非アライメントのメモリアクセス を参照してください(改めて注:この包括的な概要のページは、複雑なアーキテクチャのC言語を扱うものではありません。どの例においても、活用の際はここに書かれている以上の知識や経験が必要です)。
戻り値の型
C99では <stdbool.h>
を活用できます。これは true
を 1
に、 false
を 0
に定義するものです。
成功/失敗の戻り値として、関数は true
または false
を返すべきです。 int32_t
の型にして、手動で 1
と 0
を指定するべきではありません(さらにひどいのは、 1
と -1
(または 0
が成功で、 1
がエラー、 0
が成功で -1
がエラー、など)。
関数が入力パラメータを、パラメータが無効になるほどに変えてしまうなら、変更ポインタを戻す代わりに、API全体で、入力が無効になる可能性があるところ全て、パラメータとしてポインタへのポインタを強制しなければなりません。「ある呼び出しの場合は、戻り値は入力を無効にする」というルールでコーディングすると、大人数が使う場合にエラーを出しやすくなります。
ですから、次のような記述は避けましょう。
void *growthOptional(void *grow, size_t currentLen, size_t newLen) {
if (newLen > currentLen) {
void *newGrow = realloc(grow, newLen);
if (newGrow) {
/* resize success */
grow = newGrow;
} else {
/* resize failed, free existing and signal failure through NULL */
free(grow);
grow = NULL;
}
}
return grow;
}
代わりに下記のようにします。
/* Return value:
* - 'true' if newLen > currentLen and attempted to grow
* - 'true' does not signify success here, the success is still in '*_grow'
* - 'false' if newLen <= currentLen */
bool growthOptional(void **_grow, size_t currentLen, size_t newLen) {
void *grow = *_grow;
if (newLen > currentLen) {
void *newGrow = realloc(grow, newLen);
if (newGrow) {
/* resize success */
*_grow = newGrow;
return true;
}
/* resize failure */
free(grow);
*_grow = NULL;
/* for this function,
* 'true' doesn't mean success, it means 'attempted grow' */
return true;
}
return false;
}
さらに良い方法は以下の通りです。
typedef enum growthResult {
GROWTH_RESULT_SUCCESS = 1,
GROWTH_RESULT_FAILURE_GROW_NOT_NECESSARY,
GROWTH_RESULT_FAILURE_ALLOCATION_FAILED
} growthResult;
growthResult growthOptional(void **_grow, size_t currentLen, size_t newLen) {
void *grow = *_grow;
if (newLen > currentLen) {
void *newGrow = realloc(grow, newLen);
if (newGrow) {
/* resize success */
*_grow = newGrow;
return GROWTH_RESULT_SUCCESS;
}
/* resize failure, don't remove data because we can signal error */
return GROWTH_RESULT_FAILURE_ALLOCATION_FAILED;
}
return GROWTH_RESULT_FAILURE_GROW_NOT_NECESSARY;
}
フォーマット
コーディングスタイルは、非常に重要ながら同時に全くの無意味でもあります。
プロジェクトに、50ページのコーディングスタイルガイドラインがあるなら、誰も手を貸そうとしないでしょう。しかし、あなたのコードが読めない場合は、誰も 手を貸したい と思わないでしょう。
ここで紹介する解決法は 常に 自動コードフォーマッタを使うことです。
2016年のC言語フォーマッタで唯一使えるのは、 clang-format です。clang-formatは自動C言語フォーマッタの中でも最善のデフォルト機能を備えており、現在もなお活発に開発が進められています。
これが、良いパラメータでclang-formatを走らせる私の好きなスクリプトです。
#!/usr/bin/env bash
clang-format -style="{BasedOnStyle: llvm, IndentWidth: 4, AllowShortFunctionsOnASingleLine: None, KeepEmptyLinesAtTheStartOfBlocks: false}" "$@"
これを次のように呼び出します(スクリプトに cleanup-format
と名付けたと仮定して)。
matt@foo:~/repos/badcode% cleanup-format -i *.{c,h,cc,cpp,hpp,cxx}
-i
オプションは、新規ファイルに書いたり、バックアップファイルを作成したりするのではなく、フォーマッティング変更として既存ファイルを上書きします。
ファイルが多数ある場合は、並行して、全体のソースツリーを再帰的に処理することができます。
#!/usr/bin/env bash
# note: clang-tidy only accepts one file at a time, but we can run it
# parallel against disjoint collections at once.
find . \( -name \*.c -or -name \*.cpp -or -name \*.cc \) |xargs -n1 -P4 cleanup-tidy
# clang-format accepts multiple files during one run, but let's limit it to 12
# here so we (hopefully) avoid excessive memory usage.
find . \( -name \*.c -or -name \*.cpp -or -name \*.cc -or -name \*.h \) |xargs -n12 -P4 cleanup-format -i
他にもcleanup-tidyというスクリプトがあります。 cleanup-tidy
のコンテンツは下記です。
#!/usr/bin/env bash
clang-tidy \
-fix \
-fix-errors \
-header-filter=.* \
--checks=readability-braces-around-statements,misc-macro-parentheses \
$1 \
-- -I.
clang-tidy はポリシー主導のコードリファクタリングツールです。上記のオプションは、2つの解決法を可能にします。
readability-braces-around-statements
– 全てのif
/while
/for
文の本体が中括弧内に格納されるように強制します。- ループ構造や条件の後に処理が1文の場合、中括弧なしの宣言が可能なのは、C言語の歴史における事故です。モダンコードは、全てのループと条件に中括弧を強制しない限り、 実行不可能 です。「でも、コンパイラは受け付けてくれる」と反論しても、コードの可読性、メンテナンスのしやすさ、解読しやすさ、閲覧性とは何の関わりもありません。コンパイラのためではなく、将来、そもそもこれらがなぜ存在しているのかも忘れられたずっと後に、現在のあなたの脳の状態をメンテナンスしなければならない人々のためにプログラミングしているのです。
misc-macro-parentheses
– マクロ本体に使われた全てのパラメータの周囲に自動的に小括弧を追加します。
clang-tidy
は、うまく機能すれば非常に便利ですが、複雑なコードベースにおいては動かなくなる場合があります。また、 clang-tidy
は フォーマットしません 。そのため、新しい中括弧をアライメントさせ、マクロの書式を揃えた後で clang-format
を走らせなければなりません。
可読性
ここで筆が止まり始めました…。
コメント
コードファイルの論理的な内包部分
ファイル構造
行数を1,000行(最悪でも1,500行)に限定しましょう。静的関数をテストするためなどの理由で、テストがソースファイルのインラインにあるなら、適宜調整します。
その他の考え方
malloc
の使用を避けよう
常に calloc
を使うべきです。メモリをゼロで初期化しても、パフォーマンスのペナルティはありません。 calloc(object count, size per object)
の関数プロトタイプが好きではないなら、 #define mycalloc(N) calloc(1, N)
でラップすることもできます。
この件についての読者のコメントです。
calloc
は、 大きな 割り当ての場合、パフォーマンスに 確実に影響する。calloc
は、特殊なプラットフォーム(最小限の埋め込みシステム、ゲームコンソール、30年前のハードウェアなど)の場合、パフォーマンスに 確実に影響する。calloc(element count, size of each element)
をラップすることが良い方法とは限らない。malloc()
を避けるべき理由は、それが整数のオーバーフローをチェックできず、セキュリティリスクを抱えていることである。calloc
の割り当てによって、自動的に0
に初期化されてしまう。そのため、valgrindが、初期化されていないメモリの意図しない読み込みやコピーを警告し損ねてしまう。
どれも良い指摘です。だからこそ、コンパイラ、プラットフォーム、オペレーティングシステム、そしてハードウェアデバイスに渡って、常に速度のパフォーマンステスト、リグレッションテストを行わねばなりません。
ラッパーを用いず、 calloc()
を直接使う1つの利点は、 malloc()
とは異なり、 calloc()
は最終の割り当てサイズを取得するために引数を掛け合わせるので、整数のオーバーフローをチェックできることです。小さな割り当てのみを行うのであれば、 calloc()
のラップで機能するでしょう。無制限のデータの流れを割り当てる可能性がある場合は、正規の calloc(element count, size of each element)
呼び出し規約を保持するとよいかもしれません。
何にでも適用できる助言はありませんが、一般的な推奨事項を 完全に漏れなく 明確にしようとすると、結局、言語仕様書を読んでいるのと変わりません。
calloc()
がどのようにメモリをクリアにするかは、次の記事を参照してください。
それでもなお私は、2016年におけるほとんどの一般的なシナリオ(仮定:x64対象プラットフォーム、人間のゲノムでなく、身長程度のサイズのデータ)に対して、常に calloc()
を使うことを薦めたいと思います。「想定」からの逸脱は、「領域知識」に対する絶望をもたらします。どちらも今日び使わない言葉です。
追記: calloc()
により前もってゼロにされたメモリは、一度きりの対処法です。 calloc()
の割り当てを realloc()
すると、reallocによって拡張されたメモリは新規にゼロに初期化されたメモリではありません。拡大した割り当てには、kernelが提供する一般的な未初期化のコンテンツで埋められています。reallocの後にメモリをゼロにする必要がある場合は、手動で拡大した割り当ての範囲を memset()
しなければなりません。
memset関数の使用も避けよう(可能なら)
静的に構造体(または配列)をゼロに初期化できる場合は、クリアにする際、あるいは、インラインの複合文字列を代入するか、またはグローバルなゼロ埋めされた構造体を代入するかして、リセットできる時は、 memset(ptr, 0, len)
を使ってはいけません。
しかし、パディングバイトを含めて構造体をゼロに初期化する必要があり、 memset()
が唯一の方法である場合は別です。
もっと知りたい方に
固定長の整数型(C99以降) も見てください。
Appleの 「コードを64-bitクリーンにする」 も見てください。
アーキテクチャごとのC言語の型のサイズ も見てください。- あなたが書くコード全行のためにテーブル全体を頭で覚えておくのではなく、明確に定義された整数幅を使うべきです。char/short/int/longのビルトインの記憶領域型も使わないように。
size_tとptrdiff_t も見てください。
セキュアコーディング も見てください。全てを完璧に書きたいと本当に思うなら、ただ彼らのシンプルな1,000の例を覚えてしまいましょう。
InriaのJens Gustedtによる Modern C も見てください。
C/C++の文字リテラル、文字列リテラルを理解する で、C11におけるUnicodeサポートの詳細を参照してください。
最後に
どこでも通用する正しいコードを書くことは元来不可能です。たとえ、RAMにおけるランダムなビット反転や、未知の確率で誤った情報を送ってくるブロックデバイスなどを考慮しないとしても、複数のオペレーティングシステム、ランタイム、ライブラリ、ハードウェアプラットフォームがあり、心配は尽きません。
私たちにできる最善の方法は、シンプルで理解しやすいコードを書き、無目的な記述や、解読できないが、なぜか機能しているといった状態を極力減らすことでしょう。
出典
この記事はtwitterとHN経由で広まり、多くの人たちが不備や、私の偏った考えを指摘してくれました。
まず、Jeremy Fallerと Sos Sosowski とMartin Heistermann、その他の人たちは、私の memset()
のサンプルが壊れていたことを指摘し、適切な修正をしてくれました。
Martin Heistermannからはさらに、 localThing = localThingNull
サンプルが壊れていたとの指摘あり。
「もし避けられるならC言語を使うな」という冒頭の引用は、知的なインターネットの賢人 @badboy_ から。
Remi Gacogne からは私が -Wextra
を忘れている、との指摘あり。
Levi Pearson から、gcc-5デフォルトをc89でなく、gnu11にし、デフォルトのclangモードを明確にするよう指摘あり。
Christoper から、 -O2
対 -O3
の項が明確ではないと指摘あり。
Chad Miller からは、私のclang-formatスクリプトのパラメータの使い方が適当過ぎる、と指摘あり。
大勢 が、 calloc()
の助言は 常に 良策とは言えないという指摘あり。例えば極端な環境や、標準から外れたハードウェア(最小限の埋め込みシステム、ゲームコンソール、30年前のハードウェアなど)の場合など。
Charles Randolphから、”Building”の誤植の指摘あり。
Sven Neuhausも、私には”initialization”や”initializers”のスペルの処理能力がないことを指摘してくれました(さらに、その指摘を反映させたつもりが、再度”initialization”を誤っていたことも)。
Colm MacCárthaigh からは、 #pragma once
の記述がないと指摘あり。
Jeffrey Yasskin からは、厳しいエイリアシングも止めるべき(主にgcc最適化)と指摘あり。
Jeffrey Yasskinはさらに、 -fno-strict-aliasing
に関する項で、より良い説明文を提供してくれています。
Chris Palmer とその他数人から、calloc-vs-mallocパラメータの利点、 calloc()
のラッパーを書くことによって起こる障害について指摘あり。第一に、 calloc()
は malloc()
よりもセキュアなインターフェースである、ということでした。
Damien Sorressoは、 calloc()
呼び出しでメモリがゼロに初期化された後、 realloc()
でメモリを再確保した場合、増えた分のメモリはゼロに初期化されないことを、人々に忠告したほうがいいと、指摘してくれました。
Pat Pogsonは、私が”declare”のスペルも間違えていると指摘してくれました。
@TopShibe は、私が挙げた例がグローバル変数だったので、スタック割り当ての初期化の例は間違っていると、指摘してくれました。そこで、スタック領域またはデータセクションに「自動割り当て」されることを意味するように言葉を変更しました。
Jonathan Grynspan は、VLAは 絶対 使い方を誤ると危険なので、VLAの例に関する説明の表現を厳しくするよう提案してくれました。
David O’Mahony氏は、ご親切にも”specify”のスペルも間違っていると指摘してくれました。
David Alan Gilbert博士は、 ssize_t
はPOSIXなので、Windowsにはないと指摘してくれました。
Chris Riddは、C99は1999年策定のC言語であり、C11は2011年策定のC言語であることを明示したほうがいいと提案してくれました。そうでないと、11が99よりも新しいのは奇妙に見えるからです。
Chris Riddはまた、 clang-format
の例で、あいまいな命名規則が使用されていることに気付き、全ての例にわたってもっと一貫性を保つように提案してくれました。
Anthony Le Goff は、私たちに、 Modern C という本1冊分の最新のC言語の考え方や処方箋に目を向けさせてくれました。
Stuart Popejoyは”deliberately”のスペルが本当に間違っていると指摘してくれました。
jack rosenは、”exists”という単語の使い方が、私が意図した”exits”を意味していないと指摘してくれました。
Jo Boothは、私が”compatibility”を”compatability”のようにつづるのが好きだと指摘してくれました。そのほうが論理的に見えますが、英語を使う人たちは賛同してくれないようです。
Stephen Andersonは、私の異常な”stil”というスペルを”still”にデコードしてくれました。
Richard Weinbergerは、構造体を {0}
で初期化してもパディングはゼロに初期化されないので、 {0}
の構造体を回線を通じて送信すると、値が定まっていない構造体にある意図していなかったバイトがリークする可能性があると指摘してくれました。
@JayBhukhanwala は、「戻り値の型」の項の関数のコメントが不正確だと指摘してくれました。私がコードを変更したときに、コメントを更新していなかったからです(皆さんもよくあるでしょう)。
Lorenzoは、「パラメータの型」の項の中で、異なるプラットフォーム間でアライメントの問題が起こる可能性について、はっきりと警告したほうがいいと指摘してくれました。
Paolo G. Giarrusso は、前回追加したアライメントの警告は、私が挙げた例に関してさらに正しいと、再度はっきりさせてくれました。
Fabian Klötzlは、構造体の複合リテラルの有効な代入例を提供してくれました。それは私が今まで出会ったことのない完全に有効な構文です。
Omkar Ekboteは、”platform”、”actually”、”defining”、”experience”、”simultaneously”、”readability”などのタイプミスと矛盾点と顕著に意味不明な言い回しを徹底的に直してくれました。
Carlo Bellettiniは、異常な単語のスペルミスを修正してくれました。
Keith S Thompson は、すばらしい記事 how-to-c-response の中で、技術的な誤りを訂正してくれました。
Marc Bevandは、 fprintf
の型指定子は inttypes.h で定義されていることを述べたほうがいいと指摘してくれました。
Brian Cain は、 fast 型と least 型についても言及したほうがいいと指摘してくれました。
多くのredditユーザは、この記事のどこかに当初間違って #import
があったので、怒ってしまいました。この記事は、1年間ドラフトのまま、改訂もレビューもされずに配信したので、キレた方、すみません。今は改善されています。
また、何人かに、静的初期化の例で、とにかくデフォルトで必ずゼロに初期化される(静的に割り当てられているので、初期化すらされない)グローバル変数を使っていると指摘されました。これは私の例の選択がお粗末だったせいですが、コンセプトは関数のスコープの典型的な用法をなお表しています。例は、汎用的な”コードスニペット”となるように作られており、必ずしも最上位のグローバル変数というわけではありません。
数人の方は、この記事を「私はC言語が嫌いです」と解釈したようですが、そうでありません。C言語は間違った使い方をすると危険です(広くデプロイされるときには、十分なテストと経験が必要)。ですから、逆説的に言えば、C言語の開発者は、2種類に限られるべきです。一方は、ただの初心者の愛好家(コードの失敗は問題を引き起こしません。ただのおもちゃです)で、もう一方は、喜んで一生懸命テストする人(コードの失敗は生命や経済の損失を引き起こします。ただのおもちゃではありません)で、製品のためにC言語のコードを書いている人です。「うわべだけのC言語開発」をする余地はありません。それが、世界中でErlangが使われている理由です。
また、多くの人が自分にとって重要な同様の問題または、この記事の範囲を超えた問題(私たちにC11のジェネリックの能力を思い出させてくれた George Makrydakis のように新しいC11だけの機能を含めて)にも言及してくれました。
さらに”Practical C”に関する記事では、おそらく、テスト、プロファイリング、パフォーマンスのトレース、有用だが任意の警告レベルなどを取り上げて配信します。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa