[ C++で開発 ]
C++のコーディング標準、スタイル、習慣について、思うところを主観にもとづいて勝手にぶちまけるページ。
特に石器時代のUNIXのC言語の風習だが、やたらめったら省略形を命名している。tmp, chk, prc, ctrl, con, 名前の長さに制限のあるコンパイラやリンカだった時代の名残りだけど、可読性を破壊しているだけ。省略形はやめよう。省略形の方が可読性が高いと思う人は要注意。
マクロのオンパレード。ちょっと面倒な(と本人が思う)コードをマクロに置き換えて悦に入る。一人方言みたいなものなので、可読性を破壊しているだけ。
やたらに条件コンパイルが多いのも困りもの。モードのお化けのようなコンパイル条件は可読性を破壊しているだけ。プログラムA用、プログラムB用と入り込むならクラスを分けようよ。条件コンパイルが認められるのは、デバッグモードとクロスプラットフォームなソースにおけるプラットフォーム固有なコードを切り替えるところまで。
定数をいまだにプリプロセッサマクロで定義して使用している旧態依然なコードがある。const変数を使用すべき。
プリプロセッサは、コンパイラの構文解析以前に勝手に置換されるだけなので、どんな副作用があるか分かったものではない。
int型変数にはi を、double型変数にはdを、ポインタ型ならさらにpを、と型を示す接頭辞を付ける命名。ユーザ定義型(クラス)を多用するC++では組み込み型だけに接頭辞をつけても有効性が低い。可読性は下がるし一貫性も下がるし。かといってクラスに一律cをつけるようなやり方はハンガリアンの心を忘れ形だけ真似るに過ぎない。いずれにせよ中途半端はよくない。
アプリケーション・ハンガリアンの必要性は、変数の命名が適切でないか変数のスコープが広すぎて類似変数がいくつも登場するのが原因と考える。適切な命名と極小なスコープで変数を使用するのがまず第一。
クラスのメンバ変数にはmを、グローバル変数にはgを、といったスコープを示す接頭辞を付ける命名。ひどく悪いというものではないが、わざわざ付けるほどのことはないと思う。変数が何のためのものか分かっていれば、当然スコープも分かっているはずなので接頭辞は余分。変数が何のためのものか分からないときは、変数の意味を調べることになるので、スコープだけ分かっても片手落ち。ということで、よいコードを書けば不要なもの。
そもそも、グローバル変数は使ってはいけない(百害あって一利なし)、メンバ変数はprivateにして派生クラスにもさらさない(アクセッサ関数をprotected/publicにして、派生クラスは基底クラスのメンバ変数は触らない)。
さらに亜種で、クラス名の先頭に'C'を付ける命名がある。とあるGUIフレームワークで採用している命名だが、名前空間が導入された標準C++ではもはや不要。型名も関数名も(変数名もか?)大文字で始めるから見て判断しにくいなど命名規約自体に破綻が見える。
C言語からの風習だが、いろいろなデータを表現する際にとにかくintを使おうとする。bool型も使わずintである。せめてenumを使うべきところもintになっていたりすることすらある。
C言語の貧弱なデータ構造に起因するが、データを格納するのにとにかく配列を使おうとする。標準C++ではSTLによってコンテナが提供されているが、自分が知っている知識のみで解決しようと配列一本槍ということが多い。配列で表現する副作用として、forループとif文の複雑なネストがあちこちに現れる。
コンパイラがエラーと見なさないので、関数呼び出し時にその戻り値を取らない(無視)するコードがある。
関数の結果が戻り値ではなく、引数で渡したアドレスに格納するというコーディングがC言語ではよく行われている。これは、戻り値にエラーを返す場合と、一部古い知識で構造体の値渡しが出来ないときの名残りかと思われる。
例外機構のない旧式言語での習慣であり、例外機構のある言語でこのルールを持ち出すのは、例外を使うなと言っているに等しい。
「契約による設計(DbC)」を使用すれば、関数の入り口で事前条件(引数に渡された値が有効範囲内かどうか)を検査して異常であれば即座に例外なりエラーで復帰するのが常套。正常処理中でも例外事象が発生すれば関数から抜けることもある。
オブジェクト指向プログラミングを中途半端にかじったプログラマの悪習で、メンバ関数の入出力を
クラスの定義には、public部・protected部・private部がある。これら全てをヘッダファイルに記述するが、それによって、本来クラス利用者には見せる必要のない部分まで見えてしまう。また、プログラムを変更するときに、クラス利用者に対するインタフェースは変更がなくてもprotected部、private部に変更があれば、クラス利用者に再コンパイルを強いる結果となる。
この問題の解消には、pimplイディオムを使用するか、インタフェース定義クラスの多重継承を使う方法がある。つまりは設計上で工夫しなければいけないということ。
標準I/Oストリーム(iostream)の命名は、型も操作もすべて小文字のアンダースコアなしで単語間を連結。略語多用。C++は標準からして命名がくさっている。
std::strstream, setprecision(), てな感じ
標準テンプレートライブラリ(STL)の命名は、型も操作もすべて小文字のアンダースコアありで単語間を連結。
deque, push_back()
変数名と型名の区別ができないのはやっかいだ。。。
C++ではおそるべきことにオブジェクトファイル(ライブラリファイル)の名前のマングリング規則がコンパイラ依存である。つまり、あるコンパイラで作成したオブジェクトファイルやライブラリファイルは、別なコンパイラ(時にはバージョンが異なる同じコンパイラ)で作ったものとリンクさせることができないのである。
3.4で"much closer to full conformance to the ISO/ANSI C++ standard"だそう。3.3まででは通ったコードも3.4では厳格になった部分あり。
まずこれは、ASCII文字専用といってよいもの。漢字を入れて表示されているように見えているのはたまたま。
stringをクラスのメンバ変数にしたときのアクセッサ関数は何がよいのか?
class Name { std::string name_; // ... void setName(const std::string& newName) { name_ = newName; } const std::string& getName() const { return name_; } };
このとき、setNameで引数に渡したstd::stringインスタンスは、std::stringの代入演算子でコピーされるので、setName呼び出し側がstd::stringインスタンスその後どう料理しようが影響は受けないと思われる。しかし、std::stringが中で管理する文字列格納アドレス(char*)はアドレスがコピーされているようだ。したがって、クラスのprivateなデータメンバが実は外部と共有される望ましからざる状況が発生してしまう。
getNameで取り出したstd::stringインスタンスは、Nameクラスのメンバ変数の参照なので、getName呼び出し者がstd::stringインスタンスをその後append()等を呼んでいじってしまうとNameクラスのオブジェクトは影響を受けてしまうと想定していた。しかし、実験してみたところ影響を受けていないことが分かった。append()前後でc_str()の戻り値が変わるので、どうやらappend呼び出しによって内部的に管理しているバッファが新しいものに更新されているようだ。しかし、クラスのprivateなデータメンバが実は外部と共有される状況には違いなく、好ましい状況ではない。
class Name { std::string name_; // ... void setName(const std::string newName) { name_ = newName; } const std::string getName() const { return name_; } };
アクセッサ関数はどちらも参照ではなく値コピーで受け渡しを行う。もともとstd::string自体コピーに対して効率のよい設計となっている(とのこと)。これであれば、クラスのprivateなデータメンバは外部とは隔離されるようになり安全性が高まる。
ということで、C++のクラス設計における内部データへのアクセッサ関数は、コピー渡しを基本とするのがよい。
C++でパフォーマンスを改善するための工夫。
C++の言語仕様の広さ(マルチパラダイム)を、コーディング規約で制約を付けるということは、コーディング規約を適用しようとするそのプロジェクトなり組織に特化したアプリケーション記述プログラミング言語を1つ決めるということに他ならない。Javaや他のプログラミング言語におけるコーディング規約と大いに違う点である。
例えば、例外の使用可否、テンプレートの使用可否、多重継承の使用可否、実行時型情報(RTTI)の使用可否、などの言語仕様の骨幹に関わる部分を規約で制約してしまえば、これはもうプログラミング言語としては別物になるといっても過言ではない。
もちろん、規約で制約を付けないと、C++の言語仕様が複雑怪奇な代物なので、ソフトウェア開発現場での適用に問題が生じます。