ここにまとめられているのは、私がよく尋ねられるC++のスタイルとテクニックに 関する質問です。もしよりよい質問や回答に対するコメントがあれば、遠慮なく私に メール(bs@research.att.com)でお知らせください。なお、ホームページを改善する ために、私はあまり時間を割けないということを心に留めておいてください。
より一般的な質問は、一般的なFAQを 参照してください。
用語や概念については、C++用語解説を 参照してください。
この文書の内容は、質問と回答をまとめただけのものであるということに注意して ください。よい教科書にみられるような、注意深く選ばれた例題と解説の代わりになる ものではありません。また、リファレンスマニュアルや規格書のような、詳細で正確な 仕様を提供するものでもありません。C++のデザインに関する質問は、C++の設計と進化(The Design and Evolution of C++)を参照してください。C++と標準ライブラリの使用法に関する 質問は、プログラミング 言語C++を参照してください。
一部のQ&Aに注釈をつけた 中国語訳があります。また、 こちらには別の中国語訳があります。
こちらに日本語訳があります。
[訳注] 日本語訳に関する情報は、「日本語訳について」を ご覧ください。
とても簡単なプログラムをどう書いたらよいかという質問を(特に学期の始まりに)よく 受けます。典型的なのは、数値をいくつか読み込んで何か処理し、その結果を出力する というものです。以下はそのサンプルプログラムです。
#include<iostream> #include<vector> #include<algorithm> using namespace std; int main() { vector<double> v; double d; while(cin>>d) v.push_back(d); // 要素を読み込む if (!cin.eof()) { // 入力に失敗したか確認する cerr << "format error\n"; return 1; // エラー } cout << "read " << v.size() << " elements\n"; reverse(v.begin(),v.end()); cout << "elements in reverse order:\n"; for (int i = 0; i<v.size(); ++i) cout << v[i] << '\n'; return 0; // 成功 }
このプログラムについて、以下に少し所見を述べます。
#include<iostream> #include<vector> #include<algorithm> #include<string> using namespace std; int main() { vector<double> v; double d; while(cin>>d) v.push_back(d); // 要素を読み込む if (!cin.eof()) { // 入力に失敗したか確認する cin.clear(); // エラー状態をクリアする string s; cin >> s; // 入力終了を示す文字列を探す if (s != "end") { cerr << "format error\n"; return 1; // エラー } } cout << "read " << v.size() << " elements\n"; reverse(v.begin(),v.end()); cout << "elements in reverse order:\n"; for (int i = 0; i<v.size(); ++i) cout << v[i] << '\n'; return 0; // 成功 }
簡単なことを簡単に済ます標準ライブラリの使用例がプログラミング言語C++第3版の 「標準ライブラリひとめぐり(Tour of the Standard Library)」の章(ダウンロード 可能)にありますので、そちらを参照してください。
C++コーディング標準の主な目的は、特定の環境の中で特定の目的を達成する ために、C++の使用方法に関する規則を定めることです。したがって、あらゆる使用 目的とすべてのユーザに適したコーディング標準がひとつしかないということは ありえません。特定のアプリケーション(あるいは、会社、アプリケーション分野 など)にとって、コーディング標準がないよりは、良いコーディング標準がある ほうが望ましいです。その一方で、悪いコーディング標準のせいで、コーディング 標準がないよりももっとひどくなるという例をこれまでに多く見たことがあります。
あなたが使用する規則は、アプリケーション分野の確かな知識をもとによく注意 して選んでください。いくつかの最もひどいコーディング標準(「罪にならないように」 名前を挙げませんが)は、C++についてのしっかりとした知識がなく、アプリケーション 分野に関して相対的に無知な人達(開発者というよりは「エキスパート」と呼ばれる 人達)と、制約が多い方が少ないよりもよいといった誤った確信を持った人達によって 書かれたものです。その誤った考えとは対照的に、プログラマがもっとひどい規則に 従わなければならないようにするものもあります。いずれにしても、安全性や生産性 などというものは、設計/開発プロセスを通して向上するものであるということを忘れ ないでください。個々の言語の機能、それどころか言語によって向上するものでは ありません。
以上のことに注意したうえで、3つのコーディング標準をおすすめします。
空白で終端した1単語は、次のように読み込むことができます。
#include<iostream> #include<string> using namespace std; int main() { cout << "Please enter a word:\n"; string s; cin>>s; cout << "You entered " << s << '\n'; }
明示的なメモリ管理とオーバーフローする可能性のある固定長バッファを使用 していないことに注目してください。
(1単語だけではなく)1行全体を読み込む必要があるなら、次のようにします。
#include<iostream> #include<string> using namespace std; int main() { cout << "Please enter a line:\n"; string s; getline(cin,s); cout << "You entered " << s << '\n'; }
iostreamやstringなどの標準ライブラリの機能の簡単な紹介が、プログラミング言語C++第3版の 第3章(オンラインで参照可能)にありますので、そちらを参照してください。 CとC++におけるI/Oの簡単な使用法を詳細に比較したものが、"Learning Standard C++ as a New Language"にあります。こちらは私の出版物一覧からダウンロード できます。
最も簡単な方法は、stringstreamを使用する方法です。
#include<iostream> #include<string> #include<sstream> using namespace std; string itos(int i) // intをstringに変換する { stringstream s; s << i; return s.str(); } int main() { int i = 127; string ss = itos(i); const char* p = ss.c_str(); cout << ss << " " << p << "\n"; }
もちろん、このテクニックは、<< を使用して文字列へ出力できる型であれば、 どのような型でも変換できます。文字列ストリームの詳細は、プログラミング言語C++の21.5.3を 参照してください。
Cと同様に、C++では配置の仕方は定義していません。ただ、満足させなければ ならない意味制約(semantic constraints)だけ定義しています。したがって、実装が 異なれば、異なる配置の仕方をします。私が知っている中で最も優れた解説が、 The Annotated C++ Reference Manual(一般にARMと呼ばれている)の中でされていますが、残念ながら、それ以外の 内容は古く、最新のC++の実装に関する記述はありません。その解説には重要な配置の 例を示す図が載っています。また、非常に簡単な説明がプログラミング言語C++の第2章に あります。
基本的にC++では、単純にサブオブジェクトをつなげてオブジェクトを構成します。 したがって、次に示すコードは、
struct A { int a,b; };ふたつのintを並べて表現され、次に示すコードは、
struct B : A { int c; };Aの後にintを並べて表現されます。したがって、3個のintを並べて表現されます。
仮想関数は一般的に、仮想関数を持つクラスの各オブジェクトにポインタ(vptr)を 持たせることで実現しています。このポインタは適切な関数テーブル(vtbl)を指します。 各クラスは独自のvtblを持ち、そのクラスのすべてのオブジェクトで共有します。
"this"がC++(実際はC with Classes)に導入されたのが、リファレンスが導入される 前だったからです。ちなみに、Smalltalkの"self"ではなく、Simulaに ならって"this"を選びました。
ふたつの異なるオブジェクトのアドレスが異なるものになるように保証するため です。同じ理由で、"new"は常に異なるオブジェクトを返します。次のコードを考え てみてください。
class Empty { }; void f() { Empty a, b; if (&a == &b) cout << "ありえない: コンパイラ供給者にエラーを報告せよ"; Empty* p1 = new Empty; Empty* p2 = new Empty; if (p1 == p2) cout << "ありえない: コンパイラ供給者にエラーを報告せよ"; }
また、空の基底クラスは独立したバイトで表現する必要がないという興味深い 規則があります。
struct X : Empty { int a; // ... }; void f(X* p) { void* p1 = p; void* p2 = &p->a; if (p1 == p2) cout << "すばらしい: よいオプティマイザだ"; }
この最適化は安全であり、とても役に立ちます。プログラマは空のクラスを 使って、とても単純な概念をオーバーヘッドなしに表現することができます。 最新のコンパイラの中には、この「空の基底クラスの最適化(empty base class optimization)」機能を提供しているものもあります。
配列の範囲のような定数式の中で使用できる定数が必要な場合、ふたつの選択肢 があります。
class X { static const int c1 = 7; enum { c2 = 19 }; char v1[c1]; char v2[c2]; // ... };
一見したところ、c1の宣言の方がすっきりしているように見えますが、このクラス 内初期化構文は、定数式で初期化された整数型のstatic constか列挙型でなければ なりません。これはかなりの制限です。
class Y { const int c3 = 7; // エラー: staticではない static int c4 = 7; // エラー: constではない static const float c5 = 7; // エラー: 整数ではない };
私は「enumトリック」をよく使います。なぜなら、移植性が高く、非標準拡張の クラス内初期化構文を使う気にさせないからです。
それでは、なぜこのような不便な制限があるのでしょうか。クラスは一般的に ヘッダファイルの中で宣言され、ヘッダファイルは多くの翻訳単位(translation units)にインクルードされます。しかし、複雑なリンカ規則を避けるために、C++は すべてのオブジェクトが一意な定義を持つことを要求します。もし、C++がオブジェクト としてメモリに格納する必要のある要素のクラス内定義を認めれば、この規則に違反 することになるでしょう。C++の設計におけるトレードオフに関する説明は、C++の設計と進化を参照して ください。
constを定数式の中で使う必要がなければ、もっと柔軟性を得ることができます。
class Z { static char* p; // 定義で初期化する const int i; // コンストラクタで初期化する public: Z(int ii) :i(ii) { } }; char* Z::p = "hello, there";
クラスの外で定義されている場合に限り、staticメンバのアドレスを取得できます。
class AE { // ... public: static const int c6 = 7; static const int c7 = 31; }; const int AE::c7; // 定義 int f() { const int* p1 = &AE::c6; // エラー: c6は左辺値ではない const int* p2 = &AE::c7; // OK // ... }
簡単な答えは「もちろん呼ばれますよ!」ですが、この手の質問によく添えられて くるプログラム例を見てみましょう。
void f() { X* p = new X; // pを使用する }つまり、"new"で生成されたオブジェクトが、関数の終わりで解放される という(誤った)想定をしています。
基本的に、オブジェクトを生成したスコープの生存期間を越えてオブジェクトを 存在させておきたいときだけ"new"を使用するべきです。使用後は、"delete"でその オブジェクトを解放する必要があります。次に例を示します。
X* g(int i) { /* ... */ return new X(i); } // Xはg()の呼び出しを越えて存在する void h(int i) { X* p = g(i); // ... delete p; }スコープの中だけでオブジェクトを存在させたい場合は、"new"を使用せずに、単に 変数を定義します。
{ ClassName x; // xを使用する }変数はスコープの終わりで暗黙のうちに解放されます。
newを使用してオブジェクトを生成し、その同じスコープの終わりで解放する コードは、醜く、誤りを犯しやすく、効率が悪いです。たとえば、次のような コードです。
void fct() // 醜く、誤りを犯しやすく、効率が悪い { X* p = new X; // pを使用する delete p; }
いいえ、違反しません。"friend"はアクセスを許可するための明示的なメカニズム で、メンバシップのようなものです。(標準規格に準拠したプログラムでは)ソースの 変更なしにクラスへのアクセスを許可することはできません。以下に例を示します。
class X { int i; public: void m(); // X::m()にアクセスを許可する friend void f(X&); // f(X&)にアクセスを許可する // ... }; void X::m() { i++; /* X::m()はX::iにアクセスできる */ } void f(X& x) { x.i++; /* f(X&)はX::iにアクセスできる */ }
C++の保護モデルに関する説明は、C++の設計と進化の2.10とプログラミング言語C++の11.5, 15.3, C.11を参照してください。
この質問はいろいろとかたちを変えてやってきます。たとえば、次のような 質問です。
struct Point { int x,y; Point(int xx = 0, int yy = 0) :x(xx), y(yy) { } }; Point p1(1,2); Point p2 = p1;ここで、p2.x==p1.x と p2.y==p1.y になります。多くの場合、これはまさに期待した 通りの結果(であり、Cとの互換性にとって不可欠)ですが、次の例を考えてみてください。
class Handle { private: string name; X* p; public: Handle(string n) :name(n), p(0) { /* X を獲得し、p にそれを指させる */ } ~Handle() { delete p; /* X を解放する */ } // ... }; void f(const string& hh) { Handle h1(hh); Handle h2 = h1; // めちゃくちゃな状態になる! // ... }ここで、デフォルトコピーにより、h2.name==h1.name と h2.p==h1.p になります。 これは最悪の事態をもたらします。f()を終了したとき、h1 と h2 のデストラクタ が起動され、h1.p と h2.p が指しているオブジェクトが2度削除されてしまいます。
これを避けるにはどうすればよいでしょうか。最も簡単な解決方法は、コピーする オペレーションを非公開にしてコピーを防ぐという方法です。
class Handle { private: string name; X* p; Handle(const Handle&); // コピーを防ぐ Handle& operator=(const Handle&); public: Handle(string n) :name(n), p(0) { /* X を獲得し、p にそれを指させる */ } ~Handle() { delete p; /* X を解放する */ } // ... }; void f(const string& hh) { Handle h1(hh); Handle h2 = h1; // エラー (コンパイラにより報告される) // ... }コピーする必要がある場合は、もちろん、目的とするセマンティクスを用意する ために、コピーイニシャライザとコピー代入を定義することができます。
さて、Point に戻りましょう。Point にとって、デフォルトのコピーセマンティクス に問題はありません。問題はコンストラクタにあります。
struct Point { int x,y; Point(int xx = 0, int yy = 0) :x(xx), y(yy) { } }; void f(Point); void g() { Point orig; // デフォルト値 (0,0) を持つ orig を生成する Point p1(2); // デフォルト y 座標値 0 を持つ p1 を生成する f(2); // Point(2,0) を呼び出す }orig と p1 で使用されている利便性を得るために、デフォルト引数が用意されて います。その結果、f() の呼び出し時に 2 が Point(2,0) に変換されることに驚く 人がいるかもしれません。引数をひとつ取るコンストラクタは変換を定義します。 これはデフォルトで暗黙の変換となります。このような変換を明示させるには、 コンストラクタを explict と宣言します。
struct Point { int x,y; explicit Point(int xx = 0, int yy = 0) :x(xx), y(yy) { } }; void f(Point); void g() { Point orig; // デフォルト値 (0,0) を持つ orig を生成する Point p1(2); // デフォルト y 座標値 0 を持つ p1 を生成する // これはコンストラクタの明示的な呼び出しである f(2); // エラー (暗黙の変換を試みた) Point p2 = 2; // エラー (暗黙の変換を試みた) Point p3 = Point(2); // OK (明示的な変換) }
あなたのコンパイラに問題があるのかもしれません。コンパイラが古いか、うまく インストールされていないか、あるいは、あなたのコンピュータが旧式なのかもしれ ません。そのような問題にはお役に立てません。
しかし、あなたがコンパイルしようとしているプログラムの設計がまずいので、 コンパイラがコンパイル時に何百ものヘッダファイルと何万行ものコードを解析 している可能性が高いです。原理的には、これを避けることができます。もしこの 問題があなたが使用しているライブラリのベンダによる設計にあるなら、(もっと良い ライブラリ/ベンダに変える以外に)あなたにできることはあまりありません。しかし、 あなた自身のコードなら、変更後の再コンパイルが最小限となるように構成することが できます。そのような設計は関心事項をうまく分割させるので、一般的により優れた、 メインテナンス可能な設計となります。
次のようなオブジェクト指向プログラムの典型的な例を考えてみてください。
class Shape { public: // Shapeの利用者へのインタフェイス virtual void draw() const; virtual void rotate(int degrees); // ... protected: // 共通データ (Shapeの実装者向け) Point center; Color col; // ... }; class Circle : public Shape { public: void draw() const; void rotate(int) { } // ... protected: int radius; // ... }; class Triangle : public Shape { public: void draw() const; void rotate(int); // ... protected: Point a, b, c; // ... };上記のコードの目的は、利用者はShapeの公開インタフェイスを通して図形を操作 し、(CircleやTriangleのような)導出クラスの実装者は、protectedメンバで表現 された実装のアスペクトを共有する、というものです。
この一見単純そうな考えには、三つの重大な問題があります。
したがって、利用者へのインタフェイスとしての働きもする基底クラスの「実装 者に有用な情報」は、実装の不安定さ、(実装情報が変ったときの)利用者コードの不要 な再コンパイル、そして、利用者コードへのヘッダファイルの過剰なインクルード(「 実装者に有用な情報」がそれらのヘッダファイルを必要とするため)の原因となります。 これは「不安定な基底クラス問題」としても知られています。
明らかな解決方法は、「実装者に有用な情報」を利用者へのインタフェイスとして 使用されているクラスから取り除くことです。つまり、インタフェイス、純粋なインタ フェイスを作成するのです。そして、インタフェイスを抽象クラスで表現するのです。
class Shape { public: // Shapeの利用者へのインタフェイス virtual void draw() const = 0; virtual void rotate(int degrees) = 0; virtual Point center() const = 0; // ... // データなし }; class Circle : public Shape { public: void draw() const; void rotate(int) { } Point center() const { return cent; } // ... protected: Point cent; Color col; int radius; // ... }; class Triangle : public Shape { public: void draw() const; void rotate(int); Point center() const; // ... protected: Color col; Point a, b, c; // ... };これで、利用者は導出クラスの実装の変更に影響を受けません。私はこのテクニック で、ビルド回数を数桁減少させたことがあります。
しかしながら、すべての(あるいは単にいくつかの)導出クラスに共通な情報が 実際にあるとしたらどうでしょうか。その場合は、単純にその情報をクラスにして、 そのクラスからも継承するようにしてください。
class Shape { public: // Shapeの利用者へのインタフェイス virtual void draw() const = 0; virtual void rotate(int degrees) = 0; virtual Point center() const = 0; // ... // データなし }; struct Common { Color col; // ... }; class Circle : public Shape, protected Common { public: void draw() const; void rotate(int) { } Point center() const { return cent; } // ... protected: Point cent; int radius; }; class Triangle : public Shape, protected Common { public: void draw() const; void rotate(int); Point center() const; // ... protected: Point a, b, c; };
その必要はありません。インタフェイスの中にデータを置きたくないなら、インタ フェイスを定義しているクラスにそのデータを置かないでください。代わりに導出 クラスに置いてください。なぜコンパイルにこんなに 時間がかかるのでしょうかを参照してください。
時には、クラスの中にデータ表現を置きたくなることもあります。 次の複素数クラスを考えてみてください。
template<class Scalar> class complex { public: complex() : re(0), im(0) { } complex(Scalar r) : re(r), im(0) { } complex(Scalar r, Scalar i) : re(r), im(i) { } // ... complex& operator+=(const complex& a) { re+=a.re; im+=a.im; return *this; } // ... private: Scalar re, im; };この型は、組み込み型とほぼ同じように使用されるように設計されています。純粋な ローカルオブジェクト(つまり、ヒープ上ではなく、スタック上に割り当てられる オブジェクト)が生成できて、単純な演算が適切にインライン化されることを保証する ために、宣言の中にデータ表現が必要になります。純粋なローカルオブジェクトと インライン化は、複素数クラスのパフォーマンスを言語に組み込みの複素数型に 近づけるのに欠かせません。
多くのクラスが基底クラスとして使われるように設計されるわけではないからです。 例として、複素数クラスを参照してください。
また、仮想関数を持つクラスのオブジェクトは、仮想関数呼び出しメカニズムに 必要なメモリ空間を必要とします(一般的にひとつのオブジェクトに対して1ワード 必要になる)。このオーバーヘッドが重大であることもありますし、他の言語(たとえ ばCとFortran)のデータとのメモリレイアウトの互換性の問題になることもあります。
設計の理論的根拠については、C++の設計と進化を参照して ください。
仮想呼び出し(virtual call)は、不完全な情報をもとに動作する仕組みです。 具体的に言うと、"virtual"を使うことで、インタフェイスだけが分かっていて、 オブジェクトの正確な型が分からない関数を呼び出すことができます。 オブジェクトを生成するには、情報がすべて揃っている必要があります。特に、 生成したいオブジェクトの正確な型を知っている必要があります。 したがって、「コンストラクタの呼び出し」を仮想(virtual)にすることはでき ません。
オブジェクトの生成に間接的な方法を使用するテクニックはよく、仮想コンスト ラクタと呼ばれています。その例は、プログラミング言語C++第3版の15.6.2を参照 してください。
たとえば、以下に示すのは、抽象クラスを使用して適切な型のオブジェクトを 生成するテクニックです。
struct F { // オブジェクト生成関数へのインタフェイス virtual A* make_an_A() const = 0; virtual B* make_a_B() const = 0; }; void user(const F& fac) { A* p = fac.make_an_A(); // 適切な型の A を生成する B* q = fac.make_a_B(); // 適切な型の B を生成する // ... } struct FX : F { A* make_an_A() const { return new AX(); } // AX は A から導出されている B* make_a_B() const { return new BX(); } // BX は B から導出されている }; struct FY : F { A* make_an_A() const { return new AY(); } // AY は A から導出されている B* make_a_B() const { return new BY(); } // BY は B から導出されている }; int main() { FX x; FY y; user(x); // このuserはAXとBXを生成する user(y); // このuserはAYとBYを生成する user(FX()); // このuserはAXとBXを生成する user(FY()); // このuserはAYとBYを生成する // ... }これは、「ファクトリパターン」と呼ばれるものの一種です。 ポイントは、user()は AX や AY のようなクラスの情報とは完全に切り離されている というところです。
多くのクラスが基底クラスとして使われるように設計されるわけではないからです。 仮想関数は、導出クラスのオブジェクトへのインタフェイスの働きをするクラスの中 でだけ意味をなします(そのオブジェクトは一般的に、ヒープ上に割り当てられ、 ポインタかリファレンスを通してアクセスされる)。
それでは、いつデストラクタを仮想関数と宣言したらよいのでしょうか。クラス に少なくともひとつ仮想関数があるときはいつでもそうしてください。クラスに仮想 関数があるということは、そのクラスが導出クラスへのインタフェイスの働きをする ことのしるしであり、その場合は、導出クラスのオブジェクトは、基底クラスの ポインタを介して解体されるかもしれません。 たとえば、次のコードを見てください。
class Base { // ... virtual ~Base(); }; class Derived : public Base { // ... ~Derived(); }; void f() { Base* p = new Derived; delete p; // ~Derivedが確実に呼び出されるように仮想デストラクタが使われる }Baseのデストラクタが仮想関数でなかったら、Derivedのデストラクタは呼び出され ません。これは、Derivedのオブジェクトが所有するリソースが解放されないという ような、悪い影響をもたらすことになるでしょう。
純粋仮想関数とは、導出クラスでオーバーライドしなければならない関数で、 定義する必要はありません。仮想関数は、"=0"という奇妙な構文を使用して 「純粋(pure)」であると宣言されます。次の例を見てください。
class Base { public: void f1(); // 仮想関数ではない virtual void f2(); // 仮想関数であるが、純粋仮想関数ではない virtual void f3() = 0; // 純粋仮想関数 }; Base b; // エラー: 純粋仮想関数 f3 がオーバーライドされていないここで、Baseは(純粋仮想関数を持つため)抽象クラスになるので、Baseクラスの オブジェクトを直接生成することはできません。つまり、Baseは(明示的に)基底 クラスになります。次の例を見てください。
class Derived : public Base { // f1なし: 問題なし // f2なし: 問題なし。Base::f2から継承する void f3(); }; Derived d; // OK: Derived::f3はBase::f3をオーバーライドする抽象クラスはインタフェイスを定義するのにとても役立ちます。実際、純粋仮想関数 だけを持つクラスはインタフェイスとよく呼ばれます。
純粋仮想関数は定義できます。
Base::f3() { /* ... */ }これは(導出クラスに単純で共通な実装を提供するのに)時折役立ちます。しかし、 導出クラスでは依然としてオーバーライドしなければなりません。
導出クラスで純粋仮想関数を定義しない場合、その導出クラスは抽象クラスに なります。
class D2 : public Base { // f1なし: 問題なし // f2なし: 問題なし。Base::f2から継承する // f3なし: 問題ないが、D2 は抽象クラスのまま }; D2 d; // エラー: 純粋仮想関数Base::f3がオーバーライドされていない
できますが、注意してください。あなたが期待した通りには動作しないかもしれ ません。コンストラクタの処理中は、導出クラスのオーバーライドがまだされて いない状態なので、仮想呼び出しメカニズムは無効にされています。オブジェクトは 基底クラスから構築されます。「導出クラスの前に基底クラス」です。
次の例を考えてみてください。
#include<string> #include<iostream> using namespace std; class B { public: B(const string& ss) { cout << "B constructor\n"; f(ss); } virtual void f(const string&) { cout << "B::f\n";} }; class D : public B { public: D(const string & ss) :B(ss) { cout << "D constructor\n";} void f(const string& ss) { cout << "D::f\n"; s = ss; } private: string s; }; int main() { D d("Hello"); }このプログラムは次のように出力します。
B constructor B::f D constructorD::fでないことに注意してください。 もし規則が異なり、B::B()からD::f()が呼び出されるとしたらどうなるか考えてみて ください。コンストラクタD::D()はまだ実行されていないので、D::f()は引数をまだ 初期化されていないstring sに代入しようとするでしょう。その結果、おそらくすぐに クラッシュするでしょう。
オブジェクトの解体は「基底クラスの前に導出クラス」の順で行われます。 したがって、仮想関数はコンストラクタの中と同じように振る舞います。つまり、 ローカルな定義だけが使用されます - (今解体されている)オブジェクトの導出クラス の部分に触れないようにオーバーライドした関数への呼び出しは行われません。
詳細は、C++の設計と 進化の13.2.4.2、 あるいはプログラミング 言語C++第3版の15.4.3を参照してください。
この規則は実装が作り出したものだと言われたことがありますが、そうではあり ません。実際、コンストラクタからの仮想関数呼び出しを他の関数からの呼び出しと まったく同じようにするという、安全でない規則を実装することは非常に簡単でしょう。 しかし、それは基底クラスで確立された不変条件に依存して仮想関数を書けないという ことを暗に示しています。それはとんでもないことです。
できますが、なぜそうしたいのでしょうか。一般的にふたつの理由があります。
仮想関数呼び出しを避けるためにクラス階層に「制限」を設ける必要性が本当に あるなら、そもそもなぜ仮想関数にするのかと言う人がいるかもしれません。私は、 パフォーマンスが重要な関数を大した理由もなく単に「いつもそうしているから」 というだけで仮想関数にしている例を見たことがあります。
もう一方の、論理的な理由があって導出させないようにする、という問題には 解決方法があります。残念ながら、その方法は美しくありません。それは、クラス 階層中の最下層の導出クラスは仮想基底を構築しなければならないということに 依存しています。以下に例を示します。
class Usable; class Usable_lock { friend class Usable; private: Usable_lock() {} Usable_lock(const Usable_lock&) {} }; class Usable : public virtual Usable_lock { // ... public: Usable(); Usable(char*); // ... }; Usable a; class DD : public Usable { }; DD dd; // エラー: DD::DD()はプライベートメンバの // Usable_lock::Usable_lock()にアクセスできない(C++の設計と進化 11.4.3より)
議論を単純化してしまいましたが、これはFAQであって、学術論文ではありません。
そんなことはありません。代わりの方法を用いることで単一継承がなくても プログラミングができるのと同様に、多重継承がなくてもプログラミングできます。 それどころか、クラスすらなくてもプログラミングできます。Cがそれを証明しています。 しかし、静的型検査と継承を持つ現代のどの言語も何らかの形式の多重継承機能を 提供しています。C++では抽象クラスがインタフェイスとしての役割をしばしば果し、 クラスは多くのインタフェイスを持つことができます。「多重継承をサポートしていない」 とみなされているその他の言語では、純粋抽象クラスに相当するものに別名を 与えているだけです。それはインタフェイスと呼ばれています。 言語が継承(単一継承と多重継承の両方)を提供する理由は、言語がサポートする 継承は一般的に、継承を回避する方法(たとえば、サブオブジェクトや別々に割り当て られたオブジェクトへのフォワーディング関数の使用)よりも、プログラミングを 容易にすることや、論理的な誤りをみつけること、保守のしやすさ、そして しばしば効率の点で優れているからです。
この質問は(多くのバリエーションで)ふつう次のようなコード例で示されます。
#include<iostream> using namespace std; class B { public: int f(int i) { cout << "f(int): "; return i+1; } // ... }; class D : public B { public: double f(double d) { cout << "f(double): "; return d+1.3; } // ... }; int main() { D* pd = new D; cout << pd->f(2) << '\n'; cout << pd->f(2.3) << '\n'; }これは次のように出力します。
f(double): 3.3 f(double): 3.6しかし、次のように出力されると(誤って)予想する人もいます。
f(int): 3 f(double): 3.6
別の言い方をすれば、D と B の間にはオーバーロードの解決はありません。 コンパイラは D のスコープを調べ、関数"double f(double)"をひとつ見つけ、 それを呼び出します。B の(閉じた)スコープに惑わされることは決してありません。 C++では、スコープを跨いだオーバーロードはありません。この一般的な規則に ついては、導出クラスのスコープも例外ではありません(詳細は、C++の設計と進化、 あるいはプログラミング 言語C++第3版を参照してください)。
しかし、基底クラスと導出クラスからすべてのf()をオーバーロードしたいと したらどうするのでしょうか。それは、using宣言を使用することで簡単に行えます。
class D : public B { public: using B::f; // Bのすべてのf()を利用可能にする double f(double d) { cout << "f(double): "; return d+1.3; } // ... };上記のように変更すると、次のように出力されます。
f(int): 3 f(double): 3.6オーバーロードの解決は、最も適したf()を選択するために、Bのf()とDのf()に適用 されました。
まあ使えるでしょうが、やみくもに使用しないでください。他にもっとよい 方法があることの方が多いです。次のコードを考えてみてください。
void compute(cmplx z, double d) { cmplx z2 = z+d; // C++スタイル z2 = f(z2); // z2を使用する cmplx& z3 = *new cmplx(z+d); // Javaスタイル(Javaが + 演算子をオーバーロードできると仮定) z3 = f(z3); delete &z3; }
z3に対する"new"の使用はぎこちなく、無駄であり、ローカル変数の慣用的な 使い方(z2)と比べても遅くなります。同じスコープでオブジェクトを"delete"する のであれば、オブジェクトの生成に"new"を使用する必要はありません。そのような オブジェクトはローカル変数にすべきです。
いえ、できます。その方法はとても簡単で一般的です。
次のコードを考えてみてください。
template<class Container> void draw_all(Container& c) { for_each(c.begin(),c.end(),mem_fun(&Shape::draw)); }もし型エラーがあるとすれば、それはかなり複雑なfor_each()呼び出しの解決の中に あるはずです。たとえば、コンテナの要素の型がintである場合、for_each()呼び出し に関連したある種のわかりにくいエラーが起こります(intに対してShape::draw()を 起動できないため)。
このようなエラーを早い段階で見つけるために、次のように書くことができます。
template<class Container> void draw_all(Container& c) { Shape* p = c.front(); // Shape*のコンテナだけ受け入れる for_each(c.begin(),c.end(),mem_fun(&Shape::draw)); }仮の変数 p を初期化することで、最新のコンパイラの多くは理解しやすいエラー メッセージを出力するようになります。このようなトリックは、あらゆる言語でよく 用いられていて、新しい構文のためには作られなければならないものです。 プロダクションコードの中では、私はおそらく次のように書くでしょう。
template<class Container> void draw_all(Container& c) { typedef typename Container::value_type T; Can_copy<T,Shape*>(); // Shape*だけのコンテナを受け入れる for_each(c.begin(),c.end(),mem_fun(&Shape::draw)); }これでアサーションしていることがはっきりします。 Can_copyテンプレートは次のように定義できます。
template<class T1, class T2> struct Can_copy { static void constraints(T1 a, T2 b) { T2 c = a; b = a; } Can_copy() { void(*p)(T1,T2) = constraints; } };Can_copyは、T1がT2に代入できることを(コンパイル時に)検査します。 Can_copy<T,Shape*>は、T がShape*であるか、Shapeからpublic継承したクラス のポインタか、Shape*へのユーザ定義の変換を持つ型であるかを検査します。 定義は最小限に近いことに注目してください。各行は以下のことを行っています。
さらに、この定義には次のような好ましい特性があります。
それでは、なぜCan_copy()のような(あるいはもっと簡潔な)ものが言語に含まれて いないのでしょうか。C++の 設計と進化には、一般的な制約を表現することの難しさに関する分析が含まれて います。それ以来、これらの制約クラスを簡単に書けて、分りやすいエラーメッセージ を出力する、多くのアイデアが出てきました。たとえば、先程のCan_copyの中で行った 関数ポインタを使用する方法は、Alex StepanovとJeremy Siekが考えだしたものだと 思います。Can_copyが標準になるにはまだ早いと思います。もっと使用される必要が あります。 さらに、C++コミュニティでは別の形式の制約が使われています。広範囲にわたる 使用において、どのような形式の制約テンプレートが最も効果的であるかということ には、まだ統一した見解は得られていません。
しかし、そのアイデアは非常に一般的であり、制約の検査のために具体的に提案、 提供された言語機能よりも一般的です。いずれにしても、テンプレートを記述する ときには、私たちはC++の表現力を最大限に利用できます。 次の例を考えてみてください。
template<class T, class B> struct Derived_from { static void constraints(T* p) { B* pb = p; } Derived_from() { void(*p)(T*) = constraints; } }; template<class T1, class T2> struct Can_copy { static void constraints(T1 a, T2 b) { T2 c = a; b = a; } Can_copy() { void(*p)(T1,T2) = constraints; } }; template<class T1, class T2 = T1> struct Can_compare { static void constraints(T1 a, T2 b) { a==b; a!=b; a<b; } Can_compare() { void(*p)(T1,T2) = constraints; } }; template<class T1, class T2, class T3 = T1> struct Can_multiply { static void constraints(T1 a, T2 b, T3 c) { c = a*b; } Can_multiply() { void(*p)(T1,T2,T3) = constraints; } }; struct B { }; struct D : B { }; struct DD : D { }; struct X { }; int main() { Derived_from<D,B>(); Derived_from<DD,B>(); Derived_from<X,B>(); Derived_from<int,B>(); Derived_from<X,int>(); Can_compare<int,float>(); Can_compare<X,B>(); Can_multiply<int,float>(); Can_multiply<int,float,double>(); Can_multiply<B,X>(); Can_copy<D*,B*>(); Can_copy<D,B*>(); Can_copy<int,B*>(); } // 典型的な「要素はMybaseから導出されたものでなければならない」という制約 template<class T> class Container : Derived_from<T,Mybase> { // ... };実のところ、Derived_fromは導出を検査しているのではなく、変換できるかを検査 しています。しかし、これはしばしば良い制約になります。制約に良い名前を付ける のは難しいことです。
型システムに欠陥が生じるからです。次の例をみてください。
class Apple : public Fruit { void apple_fct(); /* ... */ }; class Orange : public Fruit { /* ... */ }; // Orangeにはapple_fct()がない vector<Apple*> v; // Appleのvector void f(vector<Fruit*>& vf) // 無害なFruit操作関数 { vf.push_back(new Orange); // orangeをfruitのvectorに追加する } void h() { f(v); // エラー: vector<Apple*>をvector<Fruit*>として渡すことはできない for (int i=0; i<v.size(); ++i) v[i]->apple_fct(); }もしf(v)の呼び出しが認められていたら、OrangeがAppleとして振る舞うことになって しまうでしょう。
他の言語設計を選択して安全でない変換を認めても、実行時検査には頼ることに なります。vの要素へアクセスするたびに実行時検査が必要になり、h()はvの最後の 要素にアクセスした時点で例外をスローしなければなりません。
いいえ。ジェネリクスとはそもそも抽象クラスの糖衣構文(syntactic sugar)です。 つまり、(JavaであろうとC#であろうと)ジェネリクスでは、正確に定義されたインタ フェイスに対してプログラムし、一般的に仮想関数呼び出しと動的キャストの両方の コスト、あるいはそのどちらかがかかります。
テンプレートはジェネリックプログラミングやテンプレートメタプログラミング などを、整数テンプレート引数、特殊化、組み込み型とユーザ定義型の統一的な取り 扱い、といった機能を組み合わせてサポートします。その結果、「ジェネリクス」 とは比べものにならないほどの柔軟性、一般性、パフォーマンスを得ています。STLが その最高の例です。
柔軟性を得る代わりに、エラーの検出が遅れたり、凄まじく分りづらいエラー メッセージを出力するといった、あまり望ましくない結果がもたらされます。これに ついては現在、制約クラスで間接的に取り上げられて いますし、C++0xのコンセプトで直接取り上げられるでしょう。(私の出版物と提案、そして、標準化委員会のサイトにあるすべての 提案を参照してください)。
初心者への回答:
qsort(array,asize,sizeof(elem),elem_compare);はかなり奇妙で、
sort(vec.begin(),vec.end());よりも理解しにくいです。
struct Record { string name; // ... }; struct name_compare { // "name"をキーとしてRecordsを比較する bool operator()(const Record& a, const Record& b) const { return a.name<b.name; } }; void f(vector<Record>& vs) { sort(vs.begin(), vs.end(), name_compare()); // ... }しかも、多くの人が、sort()は型安全であり、使用するのにキャストを必要とせず、 標準の型の場合にはcompare()関数を書く必要がないということを評価しています。
より詳しい解説は、私の論文"Learning C++ as a New language"を参照して ください。私の出版物 一覧からダウンロードできます。
sort()がqsort()よりも性能が優れていることが多いのは、インライン化がうまく 行われるからです。
関数オブジェクトとは、何らかの方法で関数のように振る舞うオブジェクトの ことです。一般的には、適用演算子operator()を定義しているクラスのオブジェクト のことを意味します。
関数オブジェクトは関数よりも一般的な概念です。なぜなら、(静的ローカル変数 のように)複数回の呼び出しにわたって保持される状態を持つことができ、(静的 ローカル変数とは異なり)その状態をオブジェクトの外から初期化することや調べる ことができるからです。以下に例を示します。
class Sum { int val; public: Sum(int i) :val(i) { } operator int() const { return val; } // 値を取り出す int operator()(int i) { return val+=i; } // 適用 }; void f(vector<int> v) { Sum s = 0; // 初期値 0 s = for_each(v.begin(), v.end(), s); // 全要素の値を合計する cout << "the sum is " << s << "\n"; // 次のようにすることもできる cout << "the sum is " << for_each(v.begin(), v.end(), Sum(0)) << "\n"; }インライン定義された適用演算子を持つ関数オブジェクトは、オプティマイザを 混乱させる可能性のあるポインタがないため、きちんとインライン化されるという ことに注目してください。現在のオプティマイザは、関数へのポインタを介した 呼び出しをインライン化することがほとんど(絶対に?)できません。
関数オブジェクトは標準ライブラリの中で、柔軟性を提供するために広範囲に わたって使用されています。
auto_ptrはとても単純なハンドルクラスの一例で、<memory>の中で定義され、 「リソース取得は初期化である」テクニックを使用して 例外安全性をサポートします。auto_ptrはポインタを保持し、ポインタのように使用 することができ、スコープの終わりで保持しているオブジェクトを解放します。 以下に例を示します。
#include<memory> using namespace std; struct X { int m; // .. }; void f() { auto_ptr<X> p(new X); X* q = new X; p->m++; // ポインタのようにpを使用する q->m++; // ... delete q; }...の部分で例外がスローされた場合、pが保持しているオブジェクトはauto_ptrの デストラクタによって正しく解放されます。一方、qが指しているXは、メモリリーク を起こします。詳細は、プログラミング言語C++の14.4.2を参照してください。
auto_ptrは非常に軽量なクラスです。具体的に言うと、これはリファレンスカウ ンタ方式のポインタでは「ありません」。auto_ptrを別のauto_ptrに「コピー」した 場合、代入先のauto_ptrがポインタを保持し、代入元のauto_ptrは0を保持します。 以下に例を示します。
#include<memory> #include<iostream> using namespace std; struct X { int m; // .. }; int main() { auto_ptr<X> p(new X); auto_ptr<X> q(p); cout << "p " << p.get() << " q " << q.get() << "\n"; }上記のコードは、次に示すように、0ポインタの後に非0ポインタを表示するはずです。
p 0x0 q 0x378d0auto_ptr::get()は保持しているポインタを返します。
この「移動セマンティクス」は、通常の「コピーセマンティクス」とは異なり、 意外でしょう。特に、auto_ptrを標準コンテナの要素として絶対に使用しないで ください。標準コンテナは通常のコピーセマンティクスを要求します。 以下に例を示します。
std::vector<auto_ptr<X> >v; // エラーauto_ptrは配列へのポインタではなく、単一の要素へのポインタを保持します。
void f(int n) { auto_ptr<X> p(new X[n]); // エラー // ... }これは、デストラクタがポインタを"delete[]"ではなく、"delete"で解放し、残り の n-1 の X に対するデストラクタを起動できないため、エラーになります。
それでは、配列を保持するためにauto_arrayを使った方がよいのでしょうか。 auto_arrayというものはありません。なぜなら、必要がないからです。vectorを 使うのがよりよい解決方法です。
void f(int n) { vector<X> v(n); // ... }...の部分で例外が発生しても、vのデストラクタが正しく起動されます。
C++標準ライブラリは、有用で、静的に型安全であり、効率的なコンテナのセット を提供します。vector, list, mapがその代表的な例です。
vector<int> vi(10); vector<Shape*> vs; list<string> lst; list<double> l2 map<string,Record*> tbl; map< Key,vector<Record*> > t2;優れたC++の教科書はみなこれらのコンテナについて説明しています。特に理由が なければ、配列や「手製」のコンテナよりもこちらを使用した方がよいでしょう。
これらのコンテナは同種(homogeneous)コンテナです。つまり、同じ型の要素を 保持します。もしコンテナにいくつかの異なる型の要素を保持したいなら、unionを 使用するか、多態型(polymorphic type)へのポインタのコンテナ(通常こちらの方が ずっと良い)を使用して表現しなければなりません。 典型的な例は次のようなものです。
vector<Shape*> vi; // Shapeへのポインタのvectorここで、viはShapeから導出されたものならどのような型の要素でも保持できます。 つまり、viは、そのすべての要素がShape(正確にはShapeへのポインタ)であるという 点で同種コンテナであり、Circle, Triangleなどのような、種々さまざまなShapeの 要素を保持できるという意味で異種(heterogenous)コンテナです。
したがって、ある意味、(どの言語でも)すべてのコンテナは、そのすべての要素に ユーザが依存する共通のインタフェイスが存在しなければならないため、同種コンテナ と言えます。異種コンテナと考えられているコンテナを提供している言語は、単に標準 インタフェイスを持つ要素のコンテナを提供しているにすぎません。たとえば、Javaの コレクションは、Object(へのリファレンス)のコンテナを提供し、要素の実際の型を 知るために(共通の)Objectインタフェイスを使用します。
C++標準ライブラリは同種コンテナを提供します。その理由は、ほとんどのケース で最も簡単に使用でき、コンパイル時に最良のエラーメッセージを出力し、不必要な 実行時オーバーヘッドがかからないからです。
C++で異種コンテナが必要なら、すべての要素に共通のインタフェイスを定義して、 それらを保持するコンテナを作成してください。以下に例を示します。
class Io_obj { /* ... */ }; // object I/Oに加わるのに必要なインタフェイス vector<Io_obj*> vio; // ポインタを直接管理したい場合 vector< Handle<Io_obj> > v2; // スマートポインタにオブジェクトを扱わせたい場合必要がなければ、実装の詳細の最も下のレベルまでは落さないでください。
vector<void*> memory; // 必要になることはほとんどない実装の詳細のレベルが低すぎるかは、キャストがやたら多くなるということから わかります。
プログラムによっては、Boost::AnyのようなAnyクラスを代わりに使用できること もあります。
vector<Any> v;
そんなに遅くはありません。「何と比べてか」というのがおそらくもっと役立つ 答えなのかもしれません。標準ライブラリコンテナのパフォーマンスについて不満を 言う人達の問題は、次のような三つの真の問題(あるいは多くの神話やデマ)のひとつ であることがわかりました。
それでは、ここでこれらの問題について調べてみましょう。 vector<X>は誰かが作った特殊化されたMy_container<X>よりも遅いことが よくあります。それは、My_container<X>が X へのポインタのコンテナとして 実装されているからです。標準コンテナは値のコピーを保持し、コンテナに追加 されるときに値をコピーします。これは小さな値にとっては基本的に非常に適して いますが、巨大なオブジェクトにとってはまったく適していません。
vector<int> vi; vector<Image> vim; // ... int i = 7; Image im("portrait.jpg"); // ファイルからイメージを読み込み、初期化する // ... vi.push_back(i); // i(のコピー)を v に追加する vim.push_back(im); // im(のコピーをvimに追加するここで、もしportrait.jpgが数メガバイトあり、Imageが値セマンティクスを持つ(すな わちコピー代入とコピーコンストラクタがコピーを作る)場合、vim.push_back(im)は 本当に高くつくでしょう。しかし、(よく言われるように)それが非常に問題となるなら、 そうしないようにしてください。代わりにハンドルのコンテナを使用するか、ポインタ のコンテナを使用してください。たとえば、もしImageが参照セマンティクスを持って いたなら、上記のコードはコピーコンストラクタ呼び出しのコストだけがかかります。 それは、多くのイメージ操作演算と比べたらささいなものです。 あるクラス(たとえば前述のImage)が正当な理由があってコピーセマンティクスを持つ 場合、ポインタを保持するコンテナが適切な解決策となることが多くあります。
vector<int> vi; vector<Image*> vim; // ... Image im("portrait.jpg"); // ファイルからイメージを読み込み、初期化する // ... vi.push_back(7); // 7(のコピー)を v に追加する vim.push_back(&im); // &im(のコピー)をvimに追加するもちろん、ポインタを使用するなら、リソース管理を考慮しなければなりませんが、 ポインタを保持するコンテナはそれ自身効率的で安価なリソースハンドルとなり ます(「所有する」オブジェクトを解放するデストラクタを持つコンテナが必要に なることがよくあります)。
次によくある本当のパフォーマンスの問題は、多数の(string,X)の組に 対するmap<string,X>の使用の問題です。mapは、less-thanのコストが低く、よいハッシュ関数 を作れない、比較的小さなコンテナ(たとえば数百、数千の要素 -- 10000要素を保持 するmapの要素へのアクセスには、およそ9回の比較のコストがかかる)には優れた ものです。多くの文字列を扱い、よいハッシュ関数があるなら、ハッシュテーブル を使用してください。標準化委員会の技術レポート(Technical Report)に よるunordered_mapが現在広く利用可能で、大抵の自家製のものよりはるかに優れて います。
(const char*,X)の組を使用すると、(string,X)の組みよりも高速化できること がありますが、< 演算子はCスタイルの文字列に対して辞書順による比較を行わない ということを忘れないでください。さらに、X が大きければ、コピーの問題も起こる かもしれません(それはいつもの方法のひとつで解決してください)。
侵入的(intrusive)リストはとても高速です。しかし、そもそもリストが必要で あるのかどうかよく考えてみてください。vectorはよりコンパクトであるので、 ほとんどの場合(追加や削除をするときでも)、より小さく、高速になります。 たとえば、少数の整数の要素を保持するリストを扱う場合、vectorは(どんな)listより もかなり高速です。さらに、侵入的(intrusive)リストは、組み込み型を直接保持 することができません(intにはリンクメンバがありません)。したがって、本当に リストが必要か、すべての要素型にリンクフィールドを追加できるかを想定して みてください。標準ライブラリのリストは初めから、要素を挿入する操作ごとに 割り当てとコピー(そして要素を削除する操作ごとに解放)を行います。デフォルト アロケータを持つstd::listにとって、これは重要です。コピーのオーバーヘッドが 大きくない小さな要素に対しては、最適化されたアロケータを使用することを検討 してください。自作の侵入的(intrusive)リストは、リストを使う必要があって、 最後にあと少しだけパフォーマンスが必要とされるところでだけ使用してください。
vectorが徐々に伸長していくときのコストを気にする人達もいます。私も以前は 気にしていましたし、伸長を最適化するためにreserve()を使用していました。しかし、 私のコードを測定して、何度も実際のプログラムの中でreserve()のパフォーマンス 上の利点を見つけるのに苦労した後は、イテレータの無効化(iterator invalidation)を 避ける必要のあるところ(私のコードの中では稀なケース)以外では使うことを止め ました。最後にもう一度。最適化する前に測定してください。
メモリリークのないコードを書くことです。コード中のいたるところでnew演算、 delete演算、ポインタ演算をしている場合は、明らかにどこかでへまをして、メモリ リークを起こしたり、迷子のポインタができてしまったりします。これはメモリ割り 当てをどれだけ用心深くするかにかかわりません。コードが複雑になると、あなたが 費やす時間と努力はやがて無駄になるでしょう。結局は、管理しやすい型の中にメモリ の取得と解放を隱すというテクニックがうまくいきます。標準コンテナがそのよい例 です。標準コンテナは過度に努力することなしに、要素のために割り当てられたメモリ をあなたが管理するよりもうまく管理します。次のコードをstringとvectorを使わず に書いてみてください。
#include<vector> #include<string> #include<iostream> #include<algorithm> using namespace std; int main() // stringをいじくり回すつまらないプログラム { cout << "enter some whitespace-separated words:\n"; vector<string> v; string s; while (cin>>s) v.push_back(s); sort(v.begin(),v.end()); string cat; typedef vector<string>::const_iterator Iter; for (Iter p = v.begin(); p!=v.end(); ++p) cat += *p+"+"; cout << cat << '\n'; }そのプログラムは最初から正しく動作するでしょうか。メモリリークしないとどう やって判断するのでしょうか。
上記のコードには、明示的なメモリ管理、マクロ、キャスト、オーバーフロー 検査、明示的なサイズ制限、そしてポインタがないことに注目してください。 関数オブジェクトと標準アルゴリズムを使えば、ポインタのように使われている イテレータも取り除くことができるでしょうが、このような小さなプログラムで そうするのはやり過ぎだと思いました。
これらのテクニックは完璧ではありませんし、体系的にいつでも簡単に使える というわけでもありません。しかし、意外と広範囲にわたって適用され、明示的な メモリの取得と解放が少なくなることで、プログラムの残りの部分がより把握しや すくなります。 早くも1981年に私は、把握しておかなければならない非常に多くのオブジェクトを 数十個に少なくすることで、プログラムを正しく動作させるのに必要な知的な作業が、 非常に難しいものから扱いやすいもの、ときには簡単になることさえあったと指摘 しました。
もしあなたのアプリケーション領域に、明示的なメモリ管理を最小限に抑えて プログラミングしやすくするライブラリがないなら、そのようなライブラリを初め に作成することが、あなたのプログラムを正確に完成させる最も早い方法かもしれ ません。
テンプレートと標準ライブラリは数年前と比べても、コンテナ、リソースハンドル の利用がずっと楽になっています。例外の使用はほとんど不可欠なものになってい ます。
それでもやはり、あなたのアプリケーションの中のオブジェクトの一部で、割り 当てと解放を暗黙に扱えない場合は、メモリリークする可能性を最小限に抑える ためにリソースハンドルを利用できます。以下に示すのは、自由記憶領域上に割り 当てられたオブジェクトを関数から返す必要のある例です。これはオブジェクトを 解放し忘れるきっかけとなります。結局、ポインタを見ただけでは、解放する必要が あるかどうか、解放する必要がある場合、解放する責任は誰にあるのかを知ることは できません。リソースハンドル(この例では標準ライブラリのauto_ptr)を使用すると、 責任の所在がはっきりとします。
#include<memory> #include<iostream> using namespace std; struct S { S() { cout << "make an S\n"; } ~S() { cout << "destroy an S\n"; } S(const S&) { cout << "copy initialize an S\n"; } S& operator=(const S&) { cout << "copy assign an S\n"; } }; S* f() { return new S; // この S を解放する責任は誰にあるのか }; auto_ptr<S> g() { return auto_ptr<S>(new S); // この S を解放する責任を明示的に移す } int main() { cout << "start main\n"; S* p = f(); cout << "after f() before g()\n"; // S* q = g(); // このエラーはコンパイラが見つける auto_ptr<S> q = g(); cout << "exit main\n"; // ここで *p はリークする // ここで *q は暗黙に解放される }
ただ単にメモリだけでなく、リソース一般について考えてください。
あなたの環境でこれらのテクニックを系統的に適用できない場合(どこか他の ところで書かれたコードを使用しなければならないときや、あなたのプログラム の一部を超保守的な人が書いた場合など)は、あなたの標準開発手順の中でメモリ リークの検出器を使うか、ガベージコレクタを組み込むようにしてください。
もしそうしたいなら、もちろんrealloc()を使うことはできます。 しかし、realloc()は、malloc()(とそれと同類の関数)が割り当てた、ユーザ定義の コピーコンストラクタを持たないオブジェクトの配列に対して動作することが保証 されているだけです。さらに、realloc()は単純な予想に反して、場合によっては 引数の配列をコピーすることがあるということを忘れないでください。
C++でメモリの再割り当てを扱うには、vectorのような標準コンテナを使用する のがよりよい方法です。標準コンテナは自然に伸長していき ます。
"malloc()"は関数で、引数に(バイト)数を取り、未初期化の記憶領域を指すvoid*を 返します。"new"は演算子で、引数に型と(オプションで)その型に対する初期値の集合 を取り、その型の(オプションで)初期化済みのオブジェクトへのポインタを返します。 両者の違いは、ユーザ定義型のオブジェクトを単純ではない初期化セマンティクスで 割り当てるときにもっともはっきりします。以下に例を示します。
class Circle : public Shape { public: Cicle(Point c, int r); // デフォルトコンストラクタなし // ... }; class X { public: X(); // デフォルトコンストラクタ // ... }; void f(int n) { void* p1 = malloc(40); // (未初期化の)40バイトを割り当てる int* p2 = new int[10]; // 未初期化のintを10個割り当てる int* p3 = new int(10); // 初期値が10のintを1個割り当てる int* p4 = new int(); // 初期値が0のintを1個割り当てる int* p4 = new int; // 未初期化のintを1個割り当てる Circle* pc1 = new Circle(Point(0,0),10); // 指定された引数の値で構築された // Circleを割り当てる Circle* pc2 = new Circle; // デフォルトコンストラクタがないのでエラー X* px1 = new X; // デフォルトコンストラクタで構築されたXを割り当てる X* px2 = new X(); // デフォルトコンストラクタで構築されたXを割り当てる X* px2 = new X[10]; // デフォルトコンストラクタで構築されたXを10個割り当てる // ... }
"(値)"という表記で初期値を指定したとき、その値で初期化されることに注目して ください。残念ながら、配列にはそのような指定はできません。多くの場合、vectorは、 自由記憶領域上に割り当てられる配列に代わる優れた選択肢となります。(たとえば、 例外安全性を考えてみてください)。
malloc()を使用するときはいつでも、初期化することと、返されたポインタを適切 な型へ変換することに注意を払わなければなりません。さらに、必要なバイト数を正し く取得したかにも気をつける必要があるでしょう。初期化を考慮に入れた場合、 malloc()とnewとの間にパフォーマンスの差はありません。
malloc()はメモリの不足を 0 を返すことで通知します。newはメモリの割り当て と初期化の失敗を例外をスローして通知します。
newで生成されたオブジェクトはdeleteで解体されます。malloc()で割り当てられ たメモリ領域はfree()で解放されます。
malloc()とnewを同じプログラムの中で使用できるという意味では、答えはイエス です。
malloc()でオブジェクトを割り当てて、deleteで解放することはできないという 意味では、答えはノーです。newで割り当てて、free()で解放することも、newで割り 当てられた配列に対してrealloc()を使用することもできません。
C++の演算子newとdeleteは、オブジェクトの適切な生成と解体を保証します。 したがって、コンストラクタとデストラクタを起動する必要があるところでは、 それを起動します。Cスタイルの関数、malloc()、calloc()、free()、realloc()は、 それを保証しません。さらに、newとdeleteで使用されるメモリの取得と解放のメカ ニズムが、malloc()とfree()のものと互換性があるという保証はありません。 仮にこれらを混在させたスタイルが、あなたのシステムで動作したとしても、今の ところそれは単に「ラッキー」であったということです。
もしrealloc()の必要性を感じるなら(そう感じる人は多い)、標準ライブラリ のvectorを使うことを検討してみてください。以下に例を示します。
// 入力から単語を読み込んで、stringのvectorに格納する vector<string> words; string s; while (cin>>s && s!=".") words.push_back(s);
vectorは必要に応じて拡張されます。
"Learning Standard C++ as a New Language"にある例と議論も参照してください。 私の出版物一覧から ダウンロードできます。
Cでは、void* を T* へ暗黙に変換することができます。これは安全ではありません。 次の例を考えてみてください。
#include<stdio.h> int main() { char i = 0; char j = 0; char* p = &i; void* q = p; int* pp = q; /* 安全ではない、Cでは合法、C++では非合法 */ printf("%d %d\n",i,j); *pp = -1; /* &iから始まるメモリを上書きする */ printf("%d %d\n",i,j); }
T を指していないものに対して T* としてしまうと、致命的な結果をもたらすお それがあります。したがって、C++では、void* から T* を得るには明示的なキャス トが必要になります。たとえば、望ましくはないけれども同様の結果を上記のプログ ラムで得るには、次のように書く必要があります。
int* pp = (int*)q;
あるいは、検査のない型変換をすることをもっとはっきりと示すために、次のよ うに新しいスタイルのキャストを使います。
int* pp = static_cast<int*>(q);キャストは避けた方がよいものです。
Cでこのような安全でない変換がもっともよく使われるのは、次のようなmalloc()の 結果を適合するポインタに代入するときです。
int* p = malloc(sizeof(int));C++では、型安全なnew演算子を使います。
int* p = new int;ついでに言うと、new演算子には、malloc()に比べて次のような利点があります。
typedef std::complex<double> cmplx; /* Cスタイル: */ cmplx* p = (cmplx*)malloc(sizeof(int)); /* エラー: 間違ったサイズ */ /* p==0のテストを忘れている */ if (*p == 7) { /* ... */ } /* おっと! *p の初期化を忘れた */ // C++スタイル: cmplx* q = new cmplx(1,2); // メモリが不足している場合は、bad_allocがスローされる if (*q == 7) { /* ... */ }
ありませんが、必要なら自分で書くことができます。
ある領域にオブジェクトを配置するplacement newを考えてみてください。
class Arena { public: void* allocate(size_t); void deallocate(void*); // ... }; void* operator new(size_t sz, Arena& a) { return a.allocate(sz); } Arena a1(some arguments); Arena a2(some arguments);これにより、次のように書くことができます。
X* p1 = new(a1) X; Y* p2 = new(a1) Y; Z* p3 = new(a2) Z; // ...しかし、これらのオブジェクトを後で正しく解放するにはどうすればよいでしょうか。 placement newに対応する組み込みの"placement delete"がない理由は、それが正しく 使用されることを保証する一般的な方法がないからです。C++の型システムは、p1 が Arena a1上に割り当てられたオブジェクトを指しているものだと推定できません。 どこか他のところで割り当てられた任意の X を指すポインタを p1 に代入することが できてしまいます。
しかし、プログラマがどこで割り当てられたかを知っていれば、方法があります。
template<class T> void destroy(T* p, Arena& a) { if (p) { p->~T(); // 明示的なデストラクタの呼び出し a.deallocate(p); } }これで、次のように書くことができます。
destroy(p1,a1); destroy(p2,a2); destroy(p3,a3);Arenaがどのようなオブジェクトを保持しているかを把握していれば、間違いを防ぐ ようにdestroy()を書くこともできます。
プログラミング 言語C++(Special Edition)の15.6で述べられているように、クラス階層に対して 演算子new()とdelete()の組を定義することもできます。 C++の設計と進化の10.4と プログラミング言語C++(Special Edition)の19.4.5も参照してください。
次の例を考えてみてください。
delete p; // ... delete p;もし ... の部分で p に触れなければ、2回目の"delete p;"は、C++の実装が(特別 な予防策なしに)効果的に防ぐことのできない深刻なエラーを引き起こします。ゼロ ポインタをdeleteすることは定義上無害であるので、"delete p;"が必要なことをし 終えた後で"p=0;"とするのが簡単な解決方法でしょう。しかし、C++はそうすること を保証していません。
その理由のひとつは、deleteのオペランドは左辺値(lvalue)である必要がないと いうことです。次の例を考えてみてください。
delete p+1; delete f(x);この例では、deleteの実装がゼロを代入できるポインタがありません。このような 例はほとんどないかもしれませんが、「deleteされたオブジェクトへのポインタは 0 である」と保証できないことを暗示しています。また、ひとつのオブジェクトを指 すポインタがふたつあると、この「規則」を簡単に擦り抜けることができてしまい ます。
T* p = new T; T* q = p; delete p; delete q; // しまった!C++は、deleteの実装が左辺値のオペランドをクリアすることをはっきりと認めてい て、私はそう実装されるだろうと期待していましたが、このアイデアは、実装者に は評判がよくなかったようです。
もしポインタをクリアすることが重要であると思うなら、次のようなdestroy関 数の使用を検討してみてください。
template<class T> inline void destroy(T*& p) { delete p; p = 0; }
これが、標準ライブラリコンテナやハンドルなどを使って、newとdeleteの明示的 な使用を最小限に抑えるもうひとつの理由です。
さらに、ポインタを(クリアできるように)参照渡しにすることで、destroy()が 右辺値(rvalue)に対して呼び出されないようにするという利点があることに注目し てください。
int* f(); int* p; // ... destroy(f()); // エラー: 右辺値を非const参照で渡そうとしている destroy(p+1); // エラー: 右辺値を非const参照で渡そうとしている
時間と空間の点から見ると、配列はメモリ上のオブジェクトの並びにアクセスす るにはほぼ最適に近いデータ構造です。しかし、配列は誤用とエラーの可能性が大変 高い、非常に低レベルなデータ構造でもあり、ほとんどすべての場合において、配列 に代わる優れた方法があります。「優れた」というのは、読み書きしやすく、エラー が起こりにくく、配列と同じくらい速い、という意味です。
配列には次のふたつの基本的な問題があります。
void f(int a[], int s) { // a に対して何か処理する。a のサイズは s である for (int i = 0; i<s; ++i) a[i] = i; } int arr1[20]; int arr2[10]; void g() { f(arr1,20); f(arr2,20); }2番目の関数呼び出しは、arr2に属さないメモリ上を書き散らかしてしまうでしょう。 もちろん、プログラマは普通はサイズを正しく扱いますが、それは余計な仕事で、 何度でも間違いを犯します。私は標準ライブラリvectorを使用した、もっと簡単で はっきりした方法を好んでいます。
void f(vector<int>& v) { // v に対して何かする for (int i = 0; i<v.size(); ++i) v[i] = i; } vector<int> v1(20); vector<int> v2(10); void g() { f(v1); f(v2); }配列は自身のサイズを知らないので、配列の代入はありません。
void f(int a[], int b[], int size) { a = b; // 配列の代入ではない memcpy(a,b,size); // a = b // ... }もう一度言いますが、私はvectorを好んでいます。
void g(vector<int>& a, vector<int>& b, int size) { a = b; // ... }vectorのもうひとつの利点は、memcpy()はstringのようなコピーコンストラクタを 持つ要素に対しては、正しく動作しないということです。
void f(string a[], string b[], int size) { a = b; // 配列の代入ではない memcpy(a,b,size); // 大惨事 // ... } void g(vector<string>& a, vector<string>& b, int size) { a = b; // ... }配列は固定長で、コンパイル時にサイズが決定されます。
const int S = 10; void f(int s) { int a1[s]; // エラー int a2[S]; // OK // a2 を拡張したい場合、malloc()を使用して自由記憶領域上に // 割り当てた配列に変更し、realloc()を使用する // ... }次の例と比べてみてください。
const int S = 10; void g(int s) { vector<int> v1(s); // OK vector<int> v2(S); // OK v2.resize(v2.size()*2); // ... }C99はローカルな配列が可変長配列になることを認めていますが、VLA(訳注: Variable Length Array)には独自の問題があります。
配列名がポインタに「格下げ(dacay)」されることは、CとC++にとって欠かせない ものです。しかし、配列の格下げは継承と非常に相性が悪いです。次の例を考えて みてください。
class Base { void fct(); /* ... */ }; class Derived { /* ... */ }; void f(Base* p, int sz) { for (int i=0; i<sz; ++i) p[i].fct(); } Base ab[20]; Derived ad[20]; void g() { f(ab,20); f(ad,20); // 大惨事! }最後の関数呼び出しで、Derived[]はBase[]として扱われ、sizeof(Derived)!=sizeof(Base)の 場合(大抵そうなるでしょう)、添字指定はもう正しく働きません。 代りにvectorを使用したなら、コンパイル時にエラーになるでしょう。
void f(vector<Base>& v) { for (int i=0; i<v.size(); ++i) v[i].fct(); } vector<Base> ab(20); vector<Derived> ad(20); void g() { f(ab); f(ad); // エラー: vector<Derived>をvector<Base>に変換できない }CとC++において、初心者が犯す驚くほど多くのプログラミングエラーは、配列の 使用(誤用)に関連したものだと、私は思います。
プログラミング言語C++の 8.3節、14章、付録Eを 参照してください。付録では、要求の厳しいアプリケーションの中で例外安全(exception-safe)な コードを書くテクニックを重点的に扱っています。初心者向けではありません。
C++では、例外はローカルで対処できないエラーが発生したことを知らせるために 使用されます。たとえば、コンストラクタの中でリソース取得に失敗したというよう な状況で使用されます。以下に例を示します。
class Vector { int sz; int* elem; class Range_error { }; public: Vector(int s) : sz(s) { if (sz<0) throw Range_error(); /* ... */ } // ... };例外を単に関数から値を返す代わりの方法として使わないでください。ほとんどの ユーザは(言語の定義がそうするよう勧めているので)、例外処理コードはエラー処理 コードであるとみなしています。また、実装はそのような想定を反映して最適化され ます。
リソース取得は初期化である(Resource Acquisiton Is Initialization: RAIIと省略されることもある)と呼ばれる重要なテクニックがあり ます。これは、デストラクタを持つクラスにリソース管理をさせるというものです。 以下に例を示します。
void fct(string s) { File_handle f(s,"r"); // File_handleのコンストラクタがファイル "s" をオープンする // f を使用する } // ここでFile_handleのデストラクタがファイルをクローズするもし、fct()の「f を使用する」の部分で例外がスローされても、デストラクタが起動 され、ファイルはきちんとクローズされます。これは、次のようなよくある危険な 使用法とは対照的です。
void old_fct(const char* s) { FILE* f = fopen(s,"r"); // ファイル "s" をオープンする // f を使用する fclose(f); // ファイルをクローズする }old_fct()の「f を使用する」の部分で例外がスローされた場合(あるいは単に関数か ら復帰しても)、ファイルはクローズされません。さらに、C プログラムではlongjmp()の 使用も危険です。
この質問は言い換えると次のようになります。例外がスローされた場所に戻り、 そこから実行を続けるプリミティブがC++にないのはなぜですか。
基本的に、例外ハンドラから戻って実行を再開するときに、例外がスローされた 場所の後のコードが、何事もなかったかのように実行を継続するように対処されて いるという確証はありません。例外ハンドラは再開する前に、コンテキストがどれ だけ「正しい」かを知ることはできません。 そのようなコードを正しく動作させるには、例外をスローする側とキャッチする側が、 お互いのコードとコンテキストの詳細を知っている必要があります。これは、再開が 認められているところならどこでも、深刻なメインテナンス上の問題となる複雑な 相互依存性を生じさせます。
私はC++の例外処理メカニズムを設計したときに、処理の再開を認めることの 実現性についてかなり検討しました。そしてこの問題は、標準化作業の中でかなり 詳細に議論されました。C++の 設計と進化の例外処理の章を参照してください。
例外をスローする前に問題を解決できるかどうか確かめたい場合は、先にそれを 確認する関数を呼んで、その問題をそこで扱うことができないときだけ例外をスロー するようにしてください。new_handlerがその一例です。
C++がそれに代わるものをサポートしているからで、ほとんどの場合、"finally"より も優れています。それは「リソース取得は初期化である」と呼ばれるテクニック(プロ グラミング言語C++第3版 14.4節)です。このテクニックは、リソースをローカルオブ ジェクトを使って表現することで、そのデストラクタがリソースを解放してくれる というアイデアに基いています。こうすることで、プログラマはリソースの解放を 忘れずに済みます。次の例を見てください。
class File_handle { FILE* p; public: File_handle(const char* n, const char* a) { p = fopen(n,a); if (p==0) throw Open_error(errno); } File_handle(FILE* pp) { p = pp; if (p==0) throw Open_error(errno); } ~File_handle() { fclose(p); } operator FILE*() { return p; } // ... }; void f(const char* fn) { File_handle f(fn,"rw"); // fn を読み込みと書き込みモードでオープンする // f を通してファイルを操作する }あるシステムでは、リソースごとに「リソースハンドル」クラスが必要になります。 しかし、個々のリソース取得に対して"finally"節を記述する必要はありません。 現実のシステムでは、リソースの種類よりもリソースを取得する場面の方がはるかに 多いので、「リソース取得は初期化である」テクニックを使用すると、"finally"構文 を使用するよりもコード量が少なくなります。
プログラミング言語C++ 付録Eのリソース 管理の例も参照してください。
プログラミング言語C++の付録Eに 用例と詳しい説明があるので、そちらを参照してください。
注意: 時間制約に厳しいシステムでは、例外を使用できないことがあります。 the JSF air vehicle C++ coding standardsに実例があるので、そちらを参照してください。
次の定義は、
void main() { /* ... */ }
現在でも、これまでにおいてもC++ではありません。C でさえもありません。ISO C++標準の3.6.1[2]、あるいはISO C標準の5.1.2.2.1を参照してください。 仕様に準拠した実装は、
int main() { /* ... */ }
と
int main(int argc, char* argv[]) { /* ... */ }
を受け入れます。
仕様に準拠した実装は、もっと多くのmain()のバージョンを規定するかもしれま せが、戻り値の型はintでなければなりません。main()が返すintは、main()を起動 した「システム」に、プログラムが値を返すためのものです。そのような機能を提 供していないシステムでは戻り値は無視されますが、"void main()"を合法なC++、 合法な C にするものではありません。あなたのコンパイラが"void main()"を受け 入れたとしても、使用しないようにするか、C プログラマとC++プログラマから無知 と思われることを覚悟の上で使用してください。
C++では、main()はreturn文を明示的に含む必要はありません。そのような場合、 値 0 を返し、実行が成功したことを表します。以下に例を示します。
#include<iostream> int main() { std::cout << "This program returns the integer value 0\n"; }
ISO C++とC99のどちらも、宣言に型を指定しないことは認めていません。つまり、 C89とARM C++とは対照的に、宣言に型が指定されていない場合は"int"とみなされま せん。したがって、次のコードは、
#include<iostream> main() { /* ... */ }
main()に戻り値の型の指定がないのでエラーになります。
ほとんどの演算子はプログラマがオーバーロードできます。例外は以下の演算子 です。
. (ドット) :: ?: sizeof?: のオーバーロードを認めない根本的な理由はありません。3項演算子をオーバー ロードする特別なケースを導入する必要性は思い当たりませんでした。 expr1?expr2:expr3をオーバーロードする関数が、expr2とexpr3の どちらかひとつだけ実行することを保証できなかったということに注目してくだ さい。
sizeofは組み込みの演算子であるため、オーバーロードできません。たとえば、 配列へのポインタのインクリメントは、暗黙のうちにこれに依存しています。 次の例を考えてみてください。
X a[10]; X* p = &a[3]; X* q = &a[3]; p++; // p はa[4]を指す // その結果、p の整数値は // q の整数値よりもsizeof(X)だけ大きいはずしたがって、基本的な言語の規則に反しないで、プログラマがsizeof(X)に新しい 異なる意味を与えることはできませんでした。
N::mは、N と m のどちらも値を持つ式ではありません。N と m はコンパイラが 識別する名前であり、:: は式の評価ではなく、(コンパイル時の)スコープ解決を 行います。x::yの x が名前空間やクラスではなく、オブジェクトであるような :: 演算子のオーバーロードを認めることが考えられるでしょうが、- 見た目に反して - (expr::exprを認めるために)新しい構文を導入することになるでしょう。 そのような複雑さがどのような利益をもたらすかはっきりしません。
演算子 . (ドット)は、原理的には -> 演算子と同じテクニックを使ってオーバー ロードすることができるでしょう。しかし、そうすると、そのオペレーションが . を オーバーロードしているオブジェクトに対するものなのか、. によって参照される オブジェクトに対するものなのか、という問題が起こる可能性があります。 以下に例を示します。
class Y { public: void f(); // ... }; class X { // . をオーバーロードできると仮定する Y* p; Y& operator.() { return *p; } void f(); // ... }; void g(X& x) { x.f(); // X::f なのか Y::f なのか、それともエラー? }この問題はいくつかの方法で解決することができます。標準化の時点では、どの 方法が最善であるかはっきりしませんでした。詳細は、C++の設計と進化を参照して ください。
残念ですが、できません。それが実現できるか何度か検討してきましたが、 その都度、私たちは得られそうな利益よりも問題の方が多そうだと判断しました。
これは言語上の技術的な問題ではありません。私が1983年にはじめて検討した ときでさえ、どのように実装すればよいかはわかっていました。しかし、私の経験 では、最も簡単な例を越えると人々は演算子の使用上の「明白な」意味について 微妙に違った意見を持つようだとわかりました。昔からよくある例は a**b**c と いうものです。** が巾乗を意味すると仮定します。ここで、a**b**c は (a**b)**c と a**(b**c) のどちらの意味を持つべきでしょうか。私は答えははっきりしていて、 私の友人たちも意見が一致していると思いました 〜 その後、どちらの解決が明白か 意見が一致していないことがわかりました。このような問題は捕えにくいバグに つながるのではないかと思います。
(C++コードの中で)Cの関数を``extern "C"''と宣言し、(CまたはC++のコード から)それを呼び出します。以下に例を示します。
// C++コード extern "C" void f(int); // ひとつの方法 extern "C" { // もうひとつの方法 int g(double); double h(); }; void code(int i, double d) { f(i); int ii = g(d); double dd = h(); // ... }関数の定義は、次のようになるでしょう。
/* Cコード: */ void f(int i) { /* ... */ } int g(double d) { /* ... */ } double h() { /* ... */ }Cの型規則ではなく、C++の型規則が使用されることに注意してください。``extern "C"''と宣言された関数を不正な数の引数を伴って呼び出すことはできません。以下 に例を示します。
// C++コード void more_code(int i, double d) { double dd = h(i,d); // エラー: 予期しない引数 // ... }
(C++コードの中で)C++の関数を``extern "C"''と宣言し、(CまたはC++のコード から)それを呼び出します。以下に例を示します。
// C++コード: extern "C" void f(int); void f(int i) { // ... }これで、f()は次のように使用できます。
/* Cコード: */ void f(int); void cc(int i) { f(i); /* ... */ }もちろん、この方法は非メンバ関数だけに使用できます。メンバ関数(仮想関数を含 む)を C から呼び出したい場合は、簡単なラッパーを用意する必要があります。 以下に例を示します。
// C++コード: class C { // ... virtual double f(int); }; extern "C" double call_C_f(C* p, int i) // ラッパー関数 { return p->f(i); }これで、C::f()は次のように使用できます。
/* Cコード: */ double call_C_f(struct C* p, int i); void ccc(struct C* p, int i) { double d = call_C_f(p,i); /* ... */ }オーバーロードされた関数を C から呼び出したい場合は、C コードが呼び出せる ように、異なる名前を持つラッパーを用意しなければなりません。 以下に例を示します。
// C++コード: void f(int); void f(double); extern "C" void f_i(int i) { f(i); } extern "C" void f_d(double d) { f(d); }これで、f()を次のように使用できます。
/* Cコード: */ void f_i(int); void f_d(double); void cccc(int i,double d) { f_i(i); f_d(d); /* ... */ }これらのテクニックは、C++のヘッダファイルを変更できない(あるいは変更したく ない)場合でも、C コードからC++ライブラリを呼び出すのに使用できます。
C++は C からポインタを受け継ぎました。したがって、重大な互換性の問題を 起こさずにポインタを取り除くことはできませんでした。リファレンスはいくつか の事柄に役に立ちますが、リファレンスをC++に導入した直接の理由は、演算子の オーバーロードをサポートするためでした。 以下に例を示します。
void f1(const complex* x, const complex* y) // リファレンスなし { complex z = *x+*y; // 不格好 // ... } void f2(const complex& x, const complex& y) // リファレンスあり { complex z = x+y; // より良い // ... }一般的に、ポインタの機能とリファレンスの機能の両方を持ちたい場合は、(C++の ように)ふたつの異なる型か、ひとつの型に対するふたつの異なる演算セットが必要 です。たとえば、ひとつの型の場合、参照されるオブジェクトへ代入するオペレー ションとリファレンス/ポインタへ代入するオペレーションの両方が必要になります。 これは、(Simulaのように)別個のオペレータを使って行うことができます。 以下に例を示します。
Ref<My_type> r :- new My_type; r := 7; // オブジェクトへ代入 r :- new My_type; // リファレンスへ代入あるいは、次のように、型検査(オーバーローディング)に頼ることもできるでしょう。
Ref<My_type> r = new My_type; r = 7; // オブジェクトへ代入 r = new My_type; // リファレンスへ代入
C++では、NULLの定義は 0 であるので、単に美的な違いでしかありません。私は マクロの使用を避けたいので、0 を使用します。NULLを使用すると、NULLが 0 とは 異なる、あるいは整数ではないと誤解してしまうという問題もあります。標準化以前 のコードでは、NULLは不適切に定義されている(いた)ので、使用を避けなければなり ません(でした)。最近はそのようなことはあまりありません。
ヌルポインタに名前を付けなければならない場合は、nullptrとしてください。 C++0xではヌルポインタをnullptrとする予定です。そして、"nullptr"はキーワード になるでしょう。
未定義です。C とC++では基本的に、変数への書き込みもしているひとつの式の 中で変数を 2 回読んだ結果は未定義です。そのようなことはしないでください。 他に次のような例もあります。
v[i] = i++;これに関連した例として、次のようなものもあります。
f(v[i],i++);関数の引数が評価される順番は未定義であるので、結果は未定義です。
パフォーマンスのよいコードを生成するために、評価される順番を未定義にして おくことが求められています。コンパイラは上記のようなコードに対して警告するこ とができるでしょう。このようなコードは、典型的(あるいは潜在的)な捕えがたい バグとなります。数十年たっても、ほとんどのコンパイラが未だにそのようなコード に対して警告しないで、そういった仕事を特殊で、独立した、十分使われていない ツールに残していることに、私はがっかりしています。
マシンには差異があるということと、C の多くの事項が未定義のままである という理由でそうなっています。用語「未定義(undefined)」「未規定(unspecified)」 「処理系定義(implementation defined)」「適格(well-formed)」の定義も含め、詳細 はISO C++標準を参照してください。これらの用語の意味は、ISO C標準における定義や 一般的な用法とは異なることに注意してください。すべての人が定義を共有している とは限らないということを理解していないと、議論が非常に混乱することがあります。
満足のゆかないものであれ、これは正しい答えです。 C と同様に、C++はハードウェアを直接、効率的に利用することを目指しています。 これは、C++が特定のマシン上におけるビット、バイト、ワード、アドレス、整数演算、 浮動小数点数演算のようなハードウェアの要素を扱わなければならないということを 暗に意味しています。 「未定義」と言われている「事項」の多くが、実際には「処理系定義」であることに 注意してください。どのマシン上で実行するか分かっている限り、完全に規定された コードを書くことができます。整数のサイズや浮動小数点数演算の丸めがこれに分類 されます。
おそらく最もよく知られた評判の悪い未定義動作の例である、以下のコードを考えて みてください。
int a[10]; a[100] = 0; // 範囲エラー int* p = a; // ... p[100] = 0; // 範囲エラー (代入の前に p に適切な値を設定しない場合)C++(と C)における配列とポインタの概念は、マシン上のメモリとアドレスの概念を オーバーヘッドなしに直接表現したものです。ポインタに対するプリミティブ演算は、 機械語命令に直接マップされます。特に範囲検査は行われません。範囲検査を行うと、 実行時間とコードサイズの面でコストがかかるでしょう。C はオペレーティングシス テムタスクでアセンブリコードを打ち負かすために設計されたので、そうする必要が ありました。さらに、C には(C++と異なり)、コンパイラが違反行為を見つけるコード を生成して、それを報告する適切な方法がありません。つまり、C には例外処理が ないということです。 C++は互換性と、(OS、組み込みシステム、そしていくつかの数値計算分野で)アセン ブラと張り合うために C を継承しました。範囲検査をしたければ、適切な検査を行 うクラス(vector、スマートポインタ、stringなど)を使用してください。優れたコン パイラであれば、コンパイル時にa[100]に対する範囲エラーを見つけることができる でしょう。p[100]に対する範囲エラーを見つけるのは非常に困難で、一般的にすべて の範囲エラーをコンパイル時に見つけることはできません。
未定義動作の他の例はコンパイルモデルに起因します。コンパイラは分割コンパ イルされた翻訳単位の中では、オブジェクトと関数の一貫性のない定義を見つける ことができません。以下に例を示します。
// file1.c: struct S { int x,y; }; int f(struct S* p) { return p->x; } // file2.c: struct S { int y,x; } int main() { struct S s; s.x = 1; int x = f(&s); // x!=s.x !! return 2; }file1.cとfile2.cをコンパイルして、同じプログラムにリンクすることは、C とC++の 両方で非合法です。リンカは S の一貫性のない定義を見つけることができるでしょう が、そうするように強制されません(そうしないものがほとんどです)。 多くのケースで、分割コンパイルされた翻訳単位間で一貫性を見つけるのはかなり 難しいでしょう。ヘッダファイルを一貫性を持って使用することで、そのような問題 を最小限にすることができ、リンクが改善している兆しが見えます。C++のリンカは、 一貫性がなく宣言された関数に関するほとんどのエラーを見つけることに注意して ください。
最後に紹介するのは、明らかに不必要で、かなり厄介な、単一の式の未定義動作 です。以下に例を示します。
void out1() { cout << 1; } void out2() { cout << 2; } int main() { int i = 10; int j = ++i + i++; // j の値は未規定 f(out1(),out2()); // 12または21と表示する }j の値はコンパイラが最適なコードを生成できるように規定されていません。 この自由を与えられたコンパイラが生成するものと、「通常の左から右への評価」 を要求するコンパイラが生成するものとの違いが大きな意味を持つことがあると 言われています。私は納得していませんが、この自由を利用している「現存する」 無数のコンパイラと、この自由を熱烈に支持する人達がいる中で、それを変更する ことは難しいでしょうし、変更が C とC++の世界に浸透するまでには長い時間が かかるでしょう。 すべてのコンパイラが++i+i++のようなコードに警告を出さないことに、私はがっか りしています。同様に、引数の評価順も規定されていません。
私の考えでは、未定義、未規定、処理系定義のままになっている事項が多すぎる と思います。しかし、それを指摘したり、例をあげることは簡単ですが、明確に決め ることは難しいことです。問題の多くを避けたり、ポータブルなコードを生成する ことは、それほど難しいことではないことも心に留めておいてください。
キャストは一般的に避けた方がよいものです。dynamic_castを除いて、キャストの 使用は、型エラーや数値の切り詰めの可能性があるということを暗に意味しています。 キャストで使用されている型が開発中かメインテナンス中に変更された場合、害がない ように見えるキャストでさえも、深刻な問題を引き起こす可能性があります。 たとえば、次のコードはどのような意味を持つでしょうか。
x = (T)y;わかりません。型 T と、x と y の型が何かによります。T はクラス名やtypedefさ れた型かもしれませんし、もしかしたら、テンプレートパラメータかもしれません。 x と y はスカラー変数で、(T)は値の変換を表しているのかもしれません。 あるいは、x の型は y のクラスから導出されたクラスで、(T)はダウンキャストなの かもしれません。x と y は関連のないポインタ型なのかもしれません。C スタイルの キャスト(T)は、多くの論理的に異なる演算を表現するために使用できるので、コン パイラは誤用を見つけるための最小限の機会しか持ちません。 同じ理由で、プログラマはキャストが正確に何をするのか分らないかもしれません。 初心者プログラマはこれを利点と考えることがあり、初心者が間違って理解すると、 捕えがたいエラーのもとになります。
「新しいスタイルのキャスト」は、プログラマの意図をもっとはっきり示す機会を プログラマに与え、コンパイラがより多くのエラーを見つけるために導入されました。 以下に例を示します。
int a = 7; double* p1 = (double*) &a; // OK (しかし a はdoubleではない) double* p2 = static_cast<double*>(&a); // エラー double* p2 = reinterpret_cast<double*>(&a); // OK: I really mean it const int c = 7; int* q1 = &c; // エラー int* q2 = (int*)&c; // OK (しかし *q2=2; は依然として不当なコードで欠落する場合がある) int* q3 = static_cast<int*>(&c); // エラー: static_castはconstを取り除かない int* q4 = const_cast<int*>(&c); // I really mean itこのアイデアは、static_castが許す変換は、reinterpret_castを要求する変換よりも、 多少エラーになりにくいという考えにもとづいています。原理的には、static_castの 結果は、キャストで元の型に戻さずに使用できますが、reinterpret_castの結果は、 互換性を保証するために、使用する前に元の型に戻すべきです。
新しいスタイルのキャストを導入したふたつ目の理由は、C スタイルのキャストは プログラム中で見つけづらいからというものでした。たとえば、普通のエディタや ワードプロセッサを使って、キャストをうまく探すことはできません。この C スタイル キャストの見えにくさは損害をもたらす可能性があるので、とりわけ残念なことです。 厄介な演算は、醜い構文形式を持つべきです。新しいスタイルのキャストの構文を決定 した理由の一端には、このような意見がありました。さらに、テンプレートの表記に 合わせるためという理由もあります。これにより、プログラマは独自のキャスト、特に 実行時検査を伴うキャストを書くことができます。
おそらく、static_castは不格好で比較的タイプしづらいので、使用をためらうの ではないでしょうか。それは良いことだと思います。なぜなら、最近のC++ではほと んどの場合において、キャストを回避できるからです。
マクロはC++のスコープ規則と型規則に従いません。このことは度々、難解な 問題とそれほど難解でもない問題を引き起こします。そのため、C++ではマクロの 代わりに使用できるものとして、インライン関数、テンプレート、名前空間といっ た、C++の他の部分によく馴染むものを用意しています。
次のようなコードを考えてみてください。
#include "someheader.h" struct S { int alpha; int beta; };
もし、誰かが(愚かにも)"alpha"や"beta"といった名前のマクロを定義していた 場合、このコードはコンパイルできないか、(もっとひどいことに)コンパイルでき たとしても、それは意図しないものである可能性があります。 たとえば、"someheader.h"には次のようなマクロが定義されているかもしれません。
#define alpha 'a' #define beta b[2]
マクロ名(だけ)をすべて大文字にするという慣習に従えば、この問題を避ける ことはできますが、言語レベルでマクロの展開を防ぐ方法はありません。 たとえば、メンバ名は構造体のスコープの中にあるということで、この問題を避ける ことはできません。マクロは、コンパイラが処理する前に、プログラムを文字列の 並びとして処理します。ついでに言えば、このことが C とC++のプログラム開発環境 とツールが洗練されていないことの主な理由です。人間とコンパイラは違うプログラム を見ることになります。
残念なことに、あなたが「本当に愚かなこと」と考えることを他のプログラマが 絶対にしないと想定することはできません。たとえば、最近ある人から聞いたのですが、 彼らはgotoを含んだマクロに出くわしたそうです。私もそのようなマクロを見たことが ありますし、思わず納得してしまいそうになる議論を聞いたこともあります。 たとえば、次のようなコードです。
#define prefix get_ready(); int ret__ #define Return(i) ret__=i; do_something(); goto exit #define suffix exit: cleanup(); return ret__ void f() { prefix; // ... Return(10); // ... Return(x++); //... suffix; }
あなたが保守プログラマになって、このコードを渡されることを想像してみて ください。このマクロがヘッダファイルに「隠されている」と - そうするのは 珍しくないが - このような「マジック」を見つけだすのは困難です。
とてもありふれた分りにくい問題のひとつが、関数引数の受け渡し規則に従 わない関数スタイルのマクロです。たとえば、次のようなコードです。
#define square(x) (x*x) void f(double d, int i) { square(d); // よし square(i++); // だめ: (i++*i++)となる square(d+1); // だめ: (d+1*d+1)、つまり、(d+d+1)となる // ... }
"d+1"の方の問題は、呼び出し側の引数を括弧で囲むか、次のようにマクロ定義中 のパラメータを括弧で囲むことで解決します。
#define square(x) ((x)*(x)) /* より良い */
しかし、(おそらく意図していない)i++が 2 度評価されるという問題は依然とし て残ります。
もちろん、C/C++プリプロセッサマクロの問題が起らないマクロがあることは 知っています。しかし、C++のマクロを改善するつもりはありません。その代りに、 インライン関数、テンプレート、コンストラクタ(初期化)、デストラクタ(後処理)、 例外(コンテキスト脱出)といった、C++言語固有の機能を使用することをおすすめ します。
"cout"は「シーアウト(原文 "see-out")」と発音します。"c"は"character"の略 です。これは、iostreamsが値とバイト(文字)表現を対応づけるものであるという 理由からです。
"char"は普通「キャー(原文 "kar")」ではなく、「チャー(原文 "tchar")」と 発音します。"character"を「キャラクタ(原文 "ka-rak-ter")」と発音するので 辻褄が合わないように思えるかもしれませんが、英語の発音("pronounciation"で はなく、"pronunciation"と綴る:-)と綴りがそういったことに無頓着だと責める 人は誰もいませんでした。
どちらも正当なCとC++であり、まったく同じ意味を持つという意味で、どちらも 「正しい」です。言語の定義とコンパイラに関する限り、``int*p;''とすることも、 ``int * p;''とすることもできるでしょう。
``int* p;''と``int *p;''のどちらにするかは、どちらが正しいとか間違ってい るとかということではなく、どちらのスタイルにするか、何を強調するのかという ことです。C は式を強調しました。宣言は必要悪にすぎないと考えられることがよく ありました。一方、C++は型を強調します。
「典型的なCプログラマ」は``int *p;''と書き、「*pはintそのものである」と 構文を強調するように説明します。そして、そのスタイルの正しさを主張するため に、C(とC++)における宣言の文法をあげるでしょう。実際、文法上、* は名前 p に 結合します。
「典型的なC++プログラマ」は``int* p;''と書き、「p はintへのポインタである」 と型を強調するように説明します。実際、p の型は int* です。私の好みは明らかに こちらの強調の仕方であり、C++のより高度な部分をうまく使うために重要であると 考えます。
ひとつの宣言で複数のポインタを宣言しようとするとき(だけ)、大きな混乱が生 じます。
int* p, p1; // もしかしたらエラー: p1はint*ではない* を名前の方に近づけても、この種のエラーが起こりにくくなることはありません。
int *p, p1; // もしかしたらエラー?ひとつの宣言につきひとつの名前を宣言することで、(とくに変数を初期化するとき は)このような問題の発生を最小限に抑えることができます。次のように書く人は ほとんどいません。
int* p = &i; int p1 = p; // エラー: int*で初期化されるintもし上記のようなコードを書けば、コンパイラが文句を言います。
何かをするのにふたつの方法があると混乱が生じます。好みの問題であるなら、 議論はだらだらと絶えることなく続くでしょう。ひとつの宣言にはひとつのポインタ だけにして、常に変数を初期化するようにすれば、混乱のもとはなくなります。 C++の設計と進化に C の 宣言の構文に関する突っ込んだ議論がありますので、そちらを参照してください。
そのようなスタイルの問題は、個人の好みの問題です。度々、コードレイアウトに 関する意見が強くありますが、どのようなスタイルにするかよりも、一貫性を保つ ことの方が重要でしょう。ほとんどの人達と同じように、私は自分の好みのスタイル について、根拠のある筋の通った議論を組み立てることがなかなかできませんでした。
私自身は"K&R"スタイルと呼ばれるスタイルを使っています。そのスタイルに C に ない構文の規約を追加すると、"Stroustrup"スタイルと呼ばれることもあるスタイル になります。以下に例を示します。
class C : public B { public: // ... }; void f(int* p, int max) { if (p) { // ... } for (int i = 0; i<max; ++i) { // ... } }このスタイルは、他の多くのレイアウトスタイルよりも縦方向のスペースを節約して、 コードがスクリーンにほどよく収まるところが私の好みです。また、関数の開始の 括弧を新しい行に置くと、関数定義とクラス定義を見分けるのに役立ちます。
インデントはとても重要です。
デザインの問題、たとえば、主要なインタフェイスと しての抽象クラスの使用や、柔軟性のある型安全な抽象化を実現するテンプレート の使用、そしてエラーを表す例外の適切な使用といった ものの方が、レイアウトスタイルの選択よりもはるかに重要です。
「ハンガリアン記法」はお勧めしません。「ハンガリアン記法」(変数名に型名を 短縮したものを埋め込む記法)は、型のない言語にとっては有用なテクニックだと思 いますが、ジェネリックプログラミングとオブジェクト指向プログラミング - どちらも(言語あるいはランタイムのサポートによる)型に基いたオペレーションの 選択を重用する - をサポートする言語にはまったく適していません。そのような 言語では、「名前にオブジェクトの型を付ける」というのは、複雑になるだけで、 抽象度を下げてしまいます。言語の技術的詳細(たとえば、スコープ、記憶クラス、 構文カテゴリ)に関する情報を名前に埋め込むというのも、程度の差はありますが、 同じ問題を抱えています。変数名に型を暗示するものを埋め込むことが、時として 役に立つことがあるというのは認めますが、一般的に、特にソフトウェアが進化する につれて、メインテナンス上有害なものとなり、深刻な損失をもたらします。 災いの種である「ハンガリアン記法」は使わないようにしてください。
したがって、私は型にちなんで変数に名前をつけることは好みません。では、 私はどのような名付け方を好み、どのような名付け方を勧めるのかというと、 それは次のようなものです。変数(関数、型、その他どんなものでも)は、それが 何であるかや何をするかに基いて名前を付けてください。意味のある名前を選んで ください。つまり、あなたのプログラムの理解を助けるような名前を選んでください。 もし、あなたのプログラムの中で、x1, x2, s3, p7というような簡単にタイプできる 変数名を使ってそのまま放置していた場合、あなた自身でさえも、あなたの プログラムが何をするものであったのか理解するのが難しくなります。 略語と頭字語は混乱の元なので、控えめに使用してください。頭字語は控えめに 使用すべきです。たとえば、mtbf, TLA, myw, RTFM, NBVといったものを考えてみて ください。これらは一目瞭然ではありますが、私でさえも数ヶ月後には少なくとも ひとつは忘れてしまっているでしょう。
短い名前、たとえば x や i のような名前を慣例的に使用するのは効果的です。 つまり、x をローカル変数またはパラメータとして使用し、i をループインデックスと して使用するのはよいでしょう。
長すぎる名前は使用しないでください。タイプするのが面倒ですし、行がとても 長くなってしまい、スクリーンに収まらなくなります。そして、素早く読みづらく なります。以下の名前はOKでしょう。
partial_sum element_count staple_partition以下の名前はたぶん長すぎるでしょう。
the_number_of_elements remaining_free_slots_in_symbol_table私は識別子の中の単語を区切るのにelementCountやElementCountのようにするよりも、 アンダースコアを好んで使用します(例: element_count)。 すべての文字を大文字にした名前(例: BEGIN_TRANSACTION)は、慣例的にマクロでの 使用に予約されているので、絶対に使用しないでください。たとえあなたがマクロを 使用しないとしても、誰かがあなたのヘッダファイルをそういった名前で汚すかも しれません。 型名には先頭を大文字にした名前を使用してください(例: Square, Graph)。C++言語 と標準ライブラリでは大文字を使用しないので、Intではなくintとし、Stringでは なくstringとしています。こうすることで、標準の型と見分けることができます。
ミスタイプしやすい名前、読み間違いしやすい名前、混乱する名前は避けてくだ さい。たとえば次のような名前です。
name names nameS foo f00 fl f1 fI fi0, o, O, 1, l, Iという文字は特に問題を起しやすいものです。
命名規約の選択は、ローカルスタイル規則によって制限されることが多くあります。 あなたが最善と考える方法に事細かに従うよりも、一貫性のあるスタイルを保つこと の方が重要なことが多いということを忘れないでください。
何を実現したいかによります。
「大きい」とはどういう意味でしょうか。 2,3ワードよりも大きいものという意味です。
なぜ引数を変更したいのでしょうか。 それは、そうしなければならないことが多くあるからですが、他の方法もあります。 それは、新しい値を生成するという方法です。次のコードを考えてみてください。
void incr1(int& x); // インクリメント int incr2(int x); // インクリメント int v = 2; incr1(v); // vは3になる v = incr2(v); // vは4になる読む人にとっては、incr2()の方が理解しやすいのではないかと思います。 incr1()の方が誤りやエラーを起す可能性が高いです。したがって、新しい値の生成 とコピーが高くつかない限り、私は値を変更するスタイルよりも、新しい値を返す スタイルの方がよいと思います。
それでも私は引数を変更したいのですが、ポインタを使うべきですか。それとも リファレンスを使うべきですか。 説得力のある論理的な根拠は私にはわかりません。 もし「オブジェクトではないもの」(例: ヌルポインタ)を受け取り可能にするなら、 ポインタを使うのがよいでしょう。私自身は、オブジェクトを変更したいときに ポインタを使用します。なぜなら、コンテキストによっては、変更可能であること がわかりやすくなるからです。
メンバ関数の呼び出しは、基本的にオブジェクトに対して参照呼び出しであるこ とにも注意してください。そのため、オブジェクトの値/状態を変更したいときは、 しばしばメンバ関数を使用します。
私は前に置きますが、これは好みの問題です。"const T"と"T const"は以前から ずっと(どちらも)認められていて、同じ意味を持ちます。以下に例を示します。
const int a = 1; // OK int const b = 2; // こちらもOK最初の例の方が混乱するプログラマは少ない(そして、より慣用的である)のではない かと思います。
私が"const"(はじめは"readonly"という名前で、それに対応する"writeonly"も あった)を考案したとき、曖昧さがなかったので、"const"を型の前後どちらにでも 置けるようにしました。標準化される前の C とC++は、識別子の並び順の規則を押し 付けることは、(少しはあったとしても)ほとんどありませんでした。
並び順に関して当時熟考したことや交した議論は覚えていません。初期の少数の ユーザ(特に私自身)は当時、
const int c = 10;の方が、
int const c = 10;よりも単純に見た目が気に入っていました。
私が初期の頃に書いたプログラムでは"readonly"を使用していて、
readonly int c = 10;の方が、
int readonly c = 10;よりも読みやすいということに影響されていたのかもしれません。"const"を使用し ている最も初期の頃の(CまたはC++)コードは、(私が)"readonly"を"const"にすべて 置き換えて作成したもののようです。
私は何人かの人達 - Dennis Ritchieを含む - と別の構文について議論したこと を覚えていますが、そのときどの言語に注目していたのかは覚えていません。
ポインタ定数では、"const"は常に"*"の後に置くことに注意してください。 以下に例を示します。
int *const p1 = q; // int変数へのポインタ定数 int const* p2 = q; // int定数へのポインタ const int* p3 = q; // int定数へのポインタ
この文書は原著者の許可を得て翻訳、公開しています。
著者: Bjarne Stroustrup
原文:
Bjarne Stroustrup's C++ Style and Technique FAQ (2008年8月29日更新)
日本語訳: 神宮信太郎(jin@libjingu.jp)
日本語訳に関するコメント、誤りの指摘などありましたら、訳者までお知らせください。
日本語訳にあたり、以下の文献を参考にさせていただきました。