POSTD PRODUCED BY NIJIBOX
POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

2016年2月18日

2016年、C言語はどう書くべきか (前編)

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よりも速くソースファイルをコンパイルします。
  • 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

  • テストの間は、すべてのプラットフォームに -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 が便利でしょう。

コードを書く

新しいコードに、 charintshortlongunsigned などの型を使おうとしているなら、それは誤りです。

最新のプログラミングでは #include <stdint.h> と記述し、 標準 データ型を使用するべきです。

さらなる詳細は stdint.h仕様書 で確認してください。

一般的な標準データ型は以下のとおりです。

  • int8_tint16_tint32_tint64_t – 符号付き整数型
  • uint8_tuint16_tuint32_tuint64_t – 符号なし整数型
  • float – 標準的な32ビットの浮動小数点数型
  • double – 標準的な64ビットの浮動小数点数型

char はもう使わないので注意してください。実際、C言語では char は誤った名称で誤った使い方をされています。

開発者は、符号なしバイトを操作する時でさえ char を”バイト”として、常習的に乱用しています。 uint8_t を単一の符号なしのバイトや1バイト(8ビット)の値として使い、 uint8_t * を符号なしのバイトのシーケンスや複数のバイトの値として使う方がずっと単純明快です。

特別な基本型

uint16_tint32_t のように固定された標準の幅に加え、 fast 型と least 型も stdint.h仕様書 に定義されています。

fast 型は以下のようになっています。

  • int_fast8_tint_fast16_tint_fast32_tint_fast64_t – 符号付き整数型
  • uint_fast8_tuint_fast16_tuint_fast32_tuint_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_tint_least16_tint_least32_tint_least64_t – 符号付き整数型
  • uint_least8_tuint_least16_tuint_least32_tuint_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 [] のままです。

例外的に{ intlong など}を使う場合

ネイティブな戻り値の型やネイティブなパラメータと共に関数を使う場合、関数のプロトタイプや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)));

を使います。


後編 に続きます。