C++ Portability Guide

by 4 contributors:
この記事内
    1. C++ 移植性上の規則
      1. C++ テンプレートを書くときに細心の注意を払う
      2. static なコンストラクタを使わない
      3. 例外を使わない
      4. 実行時型情報を使わない
      5. iostream を含む、C++ 標準ライブラリの機能を使わない
      6. namespace 機能を使わない
      7. main() を必ず C++ ファイルに入れる
      8. たくさんある C/C++ のコンパイラの中で共通の機能だけ使う
      9. C のコードに C++ 式コメントを入れない
      10. XP(クロスプラットフォーム)なコードに CR(キャリッジリターン)を入れない
      11. ニューライン(new line)コードをファイルの終端に入れる
      12. コードに余計なトップレベルセミコロンを入れない
      13. C++ ファイル名の拡張子は .cpp
      14. varargs(可変個引数リスト)とインラインを混ぜない
      15. オブジェクトへのリストによる初期化を使わない
      16. デフォルトのコンストラクタは必ず持たせる
      17. インナー (ネストされた) クラスには注意する
      18. コンストラクタや初期化を必要とする変数宣言には注意する
      19. C と C++ で互換性のあるヘッダファイルを作る
      20. for() 構文の内部で宣言される変数のスコープに気をつける
      21. ローカルな集合の初期化は static に宣言する
      22. 移植性を下げる複雑なインラインは除く
      23. インライン関数を呼ぶ return 文を使わない
      24. include のファイル階層とファイルサイズに気をつける
      25. すべてのサブクラスの virtual メンバ関数上で virtual 宣言を使う
      26. コピーコンストラクタと代入演算子はいつも定義する
      27. シグニチャ(引数と戻り値)の似たメソッドのオーバーロードには気をつける
      28. 思いがけない曖昧さを防ぐためにスカラ定数でタイプする
      29. 予期しない曖昧さを避けるためにスカラー定数を書く
      30. mutable を使わない
      31. XPCOM のコードでは nsCOMPtr を使う
      32. 識別子として予約語を使わない
    2. C でも C++ でも役に立つ要素
      1. 本質的な型には nspr 型をいつでも使う
      2. include 文を #ifdef で囲んではいけない
      3. #include 文には単純にファイル名のみ記述する
      4. Mac では boolean 式での代入について問題がある
      5. どのソースファイル名も重複してはならない
      6. 一時的にコードの塊を無効にするためにはコメントよりも #if 0 を使う
      7. コンパイラの警告を有効にして、警告のないコードを書く
      8. struct (または C++ での class ) 内でビットフィールドを使う場合、同じ型を用いる
      9. ブール値を表すビットフィールドに対して(PRBool ではなく)符号なし型を使う
    3. 改定履歴
    4. 更なる読み物
  1. 原文書の情報

この後に、多くのマシンやコンパイラを横断して移植可能な C++ のコードを作る上で役に立つ規則・ガイドライン・TIPS の集まりが続きます。

この情報は、25 にものぼる異なるマシンと1ダース分以上の C++ コンパイラを横断して多くのコードを移植した結果として生まれたものです。これらのうちいくつかはあなたを苛立たせ、もうお手上げだと感じて「えい、<愛用の C++ 機能を追加>できないのはヘボコンパイラだ」と叫びたくなるかもしれません。しかし、これが移植性の高いコードの現実です。あなたがルールを守るなら、あなたのコードはすべての Mozilla プラットフォーム上でシームレスに動作し、新しいマシンへの移植も容易となるでしょう。

私たちは情報を最新に保つよう努力しています(例えば、ときどきコンパイラの改定で制約が取り除かれるかもしれません)。これらの tips に関する最新情報や、追加の情報、追加のアイディアなどがあれば、それを Christopher BlizzardScott CollinsDavid Baron に知らせてください。

もし、以下のルールのいずれかを破っているようなコードが Mozilla の中に見つかったら、バグ として登録してください。作者を見つけるのに bonsai を使うことも出来ます。

これらのルールのどれ一つとして絶対的なものではありません。やりたいことが私たちのサポートするすべてのコンパイラで正しく動作するということをパッチの製作者が示せるならば、自由にこれらのルールを破って、改訂してもいいです。しかし、これにはたくさんの作業が必要で、それを行いたいと思う非常に良い理由がなければ推奨されません。

C++ 移植性上の規則

C++ テンプレートを書くときに細心の注意を払う

nsCOMPtrCallQueryInterface によって使われているパターンのような)Mozilla で既に使われているために移植性があるということがすでにわかっているものだけを使うか、私たちのサポートするコンパイラ上で注意深くコードをテストすることをいとわず、壊れたときにそれを戻すことをいとわないのでなければ、C++ テンプレートを使わないでください。

static なコンストラクタを使わない

移植性の低い例:

FooBarClass static_object(87, 92);

void
bar()
{
  if (static_object.count > 15) {
     ...
  }
}

static なコンストラクタは確実な動作が期待できません。static に初期化されたオブジェクトは起動時(main() が呼び出される直前)にインスタンス化されるオブジェクトです。たいていは、これらのオブジェクトには2つの部品があります。一つ目は、プログラムのグローバルなデータ部分へ読み込まれる静的なデータであるデータ部分です。二つ目の部分は、main() が呼び出される前にローダによって呼び出される初期化関数です。今までに、多くのコンパイラが初期化関数について信頼できる実装をしていないのを目にしてきました。そのとき、オブジェクトのデータを得るとき、それは決して初期化されないものなのです。この制限に対する一つの代替案として、オブジェクトの単独のインスタンスを作り、すべての参照を静的なラッパー関数への呼び出しをともなう初期化されたオブジェクトに入れ替えるラッパー関数を書くことが挙げられます:

移植性の高い例:

static FooBarClass* static_object;

FooBarClass*
getStaticObject()
{
  if (!static_object)
    static_object =
      new FooBarClass(87, 92);
  return static_object;
}

void
bar()
{
  if (getStaticObject()->count > 15) {
    ...
  }
}

例外を使わない

例外は、幅広くは実装されていない C++ の次なる一例です。そのため、それらの使用は C++ の移植性を低下させます。例外を使ってはいけません。運悪く、同じような機能をもったよい代替案はありません。

この規則の例外が一つあり(ここではそれについて言わないが)、それは多分大丈夫でしょう。ただし、いくつかのマシンに限定されたコードでだけ例外を使う必要性があるでしょう。もし、特定のマシン限定のコードの中で例外を使うなら、すべての例外をそこでキャッチしなくてはなりません。なぜならば、XP(クロスプラットフォーム)なコードにまたがって例外を飛ばすことは許されないからです。

実行時型情報を使わない

実行時型情報(RTTI:Run-time type information)は比較的新しい C++ の機能で、多くのコンパイラではサポートされていません。使わないで下さい。

もし、実行時の型が必要なら、継承の階層のベースクラスへの classOf() virtual メンバ関数の追加と、それぞれのサブクラスのメンバ関数をオーバーライドすることによって類似の結果を作り上げることができます。classOf() が階層のそれぞれのクラスについて固有の値を返すなら、実行時の型比較をすることが出来るでしょう。

iostream を含む、C++ 標準ライブラリの機能を使わない

比較的新しいコンパイラは名前空間や .h のないヘッダの使用を必要とするのに対して、比較的古いコンパイラは使用できないので、C++ 標準ライブラリの機能を使うことで移植性に関する重大な問題を引き起こします。これには cincout のような、iostream 機能を含んでいます。

さらに、C++ 標準ライブラリを使うことで、小さなデバイス上で Mozilla を使おうとしている人たちが使うのが難しくなります。

この規則には一つの例外があります:配置 new を使うことは受け入れられています。それを使うには、 #include NEW_H を書くことで標準ヘッダ <new> をインクルードしてください。

namespace 機能を使わない

namespaceusing キーワードを使った)namespace のサポートは、比較的新しい C++ の機能で、多くのコンパイラではサポートされていません。使わないでください。

main() を必ず C++ ファイルに入れる

最初の C++ コンパイラである Cfront は実は C コンパイラにとってのとても気の利いたプリプロセッサでした。Cfront は C++ のコードを読み、同じ機能を持つ C のコードを生成しました。C++ とC の起動順序は、 少々異なっています(例えば、静的なコンストラクタは C++ のためには呼ばれなくてはならない)。そして、Cfront はこの特殊は実装を "main()" と呼ばれる関数の呼び出しに注目し、それを別のもの("__cpp__main()" など)に変換し、C++ 特別の起動時動作をしないで元の関数を呼ぶ新たな main() を追加することによって実装しています。これをすべてうまく働かせるためにはもちろん、Cfront は main() 関数を見る必要があります。そのため、main() は C++ ファイルの中になければなりません。ほとんどのコンパイラはこの制限を何年か前に解除していて、C++ 特有の初期化実行をリンカ(Linker)の問題として扱っています。しかし、いくつかの商用コンパイラは未だに Cfront をベースに出荷されています。HP や SCO などがそれに該当します。

このため、代替案はきわめて単純です。main() を C++ ファイルの中に入れることに注意してください。Unix バージョンの Mozilla では、ほんの数行のコードを含む新しい C++ ファイルを加えるという形でこれを行っています。そして実際には C ファイルにある主たる main() を読んでいます。

たくさんある C/C++ のコンパイラの中で共通の機能だけ使う

私たちが使うことの出来る数々のコンパイラでは、C と C++ コンパイラの実装はまったく異なっています。これは同じマシン上で C 言語でできることが C++ ではできないことがときどきあるということです。例の一つが long long型です。いくつかのシステム(IBM のコンパイラはその一つでした。しかし、今はよくなったと考えます)では、C コンパイラは long long型をサポートしていますが、C++ コンパイラはサポートしていません。これは移植上の障害になりえます。それは、ときどきこれらの型が C と C++ のファイルで共有されるヘッダファイルに出てくるからです。それに対する唯一の対策は、両方のコンパイラでサポートしている共通要素だけを使うことです。long long型についての特別なケースでは、long long型が利用できないために、64ビットの整数型をサポートするためのマクロのセットを開発しました。C か C++ コンパイラが特別に 64ビットの型をサポートしない場合にこれらのマクロを使います。

C のコードに C++ 式コメントを入れない

Netscape の Unix エンジニアの血圧を上げるもっとも簡単な方法は、C ファイルの中に C++式コメント(//コメント)を入れることです。そうです。これは Microsoft Visual C++ の Cコンパイラの上では動くでしょう。しかし、間違った方法です。世界の圧倒的多数の C コンパイラではサポートされていません。とにかくそんなことはやめなさい。

多くのヘッダファイルは C のファイルと C++ のファイルから include されます。これらのヘッダにも同じルールを適用するのは良い考えだと思います。C ファイルから include されるヘッダファイルの中に C++式コメントを入れないでください。もしかすると #ifdef __cplusplus ブロックの中だけは、C++ スタイルのコメントを使ってもよいと考えるかもしれません。しかし、それがいつでもうまくいくとは確信できません(いくつかのコンパイラはコメント抽出とプリプロセッシングの間で奇妙な挙動をします)し、そこまでするに見合う価値があるとはとても思えません。とにかく、C ファイルから include されるかもしれないすべてのヘッダファイルのために、C スタイルの /**/式コメントを貫いてください。

XP(クロスプラットフォーム)なコードに CR(キャリッジリターン)を入れない

これは C++ 特有のことではないため、C++ コンパイラだけ以上の問題に見えます。たくさんある C/C++ のコンパイラの中で共通の機能だけ使う を参照してください。

Unix システムでは、標準的な行端文字はニューライン(new line)('\n') です。多くの PC のエディタの標準はキャリッジリターン(carriage return)('\r')です。PC のコンパイラはどちらにせよ、何も問題がないようです。しかし、いくつかの Unix コンパイラではキャリッジリターンを見つけるとつっかえてしまいます(ホワイトスペースとしてそれを認識できないのです)。そのため、いかなるクロスプラットフォームなコードにも、キャリッジリターンをチェックインしないというルールを作っています。このルールは Windows のフロントエンドコードには強制されません。そのコードは PC上でしかコンパイルされないためです。Mac のコンパイラはどちらにせよ、何も問題がないようです。しかし同じルールはすべての PC のために適用されます。クロスプラットフォームなコードにはキャリッジリターンは入れてはいけません。

MacCVSやWinCVS、DOSの改行を使用するよう設定されたcgwin上のcvsを使用する場合は、(リポジトリとワーキングコピーとの間で)自動的にプラットホームに適合した行端文字に変換されるので、行端文字について悩む必要はありません。ただ、Unixシステムの改行を使用するよう設定されたcygwin上のCVSを使用したり、Mac OS X付属のコマンドライン版cvsを使用する際は、行端文字に注意が必要です。 MacCVS, WinCVS, and cygwin CVS when configured to use DOS line endings automatically convert to and from platform line endings, so you don't need to worry. However, if you use cygwin CVS configured for Unix line endings, or command line cvs on Mac OS X, you need to be somewhat careful.

ニューライン(new line)コードをファイルの終端に入れる

ニューラインコードがファイルの終端にない場合、Sun の WorkShop コンパイラでは .h ファイルで、HP上では .cpp ファイルでトラブルを起こします。

コードに余計なトップレベルセミコロンを入れない

移植性が低い例:

int
A::foo()
{
};

これは C よりも C++ のコードで姿を見せるように見えるまた一つの問題です。この問題はちょっとした厄介者です。関数の終わりにあるちょっと余計なセミコロンは、ほとんどのコンパイラで無視されます。しかし、いくつかのコンパイラではとても問題なのです(IBM の AIX コンパイラは余計なトップレベルセミコロンを嫌います)。余計なセミコロンをつけないでください。

移植可能な例:

int
A::foo()
{
}

C++ ファイル名の拡張子は .cpp

これは、ただ面倒な問題です。C++ のファイル名は何? file.cppfile.ccfile.Cfile.cxxfile.c++file.C++? ほとんどのコンパイラでは問題になりません。しかし、いくつかのコンパイラはとても選り好みします。Mozilla を移植するすべてのプラットフォーム上で使うことのできる一つのファイル拡張子を決めることができませんでした。たいした理由もなく、file.cpp に決めました。たぶん、Mozilla の最初の C++ ソースコードがその拡張子でチェックインされたからでしょう。まぁ、そうなりました。私たちが使う拡張子は .cpp です。この拡張子はほとんどのコンパイラで問題ないようです。しかし、いくつかのコンパイラはそうではありません。それらのシステム上では、私たちはコンパイラのためのラッパーを作りました(ns/config/rules.mkns/build/* の中の STRICT_CPLUSPLUS_SUFFIX をみてください)。これは、実際には file.cpp を適切な拡張子を持つ別のファイルとしてコピーして、そのファイルをコンパイルし、削除するものです。新しいシステムに移植するとき、次のようなことをする必要があります。オリジナルの .cpp ファイルに対するデバッグ情報を生成するために、#line ディレクティブを使うことを忘れないでください。

varargs(可変個引数リスト)とインラインを混ぜない

XXXldb: Is this still relevant? Where was it a problem? Do we have any examples of this in the tree?

移植性の低い例:

class FooBar {
  void va_inline(char* p, ...) {
    // <span class="remark">コードがいろいろ</span>
  }
};

タイトルですべてを語っています。varargs とインライン関数を合わせるのはよくはみえません。もし、(それ自身で移植性の問題を引き起こしかねない)varargs を使わなければならないなら、可変個引数リストをもつメンバ関数はインラインでない関数にするよう注意してください。

移植可能な例:

// <span class="remark">foobar.h</span>
class FooBar {
    void
      va_non_inline(char* p, ...);
};

// <span class="remark">foobar.cpp</span>
void
FooBar::va_non_inline(char* p, ...)
{
       // <span class="remark">コードがいろいろ</span>
}

 

オブジェクトへのリストによる初期化を使わない

移植性の低い例:

FooClass myFoo = {10, 20};

いくつかのコンパイラはオブジェクトに対するこの構文を許していません(HP-UX が認めていません)。実際のところ、いくつかのものが許しているにすぎません。このため、この構文を用いないでください。繰り返しますが、ラッパー関数を使ってください。詳細は static なコンストラクタを使わない を参照してください。

デフォルトのコンストラクタは必ず持たせる

XXXldb: This is probably wrong, but there are a bunch of interesting points to be made related to default constructors - perhaps relating to nsAutoPtr.

たとえオブジェクトの構造・継承上意味を持たない場合であっても、必ずデフォルトのコンストラクタを持たせてください。HP-UX のコンパイラは、デフォルトのコンストラクタを持たない静的に初期化されたオブジェクトにクレームをつけるでしょう。

インナー (ネストされた) クラスには注意する

インナークラスを使用する場合は、アクセス制御に注意してください。 インナークラスのアクセス制御について、大半のコンパイラは(意図してかどうかはともかく)2003年版のC++標準に従っています。この標準では、インナークラスは、取り囲むクラスのメンバーに対して特別なアクセス権を持っています。しかしながら、一部のコンパイラは1998年版のC++標準に従っています。この標準では、インナークラスは、取り囲むクラスのメンバーに対して特別なアクセス権を持っていません。 When using nested classes, be careful with access control. While most compilers implement (whether intentionally or not) the rules in the 2003 version of the C++ standard that give nested classes special access to the members of their enclosing class, some compilers implement what is described in the 1998 version of the standard, which is that nested classes have no special access to members of their enclosing class.

移植性の低い例:

class Enclosing {
  private:
    int x;
  public:
    struct Nested {
      void do_something(Enclosing *E) {
        ++E->x;
      }
    };
};

移植性の高い例:

class Enclosing {
  private:
    int x;
  public:
    struct Nested; // forward declare |Nested| so the |friend|
                   // declaration knows what scope it's in.
    friend struct Nested; // make |Nested| a friend of its enclosing
                          // class
    struct Nested {
      void do_something(Enclosing *E) {
        ++E->x;
      }
    };
};

二番目の移植性の低い例:

class Enclosing {
  private:
    struct B;
    struct A {
      B *mB;
    };
    struct B {
      A *mA;
    };
};

そして同等の機能を持つ移植性の高い例:

class Enclosing {
  private:
    struct A;
    friend struct A;
    struct B;
    friend struct B;
    struct A {
      B *mB;
    };
    struct B {
      A *mA;
    };
};

コンストラクタや初期化を必要とする変数宣言には注意する

移植性の低い例:

void
A::foo(int c)
{
  switch(c) {
  case FOOBAR_1:
    XyzClass buf(100);
    // <span class="remark">stuff</span>
    break;
  }
}

ブロックや switch 構文の周辺では、変数の配置には気をつけてください。いくつかのコンパイラ(HP-UX)は実行するためにコンストラクタや初期化子を必要とするどの変数もメソッドの最初に置くことを要求します。-- 変数が switch 文の内部で宣言され、デフォルトのコンストラクタを実行する必要があるため、これはコンパイルできないコードです。

移植可能な例:

void
A::foo(int c)
{
  XyzClass buf(100);

  switch(c) {
  case FOOBAR_1:
    // <span class="remark">stuff</span>
    break;
  }
}

C と C++ で互換性のあるヘッダファイルを作る

移植性の低い例:

/*<span class="remark">oldCheader.h</span>*/
int existingCfunction(char*);
int anotherExistingCfunction(char*);

/*<span class="remark"> oldCfile.c </span>*/
#include "oldCheader.h"
...

// <span class="remark">new file.cpp</span>
extern "C" {
#include "oldCheader.h"
};
...

C へのインタフェースが露出している新しいヘッダファイルを作るときは、C と C++ ファイルの両方からインクルードされた時に正しく動くように作る必要があります。新しい C++ ファイルの中で既存の C ヘッダをインクルードし始めると、C ヘッダファイルを(C と同様に)C++ もサポートするよう修正する必要があります。ただ単に古いヘッダファイルに extern "C" {} と入れるだけではだめです。このようにしてください:

移植可能な例:

/*<span class="remark">oldCheader.h</span>*/
PR_BEGIN_EXTERN_C
int existingCfunction(char*);
int anotherExistingCfunction(char*);
PR_END_EXTERN_C

/*<span class="remark"> oldCfile.c </span>*/
#include "oldCheader.h"
...

// <span class="remark">new file.cpp</span>
#include "oldCheader.h"
...

このようにするのには、見栄え上よいという以上にいくつかの理由があります。一つには、これをインクルードするすべての C++ ファイルの代わりに一つの共用空間(ヘッダファイル)で仕事をすることで、他の人にとって快適にするのです。また、C++ でも安全な C ヘッダを作ることで、「あのー、このファイルは今、 C++ からも呼び出されていますよ」と文書化したことになります。これはよいことです。そしてまた、修正がやっかいな大きな移植上の大きな悪夢を防ぐことができるのです。。。

いくつかのシステムでは C または C++ でインクルードされるべきシステムヘッダファイルの中で C++ を include します。ただ単に extern "C" {} で守るだけではだめで、通常だと"最適化"として提供されるインライン関数の形で用いられる実際の C++ コードでなくてはなりません。わたしたちは、ヴェンダがそのようにするという知恵に疑問を持っていますが、私たちに出来ることはありません。システムヘッダファイルを変更することは、できれば望みたくない方法です。どっちにしろ、これがそんなに問題でしょうか?以下のコードの一部分を例に取りましょう:

移植性の低い例:

/*<span class="remark">system.h</span>*/
#ifdef __cplusplus
  /*<span class="remark"> optimization </span>*/
inline int sqr(int x) {return(x*x);}
#endif

/*<span class="remark">header.h</span>*/
#include <system.h>
int existingCfunction(char*);

// <span class="remark">file.cpp</span>
extern "C" {
#include "header.h"
}

何が起きそうなのか?C++ コンパイラは extern "C" 宣言を file.cpp に見つけたとき、言語を C に変更するでしょう。なぜならば、内部のコードはすべて C と仮定されるからです。C 型の自由な命名規則が適用されます。しかし、(これはコンパイラにではなく、プリプロセッサに見つけられる)__cplusplus プリプロセッサマクロがすでに定義されています。システムヘッダファイルの中で、#ifdef __cplusplus ブロックの内部の C++ コードは(すでにC モードで実行している)コンパイラに見つけられるでしょう。文法エラー(Syntax Error)が大量に出ます!もし、代わりにextern "C" がヘッダファイルの中でなされていれば、C 関数は正しく守られ、システムヘッダファイルを均衡の外に離します。これは動作します:

移植性の高い例:

/*<span class="remark">system.h</span>*/
#ifdef __cplusplus
  /*<span class="remark"> optimization </span>*/
inline int sqr(int x) {return(x*x);}
#endif

/*<span class="remark">header.h</span>*/
#include <system.h>
extern "C" {
int existingCfunction(char*);
}

// <span class="remark">file.cpp</span>
#include "header.h"

プログラムの extern "C" セグメントから離れる前のもう一つの問題があります。ときどき、システムファイルで extern "C" すべきと感じるでしょう。これは extern "C" による保護を自身でもたない C システムヘッダファイルをインクルードする必要があるからです。多くのヴェンダはすべてのヘッダで C++ をサポートするように更新していますが、C++ を十分に理解しないものがまだいくつかあります。他の大多数ではなく、ほんのいくつかのプラットフォームのためにそうしなければならないでしょう(そういう場合には #ifdef SYSTEM_X を使います)。(実はシステムヘッダファイルをインクルードするのにもっとも安全な場所である)システムヘッダファイルで extern "C" するためのもっとも安全な場所は、ヘッダファイルのインクルード階層のなかでできる限り低い場所です。言い換えます。このフレーズをシステムコードにより近い低位置のヘッダファイルへ入れてください。mail のヘッダファイルの中に入れてはいけません。理想を言えば、もっとも良い場所は直接的にシステムコードが入っている NSPR や XP のヘッダファイルの中です。

for() 構文の内部で宣言される変数のスコープに気をつける

移植性の低い例:

void
A::foo()
{
  for (int i = 0; i < 10; i++) {
    // <span class="remark">do something</span>
  }
  // <span class="remark"><strong>i</strong> might get referenced</span>
  // <span class="remark"> after the loop.</span>
  ...
}

これは実のところ C++ 標準が時間をかけて変更されているため生じた問題です。もともとの C++ の規格では i のスコープは外のブロックと同様(この場合は関数 A::foo())です。標準は変更され、ゆえに今では i のスコープは for() {} ブロック内部です。ほとんどのコンパイラはこの新しい標準を用います。いくつかのコンパイラ(例えば、HP-UX)は未だに古い標準を使っています。いくつかのほかのコンパイラ(例えば、gcc)は新しいルールを使っていますが、古いものも許容しています。もし、ifor() {} ブロックの中の後ろで参照されたとき、gcc は構築を許すでしょうが、"obsolete binding"(もう使われない結びつき) を使っていると警告を出すでしょう。そのため、上記のコードが有効な一方で i が関数の後に使われた場合に不明瞭となるでしょう。おそらくは大事を取り、巡回変数の宣言を for() ループの外側で行うことが望ましいでしょう。そうすることにより、すべてのプラットフォームでの動作を把握することが出来るのです:

移植性の高い例:

  void
  A::foo()
  {
    int i;
    for (i = 0; i < 10; i++) {
      // <span class="remark">do something</span>
    }
    // <span class="remark"><strong>i</strong> might get referenced</span>
    // <span class="remark"> after the loop.</span>
    ...
  }

また、ループのための変数をその後ろで再利用しても、かわりに別の方法で変数を再宣言してもいけません。これは現在の標準で許容されていますが、多くのコンパイラはエラーとして扱います。以下の例を参照ください:

移植性の低い例:

void
A::foo()
{
  for (int <strong>i</strong>; 0;) {
    // <span class="remark">do something</span>
  }
  for (int <strong>i</strong>; 0;) {
    // <span class="remark">do something else</span>
  }
}

移植性の高い例:

void
A::foo()
{
  for (int <strong>i</strong>; 0;) {
    // <span class="remark">do something</span>
  }
  for (int <strong>j</strong>; 0;) {
    // <span class="remark">do something else</span>
  }
}

ローカルな集合の初期化は static に宣言する

移植性の低い例:

void
A:: func_foo()
{
  char* foo_int[] = {"1", "2", "C"};
  ...
}

この一見無害なコード断片は HP-UX のコンパイラとリンカを使うと "loader error" を生成します。もし、配列を static なデータとしたいのなら、このように:

移植性の高い例:

void
A:: func_foo()
{
  static char *foo_int[] = {"1", "2", "C"};
  ...
}

さもなければ、自動的な配列を保持することもできます。その場合初期化は手動で行います:

移植性の高い例:

void
A:: func_foo()
{
  char *foo_int[3];

  foo_int[0] = XP_STRDUP("1");
  foo_int[1] = XP_STRDUP("2");
  foo_int[2] = XP_STRDUP("C");
  // <span class="remark">or something equally Byzantine...</span>
  ...
}

移植性を下げる複雑なインラインは除く

移植性の低い例:

class FooClass {
  ...
  int fooMethod(char* p) {
    if (p[0] == '\0')
      return -1;

    doSomething();
    return 0;
  }
  ...
};

これは驚きです。しかし、多くの C++ コンパイラはインラインメンバ関数のハンドリングでとても悪い挙動をします。Cfront ベースのコンパイラ(SCO や HP-UX 上のそれらなど)はぜんぶではなくともほとんどの単純なインライン関数でも、"sorry, unimplemented(ごめんなさい、実装されていません)"とエラーメッセージを出して音を上げがちです。多くの場合、この問題を起こすソースは、複数の return 文を持つインラインです。これを修正するためには、関数の最後の単独の箇所に return を持ってくることです。しかし、ほかにも "not implemented(実装されていません)" という結果に終わらせてしまうだろう要素があります。こういった理由のため、Mozilla の C++ コードのほとんどはインライン関数を使っていないことがわかるでしょう。インライン関数をすべて追い払うことをルール化したくはありませんが、インライン関数の仕様が一定の危険をともなうことに注意する必要があります。そのため、(ただパフォーマンス上のメリットがあるかもしれないという行き当たりばったりの望みではなく)目に見えるメリットがあるときだけ使うと良いでしょう。まぁ、そんなことはするなということです。

移植性の高い例:

class FooClass {
  ...
  int fooMethod(char* p) {
    int return_value;

      if (p[0] == '\0') {
         return_value = -1;
      } else {
         doSomething();
         return_value = 0;
      }
      return return_value;
  }
  ...
};

もしくは

class FooClass {
  ...
  int fooMethod(char* p);
  ...
};

int FooClass::fooMethod(char* p)
{
  if (p[0] == '\0')
    return -1;

  doSomething();
  return 0;
}

インライン関数を呼ぶ return 文を使わない

前のティップと同じ理由で、インライン関数を呼ぶ return 文を使ってはいけません。同様に "not implemented(実装されていません)" というエラーが得られるでしょう。戻り値は一時的に保存し、そして戻してやりましょう。

include のファイル階層とファイルサイズに気をつける

include のファイル階層とファイルサイズに気をつけてください。Microsoft Visual C++ 1.5 は、深いファイル階層や大きなファイルサイズの include をすると、インターナルコンパイラエラーを出します。ヘッダファイルのファイルサイズと同様にヘッダファイルの階層も上限に気をつけてください。

すべてのサブクラスの virtual メンバ関数上で virtual 宣言を使う

移植性の低い例:

class A {
  virtual void foobar(char*);
};

class B : public A {
  void foobar(char*);
};

また一つの足手まといです。上記のクラス宣言の中で、A::foobar() は virtual として宣言されています。C++ ではサブクラスの void foobar(char*) のすべての実装もまた virtual(一度 virtual なら、いつも virtual)であると言っています。このコードは本当に問題ないのですが、いくつかのコンパイラはサブクラスの中で virtual の関数のオーバーロードでも virtual 宣言を求めます。あなたがそうしなければ、警告が返ってきます。これはハードエラーではない一方、この要素はヘッダファイルにありがちであるため、あなたを苛立たせるほどに多くの警告を表示するでしょう。サブクラスで virtual 宣言を含むことによってコンパイラの警告を黙らせるのがよりよいです。これもまた、よりよい文書です:

移植性の高い例:

class A {
  virtual void foobar(char*);
};

class B : public A {
  virtual void foobar(char*);
};

コピーコンストラクタと代入演算子はいつも定義する

C++ を疑わしくする機能の一つが、コピーコンストラクタの使用です。クラスのコピーコストラクタはオブジェクトの引渡しや戻りでオブジェクトを値渡しすることを意味します(あなたがお望みなら、値渡しがコピーコンストラクタの読み出しを意味します)。これをはっきりさせることは重要なことです。あなたが望まないかもしれなくても、コンパイラが暗黙にコピーコンストラクタへの呼び出しを生成することがあります。例えば、関数の引数としてオブジェクトを値渡ししたとき、一時コピーが作られ、そのコピーが渡され、関数から戻るときに破棄されます。こうしたくないこともあるかもしれませんし、いつもあなたのクラスのインスタンスを参照渡ししたいこともあるかもしれません。コピーコンストラクタをあなたが作らなければ、C++ コンパイラは(デフォルトのコピーコンストラクタ)を一つあなたのために作るでしょう。そしてこれの自動的に生成されたコピーコンストラクタは、そう、大抵低品質です。それによって、コンパイラが暗黙で(多分間違った)動作のためにすばらしいコードでないだろうコード断片の呼び出しを生成する場面に出くわします。

ええ、「問題ない、私はコピーコンストラクタをいつ呼んでいるかわかっている、そんなことはしない」と思うでしょう。しかし、あなたのクラスを他の人が使う場合はどうでしょう?安全な方策としては、二つのうちいずれかです:もし、あなたがあなたのクラスを値渡しをサポートさせようとするなら、クラスによいコピーコンストラクタを書いてください。もし、クラスに値渡しをサポートする理由が見当たらないなら、明示的にそれを禁止すべきです。コンパイラのデフォルトのコピーコンストラクタにそれを任せてはいけません。その方針を強制する方法は、private でコピーコンストラクタを宣言して、定義させないことです。そういう状況では、同じクラスのオブジェクトの代入のために使われた代入演算子と同じようにしてください。例:

class foo {
  ...
  private:
  // <span class="remark">These are not supported</span>
  // <span class="remark">and are not implemented!</span>
  foo(const foo& x);
  foo& operator=(const foo& x);
};

このようにしたとき、コピーコンストラクタを暗黙で呼ぶコードがコンパイルもリンクもされないことを確信できるでしょう。このように、知らないうちには何も起きません。ユーザのコードがコンパイルできないとき、参照渡しをしたかったところで(おっと!)値渡しされていることに気づくでしょう。

シグニチャ(引数と戻り値)の似たメソッドのオーバーロードには気をつける

メソッドの型仕様(型のシグニチャ)がたった一つ "abstract" 型かどうかによって区別されるとき、メソッドのオーバーロードをしないのが最善です(例:PR_Int32int32)。そのコードを他のプラットフォームに持っていったときに、突然とある Foo2000 といったコンパイラ上ではオーバーロードしたメソッドと同じ型仕様を持っていることが見られるでしょう。

思いがけない曖昧さを防ぐためにスカラ定数でタイプする

移植性の低い例:

class FooClass {
  // <span class="remark">having such similar signatures</span>
  // <span class="remark">is a bad idea in the first place.</span>
  void doit(long);
  void doit(short);
};

void
B::foo(FooClass* xyz)
{
  xyz->doit(45);
}

スカラ定数(例:PR_INT32(10) や 10L)で記述することに気をつけてください。でなければ、特に上記の2つの関数のような複数の関数の呼び出しの解決を潜在的にともなう曖昧関数呼び出しを行うことになります。すべてのコンパイラではないですが、曖昧な関数呼び出しに警告を出すものがあります。

移植性の高い例:

class FooClass {
  // <span class="remark">having such similar signatures</span>
  // <span class="remark">is a bad idea in the first place.</span>
  void doit(long);
  void doit(short);
};

void
B::foo(FooClass* xyz)
{
  xyz->doit(45L);
}

予期しない曖昧さを避けるためにスカラー定数を書く

(Linux などの)いくつかのプラットフォームでは、ときおりクロスプラットフォームなコードの中の定義と衝突するようなブールのような型宣言をネイティブに持っています。いつでも、PRBool (PR_TRUE, PR_FALSE) か XP_Bool (TRUE, FALSE) を使ってください。

mutable を使わない

すべての C++ コンパイラが mutable キーワードをサポートしているわけではありません:

データメンバの const 性を除去するための "fake this" 手法を使う必要があるでしょう:

void MyClass::MyConstMethod() const
{
  MyClass * mutable_this = NS_CONST_CAST(MyClass *,this);

  // Treat mFoo as mutable
  mutable_this->mFoo = 99;
}

XPCOM のコードでは nsCOMPtr を使う

Mozilla は最近、XPCOM コードで nsCOMPtr を採用しました。

使用法の詳細は、nsCOMPtr ユーザーマニュアル を参照ください。

識別子として予約語を使わない

C++ 標準規格 17.4.3.1.2 節 グローバル名 [lib.global.names] 第1パラグラフによると:

名前と関数シグネチャのある特定の組は、実装によって常に予約されています:

  • 二重のアンダースコア(__)が含まれる名前、または後ろに大文字 (2.11) が付くアンダースコアで始まる名前は、ある目的のために実装によって予約されています。
  • アンダースコアで始まる名前は、グローバルな名前空間で使う名前として実装によって予約されています。

C でも C++ でも役に立つ要素

本質的な型には nspr 型をいつでも使う

本質的な型には nspr 型をいつでも使う。この規則の唯一の例外は、クロスプラットフォームなコードから呼ばれる機種依存のコードを書いているときです。この場合、たぶん型システムに橋渡しをし、nspr 型からネイティブの型へのキャストが必要でしょう。

include 文を #ifdef で囲んではいけない

include 文を #ifdef で囲んではいけません。その理由は、そのシンボルが未定義だったとき、ほかのコンパイラのシンボルも未定義になって、すべてのプラットフォーム上でコードをテストするのが困難になるでしょう。そのようにしなかった例:

悪いコード:

// don't do this
#ifdef X
#include "foo.h"
#endif

この規則の例外は、異なったマシンに対する異なったシステムファイルの include をするときです。この場合、#ifdef SYSTEM_X のような include をする必要があるでしょう。

#include 文には単純にファイル名のみ記述する

移植性の低い例:

#include "directory/filename.h"

Mac のコンパイラは #include のパス名を他のシステムと異なる基準で扱います。結果として、#include 文は単にファイル名だけを含むものでなくてはなりません。必要な結果が得られるようコンパイラが探せるディレクトリに変更してください。しかし、Mozilla モジュールとディレクトリ体系にしたがっていたのなら、変更の必要はないのです。

移植性の高い例:

#include "filename.h"

Mac では boolean 式での代入について問題がある

Mac で警告を引き起こすまた一つのコードの例を示します:

警告を出す例:

if ((a = b) == c) ...

Mac は if 文での代入を好みません。たとえ括弧で正しくくくられていたとしてもです。

より移植性の高い例:

a=b;
if (a == c) ...

どのソースファイル名も重複してはならない

移植性の低いファイルツリー:

feature_x
    private.h
    x.cpp
feature_y
    private.h
    y.cpp

Mac コンパイラでは、どのファイル名も重複してはいけません。あるファイルが場所限定的にのみ使われるからといって、他の場所のヘッダファイルと同じ名前を使ってもいいと考えてはいけません。よくないのです。すべてのファイル名は異なっている必要があります。

移植性の高いファイルツリー:

  feature_x
      xprivate.h
      x.cpp
  feature_y
      yprivate.h
      y.cpp

一時的にコードの塊を無効にするためにはコメントよりも #if 0 を使う

移植性の低い例:

int
foo()
{
  ...
  a = b + c;
  /*
   * Not doing this right now.
  a += 87;
  if (a > b) (* have to check for the
                candy factor *)
    c++;
   */
  ...
}

すでにコメントを含むコードの塊を無効にしたくなってしまうのはしょっちゅうであるので、悪いアイディアなのです。コメントのネストが適切になっていることをあてにはできません。これは移植性から程遠いものです。例えば、/**/ の組を (**) の組に変えるなどの変な変更をする必要があります。そんなことは忘れてしまうでしょう。また、#ifdef NOTUSED を使うのもやめてください。使った日はいいとして、次の日に誰かが何の連絡もなくどこかで NOTUSED を定義するかもしれません。コードの塊を無効にするためのよりよい方法は #if 0#endif の組を使い、先頭にコメントを入れておくことです。もちろん、この類のことは、いつでも一時的なことで、ドキュメント目的にコードの塊を無効にするのはばかげたことです。

移植性の高い例:

int
foo()
{
  ...
  a = b + c;
#if 0
  /*<span class="remark"> Not doing this right now. </span>*/
  a += 87;
  if (a > b) /*<span class="remark"> have to check for the
                candy factor </span>*/
    c++;
#endif
  ...
}

 

コンパイラの警告を有効にして、警告のないコードを書く

これはもっとも重要な Tip かもしれません。寛大なコンパイラには気をつけろ!あるプラットフォームで警告を発生させるものは、他のプラットフォームではエラーとなりうるものです。警告を有効にしてください。警告のないコードを書いてください。それはあなたのためになります。

struct (または C++ での class ) 内でビットフィールドを使う場合、同じ型を用いる

(MSVC++ 8といった最近のコンパイラも含め)いくつかのコンパイラは、異なった型のビットフィールドを一緒に使用した場合、構造体を正確にパック出来ません。例えば、以下に示す構造体はサイズを1バイトとしてコンパイルされるべきですが、8バイトとしてコンパイルされてしまいます。 Some compilers (even recent ones like MSVC++ 8) mishandle code which uses different types for bitfields and fail to properly pack the bits, even when they should be packed. For example, the following struct might be miscompiled to have a size of 8 bytes, even though it fits in 1:

struct {
  char ch : 1;
  int i : 1;
};

ブール値を表すビットフィールドに対して(PRBool ではなく)符号なし型を使う

単一のビットでブール値を表したいならば、それを行うために符合なし型を使ってください。符合あり型を使うと(これには PRBool が含まれます)、設定されたとき値は +1 の代わりに -1 となり、これは XPCOM の慣習に反します。

改定履歴

  • 0.5 初期バージョン。 1998/3/27 David Williams
  • 0.6 "C++ 型のキャスト" と "mutable" の項目を追加。 1998/12/24 Ramiro Estrugo
  • 0.7 "nsCOMPtr" 項目と mozillaZine リソースへのリンクの追加。 1999/12/2 Ramiro Estrugo
  • 0.8 "予約語" の項目の追加。 2001/2/1 Scott Collins

更なる読み物

移植性の高い C++ のコードを書くために更なる助言を与えてくれる書籍とページをいくつか挙げておきます。

原文書の情報

 

ドキュメントのタグと貢献者

タグ: 
Contributors to this page: jezdez, Nog, Anz, Kohei
最終更新者: jezdez,