2016年2月18日
2016年、C言語はどう書くべきか (前編)
(2016-01-07)by Matt Stancliff
本記事は、原著者の許諾のもとに翻訳・掲載しております。
(訳注:2016/3/2、いただいた翻訳フィードバックをもとに記事を修正いたしました。)
(訳注:著者のMattより、「本文中で明言はしていないが、この記事の内容はx86-64 Unix/Linux/POSIXでアプリケーションをプログラミングする場合にフォーカスしている。他のプログラミング領域では、対象とするシステムに応じた(例: 8-bitの組み込みシステム、10年前のコンパイラ、多くの異なるCPUアーキテクチャで動く必要のあるアプリケーション、Win/Linuxでのビルド互換性など)特有のアドバイスが必要」との補足を頂いております。)
以下の文章は2015年の始めに書いたドラフトで、今まで公開していませんでした。私のドラフト用フォルダの中で誰の目も引かなかったため、大部分が書いた時のままです。公開するにあたり、単純に2015年を2016年に変更しました。
必要な修正、改善、苦情がありましたら、いつでも Matt までご連絡ください。
Adrián Arroyo Calle が、 ¿Cómo programar en C (en 2016)? で、スペイン語版を公開しています。
Keith Thompson が、 howto-c-response で、一連の修正と代替案を出しています。
Rob Graham が、 Some notes C in 2016 で、ここのスコープの外側で他の方法を載せた感想を投稿しています。
さあ、ここから本編が始まります。
C言語の第1のルールは、「もし避けられるならC言語を使うな」ということです。
もしC言語を使わざるをえないなら、最新のルールに従ってください。
C言語は 1970年代の始め に登場しました。C言語が進化する過程の中で、人々はその時その時の「C言語を習得」しました。しかし、習得したあと知識がそこで留まり、学び始めた時期のC言語が全てだと考え、人によって異なる一連の知識を持ってしまいました。
C言語の開発では「80年代90年代に学んだ事柄」に留まろうという気持ちを捨てなくはなりません。
ここでは、最新の基準に適合した最新のプラットフォームのもとで、過去のプログラムに対して過大な互換性を求めないことを前提としています。ある種の会社が20年前のシステムのアップグレードを拒否するというだけで、何もかもがいにしえの基準に縛られるべきではありません。
事前準備
標準のc99(c99は「1999年のC言語の標準」、c11は「2011年以後のC言語の標準」の意味。よって11>99です)
- Clang、デフォルト
- ClangはデフォルトでC11の拡張バージョンを使っています(
GNU C11 mode
)。よって最新版では他の特殊なオプションは不要です。 - 標準のC11を使いたいなら
-std=c11
、標準のC99なら-std=c99
と指定する必要があります。 - ClangはGCCよりも速くソースファイルをコンパイルします。
- ClangはデフォルトでC11の拡張バージョンを使っています(
- GCCには
-std=c99
または-std=c11
の指定が必要- GCCを使ってソースファイルをビルドすると、Clangよりも時間がかかりますが、 時には 早いプログラムを生成します。パフォーマンスの比較と回帰テストが大切です。
- GCC-5は(Clangと同様に)デフォルトで
GNU C11 mode
が設定されていますが、もし厳密にc11やc99が必要なら、さらに-std=c11
または-std=c99
を指定してください。
最適化
-
-O2、-O3
- 通常は
-O2
を使いたいところだと思いますが、時には-O3
を使いたい時もあるでしょう。双方のレベル(と複数のコンパイラ)で試し、最もよいパフォーマンスを出したバイナリを残しておきましょう。
- 通常は
-
-Os
- キャッシュの効率性を気にするのであれば(それがあるべき姿です)、
-Os
が役立ちます。
- キャッシュの効率性を気にするのであれば(それがあるべき姿です)、
警告
-Wall -Wextra -pedantic
- より新しいバージョンのコンパイラ には
-Wpedantic
がありますが、より広く過去のプログラムとの互換性を保つため、現在でも以前の-pedantic
も使えるようになっています。
- より新しいバージョンのコンパイラ には
- テストの間は、すべてのプラットフォームに
-Werror
と-Wshadow
を追加しなければなりません。- プロダクションのコードを
-Werror
を使ってデプロイするのは厄介かもしれません。プラットフォームやコンパイラやライブラリが異なると、出てくる警告も異なって出てくるからです。見たこともないプラットフォーム上のGCCのバージョンが、初めて見るような不可思議なやり方で文句を出してくるというだけの理由で、ユーザの全ビルドを台無しにしたくないですよね。
- プロダクションのコードを
-Wstrict-overflow -fno-strict-aliasing
などの、面白い追加オプションがあります。-fno-strict-aliasing
を指定するか、作られた時の型のオブジェクトへ確実にアクセスするだけかにします。C言語にはタイプをまたがって多くのエイリアスが存在するので、ソースプログラム中の全ての連携をコントロールしていない場合には、-fno-strict-aliasing
を使うとかなり安全です。
- 現時点では、Clangが正しいシンタックスに対して警告を出すことがあります。よって、
-Wno-missing-field-initializers
を追加すべきです。- GCCではGCC4.7.0以後、この不必要な警告が修正されました。
ビルド
-
コンパイルの単位
- 最も普通の方法では、C言語のビルドの過程は、すべてのソースファイルを分解してオブジェクトファイルを作り、全部のオブジェクトをリンクさせて終了します。このやり方はインクリメンタル型の開発には大きく寄与しますが、パフォーマンスや最適化にとっては、最良の方法ではありません。この方法ではコンパイラはコンパイルの単位を越えた最適化の可能性を見つけることができません。
-
LTO ― Link Time Optimization
- LTOは、オブジェクトファイルに中間表現を付すことで、「コンパイルの単位を越えたソースファイルの分析と最適化の問題」を修正しました。リンク時には、最適化できるソースファイルが単位を越えてコンパイルされます。
- LTOはリンクのプロセスを著しく遅くするかもしれませんが、もし、ビルドに相互に依存しないターゲット(.a、.so、.dylib、テスト用実行ファイル、アプリケーション用実行ファイル)が多数あるのならば、
make -j
が役に立ちます。 - Clang LTO ( ガイド )
- GCC LTO
- 2016年、ClangとGCCがLTOのサポートをリリースしました。コンパイルされてオブジェクトが生成され、最終的にライブラリとプログラムがリンクしている間に、
-flto
をコマンドラインのオプションに追加するだけでサポートが利用できます。 LTO
については、まだ注意深く見ておかなければなりません。コードがプログラム内で直接に使われるのではなく、追加のライブラリから使われる場合に、LTOが関数やコードを排除することがあります。というのも、LTOは、全体をリンクする際に、ある種のコードが使われていないことや到達できないこと、さらに、それらがリンクの最終的な結果には不要だということを検出してしまうからです。
Arch
-march=native
- CPUの全機能を使用する許可をコンパイラに与えます。
- ここでもパフォーマンステストと回帰テストが大切です(そして複数のコンパイラとコンパイラのバージョンの結果を比較します)。これは最適化の際、副作用が絶対にでないようにするために行います。
- 自分のビルドマシンにはない機能をターゲットとする必要がある場合、
-msse2
と-msse4.2
が便利でしょう。
コードを書く
型
新しいコードに、 char
、 int
、 short
、 long
、 unsigned
などの型を使おうとしているなら、それは誤りです。
最新のプログラミングでは #include <stdint.h>
と記述し、 標準 データ型を使用するべきです。
さらなる詳細は stdint.h仕様書 で確認してください。
一般的な標準データ型は以下のとおりです。
int8_t
、int16_t
、int32_t
、int64_t
– 符号付き整数型uint8_t
、uint16_t
、uint32_t
、uint64_t
– 符号なし整数型float
– 標準的な32ビットの浮動小数点数型double
– 標準的な64ビットの浮動小数点数型
char
はもう使わないので注意してください。実際、C言語では char
は誤った名称で誤った使い方をされています。
開発者は、符号なしバイトを操作する時でさえ char
を”バイト”として、常習的に乱用しています。 uint8_t
を単一の符号なしのバイトや1バイト(8ビット)の値として使い、 uint8_t *
を符号なしのバイトのシーケンスや複数のバイトの値として使う方がずっと単純明快です。
特別な基本型
uint16_t
や int32_t
のように固定された標準の幅に加え、 fast 型と least 型も stdint.h仕様書 に定義されています。
fast 型は以下のようになっています。
int_fast8_t
、int_fast16_t
、int_fast32_t
、int_fast64_t
– 符号付き整数型uint_fast8_t
、uint_fast16_t
、uint_fast32_t
、uint_fast64_t
– 符号なし整数型
fast型は最低限でも X
ビットを提供しますが、実際の記憶領域が要求通りのサイズになっているとは限りません。ターゲットとするプラットフォームでは大きい型の方がサポートが充実している場合、 fast 型は自動的にサポートの手厚い、大きな型を使います。
最もいい例を挙げましょう。64ビットのシステムで uint_fast16_t
を要求すると uint64_t
を得ます。ワードサイズの整数で操作する方が、16ビットの整数で操作するよりも速いためです。
fast 型のガイドラインは全てのシステムに対応しているわけではありませんが、対応している代表的なシステムはOS Xです。OS Xでは fast 型は 対応部分の固定幅にぴったりと一致すると定義されています 。
fast型は、コードの自己説明性という点でも便利です。例えば、16ビットしか計算に必要ないのに、プラットフォームで速く処理するために64ビットの整数を使いたいとします。この場合は uint_fast16_t
が役に立ちます。 uint_fast16_t
は、文書内のコードレベルでは「16ビットしか必要ない」としながらも、64ビット以下のLinuxプラットフォームで処理の速い64ビットで計算をしてくれます。
fast型で知っておくべきことは、一部のテストケースに影響を与えることがあるということです。記憶領域幅のエッジケースをテストする必要がある場合、一部のプラットフォーム(OS X)上では uint_fast16_t
を16ビットとし、他のプラットフォーム(Linux)上では64ビットとします。そのため、テストを合格させなければならないプラットフォームの最低限の数が増えてしまうのです。
fast 型は、プラットフォームによってサイズが変わる int
型と同じような不確かさがあります。しかし fast 型であれば、コード内の安全だと分かっている範囲に不確かさを制限できます(カウンタや、確認済みのバインド付きの一時的な値など)。
least 型は以下のようになっています。
int_least8_t
、int_least16_t
、int_least32_t
、int_least64_t
– 符号付き整数型uint_least8_t
、uint_least16_t
、uint_least32_t
、uint_least64_t
– 符号なし整数型
least型は、リクエストした型に対して、最も コンパクトな ビット数を提供します。
実際のところ、 least 型のガイドラインは標準的な固定幅の型に対してleast型を定義したに過ぎません。なぜなら、標準的な固定幅の型は要求された最小のビット数の正確な値をすでに提供しているからです
int
を使うべきか、使わざるべきか
int
が大好きな方もいるでしょう。絶対に手放さないという方もいるはずです。しかし、型のサイズが想定しているサイズから変わってしまったら、正確にプログラミングすることは技術的に不可能です。
inttypes.h に書かれた RATIONALE(理論的根拠) の項も読んでみてください。幅の固定されていない型を使うと危険だという理由が分かります。あなたがとても頭がよく、「 int
を使ったあらゆる場所で全ての16ビットと32ビットのエッジケースをテストする一方で、あるプラットフォーム上では int
が16ビットであり、別のプラットフォーム上では32ビットであると理解しながら開発できる」というのであれば、心置きなく int
を使ってください。
それ以外の人、私たちのような、頭の中でマルチレベルの決定木プラットフォームの詳細なヒエラルキー全体を把握できない人は、固定幅の型を使って、正確性の高いコードが自動的に手に入るようにしましょう。概念的に難しい問題が大幅に減り、テストによる間接費もずっと削減できます。
仕様書では、「ISOが定めたC言語の標準整数拡張ルールは、予告なく変更する場合があります」と、さらに簡潔に説明しています。
うまくこれと付き合いましょう。
例外的に char
を使う場合
2016年の現在、 char
の使用を 唯一 許されるのは、以前から存在するAPIが char
を要求した場合(例えば strncat
や、printfで用いる”%s”など)、または読み取り専用文字列を初期化した場合(例えば const char *hello = "hello";
など)だけです。後者が許されるのは、C言語の文字列リラテル( "hello"
)が、 char []
となるためです。
追記:C11ではネイティブでユニコードのサポートがあります。UTF-8の文字列リラテルは、 const char *abcgrr = u8"abc????";
のようなマルチバイトのシーケンスであったとしても、依然として char []
のままです。
例外的に{ int
、 long
など}を使う場合
ネイティブな戻り値の型やネイティブなパラメータと共に関数を使う場合、関数のプロトタイプやAPIの仕様書によって説明できるとして、これらの型を使います。
符号
unsigned
という型をコード内で使うことはありません。読みやすさや使いやすさを阻害するマルチワード型という悪しきC言語の慣習に従わなくともプログラミングはできます。 uint64_t
が使えるのに、 unsigned long long int
を使いたがる人なんているでしょうか。 stdint.h
の型はより 明快 で、意味が 正確 で、 意図 を伝えるのに優れています。文字の 使用上 も 小さくまとまっていて読みやすく なっています。
整数としてのポインタ
しかし、「煩雑なポインタの計算のために、ポインタを long
型にキャストしなければ」と思うかもしれません。
しかし、それは間違った考え方です。
ポインタの計算に使う正しい型は、 <stdint.h>
で定義された uintptr_t
です。また、同じように stddef.h で定義された ptrdiff_t
も便利です。
long diff = (long)ptrOld - (long)ptrNew;
の代わりに
ptrdiff_t diff = (uintptr_t)ptrOld - (uintptr_t)ptrNew;
または
printf("%p is unaligned by %" PRIuPTR " bytes.\n", (void *)p, ((uintptr_t)somePtr & (sizeof(void *) - 1)));
を使います。
後編 に続きます。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa