
2026/05/09 2:06
メモリリークの対処方法(2022 年版)
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
提供されたテキストは、現代的な C++ ベストプラクティスに関する高レベルの要約ですが、詳細チェックリストに含まれている具体的な実行可能なガイダンスや技術的なニュアンスには欠けています。同様に、レガシーな慣行(C 式キャスト、生配列、マクロなど)からモダンな規格(RAII、
std::vector、static_cast など)への移行を正しく特定し、スト劳斯パープスの用語集や C++ Core Guidelines のようなリソースにも言及していますが、特定のファイル処理規則、コーディングスタイル規約(例:curly brace の配置、nullptr の使用)、コンストラクタ/デストラクターにおけるオブジェクトのライフタイム管理、ポリモーフィズムに対する具体的な戦略(ファクトリーパターン対仮想コンストラクタ)といった重要な実装の詳細に欠けています。
改善された要約: 本ドキュメントは、安全で効率的な C++ 開発のための現代ベストプラクティスを概説しており、レガシーな慣行よりも現在の業界標準への準拠を強調しています。このアプローチの中核には、自動メモリ管理のための RAII(Resource Acquisition Is Initialization)の使用があり、バッファオーバフローを防ぐために生配列を
std::vector で置換し、C 式キャストの代わりに static_cast を使用する点が挙げられます。開発者には、特定のコーディングスタイル(例:K&R レイアウト、curly brace を新しい行に配置する)、nullptr の使用を NULL に代えて使用し、仮想コンストラクタやマクロではなく、堅牢なインターフェース設計のために abstract base classes と factory patterns を採用するように導かれています。また、本テキストは、仮想関数とデストラクターの相互作用、派生クラスで純粋仮想関数をオーバーライドする必要性、コンストラクタ内 versus デストラクター内の例外処理に関する特定の規則など、重要な技術的な振る舞いを明確化しています。Bjarne Stroustrup の用語集や ISO C++ FAQ などの権威あるリソースを活用することで、開発者は陳腐化した保守上の問題を克服し、コンパイル時間の短縮、ツール互換性の向上、長期的なコードの健全性を高めるモダンで保守可能な基盤へと移行することができます。本文
C++ 様式および技法に関する FAQ(頻問集)
最終更新日:2022 年 2 月 26 日
以下は、私に頻繁に寄せられる C++ の様式(スタイル)および技法に関する質問です。もしより優れた質問や回答に対するご意見等があれば、お気軽にメールまでご連絡ください(bs at cs dot tamu dot edu)。なお、私には自家 Web サイトの改善に時間を費やす余裕がすべてあるわけではありませんことをご了承ください。
私は、C++ 財団(The C++ Foundation)の役員を務めることで、統一された新しい ISO C++ FAQ を維持しています。この FAQ のメンテナンスは今後、ますます間引きられる可能性が高いです。
また、「C++ コアガイドライン」(C++ Core Guidelines)には、現代の C++ を使用する際の多数の維持管理されているガイドラインが用意されています。
より一般的な質問については、私の一般 FAQ を参照してください。
用語や概念については、私の C++ 用語辞典を参照してください。
なお、これらは単なる質問と回答の集まりにすぎません。優れた教科書に見られるように厳選された例と説明の順を追った構成の代わりにはなりません。また、参考文献マニュアルや規格書で見られるような詳細で正確な仕様の提供もありません。C++ の設計に関するご質問については『C++ の設計と進化』を、C++ の使用方法およびその標準ライブラリーに関するご質問については『The C++ Programming Language』(TC++PL)を参照してください。
翻訳版:
- 中国語(一部に注釈付きの 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; // 成功返却 }
このプログラムに関するいくつかの観察点:
- これは標準ライブラリーを使用する標準 ISO C++ プログラムです。標準ライブラリーの機能は、.h 拡張子を付与しないヘッダーファイル内の namespace std で宣言されています。
- これを Windows マシンでコンパイルしたい場合は、「コンソールアプリケーション」としてコンパイルする必要があります。ソースファイルに .cpp という拡張子を付けるのを忘れないようにしてください。そうしないとコンパイラはそれが C(C++ ではない)のソースであると誤認する可能性があります。
- はい、main() は int を返します。
- 標準的なベクトルへ読み込むことは、特定のバッファをオーバーフローしないことを保証します。シリアスなミスを犯さずに配列へ読み込むことは完全な初心者には不可能です。その技術に慣れる頃には、もはや完全な初心者ではないことになります。この主張に懐疑的であれば、私の論文『Standard C++ を新しい言語として学ぶ』(Learning Standard C++ as a New Language)を読んでください。これは私の公開リストからダウンロード可能です。
はストリームの形式をテストするものです。具体的には、ループがファイル末尾 (EOF) を検知して終了したかどうかを検出します(そうでない場合は、期待される入力タイプまたは形式の入力が得られていないことを意味します)。詳細については、C++ の教科書で「ストリーム状態(stream state)」を検索してください。!cin.eof()- ベクトルは自身の大さを認識しているため、要素数を数える必要はありません。
- はい、i を単なる int ではなく
と宣言することで一部の過剰に警戒心の強いコンパイラによる警告を消せることは分かっています。しかし、この場合はそのようにする方が余計で混乱を招くと考えます。vector<double>::size_type - このプログラムには明示的なメモリ管理は一切含まれておらず、メモリリークも発生しません。ベクトルは要素を格納するために使用しているメモリを追跡しています。ベクトルが要素のために追加のメモリが必要な場合、それらを割り当てます。ベクトルのスコープが終了すると、そのメモリは自動的に解放されます。したがって、ユーザーはベクトル要素のためのメモリの割り当てと解放について心配する必要はありません。
文字列の読み込みについては、「How do I read a string from input?」を参照してください。
このプログラムは「ファイル末尾(end of file)」を検知すると入力読み込みを終了します。Unix マシンからキーボード経由でプログラムを実行する場合は、「ファイル末尾」を入力するには Ctrl-D を押してください。Windows マシンで、バグにより EOF 文字を認識しない場合は、入力を単語"end"で終了させるこのやや複雑なバージョンのプログラムの方をお勧めします:
#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; // 成功返却 }
標準ライブラリーを用いて単純なことを簡単に実行する方法のさらなる例については、『TC++PL4』の「標準ライブラリー入門(Tour of the Standard Library)」章を参照してください。
コーディング基準(スタンダード)をご推薦できますか?
はい:C++ コアガイドラインです。これは、現代の C++ の効果的なスタイルへ導くための野心あるプロジェクトであり、その規則をサポートするツールを提供することを目的としています。それは、パフォーマンスを犠牲にしたり冗長性が増したりすることなく、C++ を完全に型安全かつリソース安全な言語として使用するよう人々を奨励しています。ガイドラインプロジェクトに関するビデオもあります。
C++ のコーディング基準の主なポイントは、特定の環境下における C++ の使用のための規則のセットを提供することです。したがって、すべての用途およびすべてのユーザーに対する一つのコーディング基準などあり得ません。特定のアプリケーション(または企業、応用分野等)に対しては、コーディング基準がないよりも良いコーディング基準が存在します。一方で、私がこれまで見た多くの例において、悪いコーディング基準はコーディング基準がないよりも悪いことが示されています。
C のコーディング基準を使用しないでください(C++ に多少の修正を加えたものであっても)。また、10 年前の C++ のコーディング基準も使用しないでください(その時代には良いものであったとしても)。C++ は単に C ではありませんし、標準的な C++ も単にプレスタンダードな C++ ではありません。
クラス
なぜメンバー関数はデフォルトで virtual でないのですか?
多くのクラスは基底クラスとして使用されるよう設計されていないためです。例えば、
class complex を参照してください。
また、virtual な関数を持つクラスのオブジェクトには、virtual な関数呼び出し機構に必要なスペースが要求されます——通常、オブジェクトごとに 1 ワード程度です。このオーバーヘッドは著しく大きく、他の言語(例えば C や Fortran)からのデータとのレイアウト互換性の妨げになる可能性があります。
より詳細な設計の理由は『The Design and Evolution of C++』を参照してください。
なぜデストラクタはデフォルトで virtual でないのですか?
多くのクラスが基底クラスとして使用されるよう設計されていないためです。Virtual な関数は、派生クラスのオブジェクトに対してインターフェースとして動作することを意図されたクラス(通常はヒープ上に割り当てられポインタまたは参照を通じてアクセスされる)においてのみ意味を持ちます。
したがって、いつデストラクタを virtual に宣言すべきでしょうか?クラスに少なくとも 1 つの virtual な関数がある場合です。Virtual な関数を持つことは、そのクラスが派生クラスのオブジェクトに対してインターフェースとして動作することを示しており、そのような場合は派生クラスのオブジェクトが基底クラスのポインタを通じて破壊される可能性があります。例えば:
class Base { // ... virtual ~Base(); }; class Derived : public Base { // ... ~Derived(); }; void f() { Base* p = new Derived; delete p; // 仮想デストラクタを使用することで、~Derived が確実に呼ばれます }
もし Base のデストラクタが virtual でなかった場合、Derived のデストラクタは呼び出されず、Derive によって所有されているリソースが解放されないなどの望ましくない結果を招く可能性がありました。
なぜスコープの末尾でデストラクタが呼ばれないのですか?
簡単な答えは「もちろん呼ばれているのです!」ですが、よくあるようなこの質問に付随する例を見てみましょう:
void f() { X* p = new X; // use p }
すなわち、「new」で作成されたオブジェクトが関数の末尾で破棄されるという(誤った)仮定があったのです。基本的には、「new」を使用するのは、作成したスコープの寿命を超えてオブジェクトを存続させたい場合に限ります。そうした場合、「delete」を使用してそれを破棄する必要があります。例えば:
X* g(int i) { /* ... */ return new X(i); } // 作成した関数 g() の呼び出し後にも X は存続する void h(int i) { X* p = g(i); // ... delete p; }
スコープ内にのみオブジェクトの寿命を望む場合は、「new」を使用せず、単に変数を定義してください:
ClassName x; // use x
変数はスコープの末尾で自動的に破棄されます。new を使用してオブジェクトを作成し、その後同じスコープの末尾で delete するコードは醜く、エラーを起こしやすく、非効率です。例えば:
void fct() // 醜く、エラー起こりやすく、非効率 { X* p = new X; // use p delete p; }
クラスの階層(ヒエラルキー)
なぜコンパイルがそれほどうまくも長引きますか?
コンパイラの問題かもしれません。古いもの、誤ってインストールされたもの、またはお使いのコンピューターが昔物である可能性があります。そのような問題についてはお手伝いできません。
しかし、コンパイルしようとしているプログラム自体の設計が悪い場合の方が可能性が高いです。その結果、数百個のヘッダーファイルと数万行のコードを検討する必要があります。原則としてはこれは回避可能です。もしこの問題がライブラリーベンダーの設計に起因している場合(より良いライブラリーまたはベンダーへの変更を除き)は何もできませんが、自身のコードを構築して変更後の再コンパイルを最小限に抑えるように設計することは可能です。そのような設計は通常、分離の観点から優れているため、より保守しやすい設計となります。
オブジェクト指向プログラムの古典的な例を考えてみましょう:
class Shape { public: // Shapes のユーザーへのインターフェース virtual void draw() const; virtual void rotate(int degrees); // ... protected: // 実装者(Shapes の実装者)のための共通データ 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; // ... };
この考え方は、ユーザーが Shapes の公開インターフェースを通じて形状を操作し、派生クラスの実装者(例えば Circle や Triangle)は保護されたメンバーによって表される実装の側面を共有するというものです。
この一見単純なアイデアには 3 つの深刻な問題があります:
- すべての派生クラスに役立つ実装の共通側面を定義することは容易ではありません。そのため、保護されたメンバーのセットは公開インターフェースよりもはるかに頻繁に変更を必要とします。例えば、「center」がすべての Shapes のための有効な概念であるとしても、Triangle に対して Point "center" を維持しなければならないのは不便です——三角形については、誰かがそれを示す関心を持つ場合のみ中心点を計算する方が合理的です。
- 保護されたメンバーは、Shapes のユーザーが依存したくない「実装」の詳細に依存する可能性があります。例えば、多くの(多く?)Shape を使用しているコードは論理的に「Color」の定義には独立していますが、Shape の定義内にある Color の存在は、おそらくオペレーティングシステムのカラー概念を定義するヘッダーファイルのコンパイルが必要になるでしょう。
- 保護された部分の一部が変更されると、Shape のユーザーは再コンパイルを行わなければなりません——実装者(派生クラスの実装者)のみが保護されたメンバーにアクセスできるにもかかわらずです。
したがって、「実装者に役立つ情報」がユーザーのインターフェースとしても機能する基底クラスの存在が、実装の不安定さ、実装情報の変更によるユーザーコードの誤った再コンパイル、および「実装者に役立つ情報」に必要なヘッダーファイルを含めすぎてしまう原因となります。これは時には「脆い基底クラス問題(brittle base class problem)」と呼ばれています。
明確な解決策は、ユーザーへのインターフェースとして使用されるクラスから「実装者に役立つ情報」を省略することです。つまり、純粋なインターフェース(abstract class)としてインターフェースを表現することです:
class Shape { public: // Shapes のユーザーへのインターフェース 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: // Shapes のユーザーへのインターフェース 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; };
なぜデータをクラス宣言の中に置く必要があるのですか?
必要ありません。インターフェースにデータを持たないようにしたい場合は、インターフェースを定義するクラスにデータを置かないでください。代わりに派生クラスに置いてください。参照:『なぜコンパイルがそれほどうまくも長引きますか?』
時には、クラス内に表現データを持ちたい場合もあります。
class complex を考えてみてください:
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; };
この型は内蔵型として使用されるように設計されており、宣言内に表現データを置くことで真正なローカルオブジェクト(ヒープではなくスタックに割り当てられたオブジェクト)を作成できるようにし、単純な操作の適切なインライン化を確保します。真正なローカルオブジェクトとインライン化は、complex のパフォーマンスを内蔵 complex 型を持つ言語で提供されるものに近づけるために必要です。
なぜオーバーロードは派生クラスで機能しないのですか?(スコープを超えたオーバーロード)
その質問(多様なバリエーションを含め)は、次のような例によってよく誘発されます:
#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++ ではスコープを超えたオーバーロードはありません——派生クラスのスコープはこの一般的な規則の例外ではありません(詳細は D&E または TC++PL3 を参照)。
しかし、基底クラスと派生クラスのすべての f() 関数からオーバーロードセットを作成したい場合はどうすればよいでしょうか?using-declaration を使用すると簡単にできます:
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 になります。つまり、オーバーロード解決が B の f() および D の f() に適用され、最も適切な f() を選択しました。
私はクラスから派生する人を止められるのですか?
はい、できますが、なぜ止めたいのですか?一般的な答えは 2 つあります:
- 効率性のために:私の関数呼び出しが virtual でないようにするため
- 安全性のために:私のクラスが基底クラスとして使用されないようにするため(例えば、オブジェクトのコピーを行う際にスライシングの恐れがないことを保証するため)
私の経験では、効率性の理由は通常誤った恐怖に基づいています。C++ では virtual な関数呼び出しは非常に高速であり、virtual な関数を持つように設計されたクラスの現実世界の用途は、通常の関数呼び出しを使用する代替案と比較して測定可能な実行時間オーバーヘッドを生じません。virtual な関数呼び出し機構は通常、ポインタまたは参照を通じて関数を呼び出す場合のみ使用されます。名前付きオブジェクトに対する関数の直接呼び出しでは、virtual な関数クラスのオーバーヘッドは容易に最適化されます。
クラス階層を「頂点(キャップ)」して virtual な関数呼び出しを避けるための真の必要性がある場合、なぜ最初からそれらの関数が virtual なのかを考えます。パフォーマンスが重要な関数が単に「それが私たちの通常のやり方だから」という理由なしに virtual に設定された例を見たことがあります。
この問題のもう一つのバリエーション、つまり論理的な理由から派生を防ぐ方法は C++11 で解決されています。例えば:
struct Base { virtual void f(); }; struct Derived final : Base { // これよりDerived は final;これより派生することはできません void f() override; }; struct DD: Derived {// エラー:Derived は final です // ... };
古いコンパイラの場合、少し笨拙なテクニックを使用できます:
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(): private メンバーにアクセスできません
テンプレートと汎用プログラミング
なぜ vector を vector に割り当てられないのですか?
それは型システムの穴を開けてしまうことになるためです。例えば:
class Apple : public Fruit { void apple_fct(); /* ... */ }; class Orange : public Fruit { /* ... */ }; // Orange は apple_fct() を持たない vector<Apple*> v; // Apples のベクトル void f(vector<Fruit*>& vf) // 無害な Fruit 操作関数 { vf.push_back(new Orange); // フruits のベクトルにオレンジを追加する } 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 の最後の要素に遭遇した際例外を投げる必要があります。
なぜ C++ は heterogeneous containers(多様な要素を持つコンテナ)を提供しないのですか?
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;
これらのコンテナは homogeneous(単一型)です;すなわち、同じタイプの要素のみを持っています。コンテナに複数の異なるタイプの要素を持つようにしたい場合は、union として表現するか(通常ははるかに良い)、多態的な型のポインタのコンテナとして表現する必要があります。古典的な例は:
vector<Shape*> vi; // Shapes のポインタへのベクトルです。
ここで、vi は Shape から派生するすべてのタイプの要素を持てるようにします。つまり、vi はすべての要素が Shapes(正確には Shapes へのポインタ)であるという意味で homogeneous であり、Circle, Triangle などの多様な Shapes を含むことができるという意味で heterogeneous です。
したがって、ある種の意味ですべてのコンテナ(あらゆる言語における)は homogeneous です;それらを使用するためには、ユーザーが頼り得るすべての要素の共通インターフェースが必要です。heterogeneous であるとみなされるコンテナを提供する言語は、標準インターフェースを提供するすべての要素からなるコンテナを単に提供しています。例えば、Java コレクションは(参照先の)Objects からなるコンテナを提供し、(共通の)Object インターフェースを使用して要素の実質的なタイプを発見します。
C++ 標準ライブラリーは homogeneous コンテナを提供します;それは多数のケースで最も容易に使用でき、最良のコンパイル時エラーメッセージを与え、不必要な実行時オーバーヘッドを課さないためです。
C++ で heterogeneous コンテナが必要なのは、すべての要素の共通インターフェースを定義し、それらのコンテナを作成してください。例えば:
class Io_obj { /* ... */ }; // オブジェクト I/O に参加するために必要なインターフェース
vector<Io_obj*> vio; // ポインタを直接管理したい場合
vector< Handle<Io_obj> > v2; // オブジェクトを扱う"スマートポインタ"を持ちたい場合
実装の詳細に堕するべきではありません(必要がない限り):
vector<void*> memory; // 稀に必要なケース
Any クラス(例えば Boost::Any)を使用していくつかのプログラムで代替手段をとることができます:
vector<Any> v;
なぜ標準コンテナはそれほど遅いのですか?
そうではありません。「何と比較しているのか?」という方がより良い答えかもしれません。標準ライブラリーコンテナのパフォーマンスについて苦情を言う場合、私は通常、3 つの真の問題(または多くの神話と赤信号)のいずれかを見つけます:
- コピーオーバーヘッドに苦しんでいます
- ルックアップテーブルでの低速に苦しんでいます
- 私の手書き(intrusive)リストは std::list よりもはるかに高速です
最適化を試みる前に、真のパフォーマンス問題があるかどうかを検討してください。私に送信されたケースのほとんどで、パフォーマンス問題は理論的または架空です:まず測定し、必要である場合のみ最適化します。
それらの問題を順に見ていきましょう。多くの場合、vector は My_container という特定の専用コンテナよりも遅いです;My_container は X のポインタへのコンテナとして実装されているためです。標準コンテナは値のコピーを持ち、コンテナに代入するときに値をコピーします。これは本質的に小規模な値には打ち負かされませんが、巨大なオブジェクトの場合には非常に不適切になる可能性があります:
vector<int> vi;
vector<Image> vim;
// ...
int i = 7;
Image im("portrait.jpg"); // ファイルから画像を初期化
// ...
vi.push_back(i); // i の(コピーを)vi に入れる
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 の(コピーを)vi に入れる
vim.push_back(&im); // &im の(コピーを)vim に入れる
当然のことながら、ポインタを使用する場合、リソース管理について考える必要がありますが、ポインタのコンテナ自体も効果的で安価なリソースハンドルとなり得ます(通常、「所有」オブジェクトの削除のためのデストラクターを持つコンテナが必要になります)。
もう一つの頻繁に発生する真のパフォーマンス問題は、多数の(string, X)ペアのために map<string, X> を使用することです。マップは比較的少ないコンテナ(数百要素または数千要素——10000 要素のマップの要素へのアクセスのコストは約 9 つの比較)ではうまくいきます;less-than が安価で、良いハッシュ関数を作成できない場合です。多くの文字列と良いハッシュ関数の場合、ハッシュテーブルを使用します。標準委員会の技術報告書からの unordered_map は現在広く利用可能であり、ほとんどの人のホームブリューよりもはるかに優れています。
時には、(const char*, X)ペアではなく(string, X)ペアを使用して物事をスピードアップできますが、< が C スタイルの文字列に対して辞書順比較を行わないことを覚えておいてください。また、X が大きい場合、コピー問題も発生する可能性があります(通常のいずれかの方法で解決してください)。
intrusive リストは本当に高速です。しかし、リスト自体が必要かどうかを検討してください:vector はよりコンパクトであり、したがって多くの場合小さくかつ高速になります——挿入と削除を実行する場合でも。例えば、論理的に整数要素のリストを持っている場合、vector は(あらゆるリストよりも)著しく高速です。また、intrusive リストは内蔵型を直接保持できません(int は link メンバーを持っていません)。したがって、本当にリストが必要で、各要素タイプに link フィールドを提供できることを前提としてください。標準ライブラリーのリストはデフォルトで、要素を挿入する各操作に対して割り当ておよびコピーを実行し(要素を削除する各操作に対して解放を実行)します。std::list でのデフォルトアロケータの場合、これは著しくなります。小規模な要素でコピーオーバーヘッドが重要でない場合、最適化されたアロケータの使用を検討してください。手書きの intrusive リストは、リストと最後のオンスのパフォーマンスが必要な場所でのみ使用します。
人々は std::vector が段階的に成長するコストを心配することがあります。私は以前それを心配し、成長を最適化するために reserve() を使用していました。実際のプログラムで reserve() のパフォーマンス利益を見つけるのに困難を経験した後に、コードを測定し、必要な場合を除いてそれを使用することをやめました(マーカーの無効化を防ぐために必要である稀なケース)。再び:最適化する前に測定します。
なぜ sort() を使用する必要があるのか、そして"古い qsort()"があるのですか?
初心者にとっては、
qsort(array,asize,sizeof(elem),elem_compare);
は奇妙に見えるし、sort(vec.begin(),vec.end()); よりも理解しにくいものです。
エキスパートにとっては、同じ要素と同じ比較基準のために sort() が qsort よりも傾向として高速であるという事実がしばしば重要です。また、sort() は汎用的なので、あらゆる合理的な組み合わせのコンテナタイプ、要素タイプ、および比較基準で使用できます。例えば:
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() 関数を書く必要があると評価します。
より詳細な説明については、私の論文『C++ を新しい言語として学ぶ』を参照してください;これは私の公開リストからダウンロード可能です。
sort() が qsort よりも優位に立つ主な理由は、比較がインライン化されるためです。
関数オブジェクトとは何ですか?
当然のことながら、いくつかの方法で関数のように振る舞うオブジェクトです。典型的には、application operator - 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"; }
インライン application operator を持つ関数オブジェクトは、最適化者を混乱させる可能性があるポインタが関与しないため美しくインライン化されます。対照として:現在の最適化器は関数のポインタへの呼び出しをインライン化することは稀(おそらく決して)ありません。
関数オブジェクトは標準ライブラリーに柔軟性を提供するために広く使用されています。
メモリ管理
どのようにメモリリークに対処すればよいですか?
そのコードを書かないことです。
明らかに、あなたのコードが new 操作、delete 操作、および配列全体を通じてポインタ算術を多用している場合、どこかで誤った処理をし、リークや迷走ポインタを得ることになるでしょう。これは、割り当てに関するあなたの意識性とは関係なく真です;やがてコードの複雑さが、あなたが費やすことができる時間と努力を超えます。したがって、成功する技術は、割り当てと解放をより管理可能な型の中に隠すことに依存します。良い例としては標準コンテナがあります。それらは要素のためのメモリをあなたが非比例の努力なしによりも良く管理します。string および vector の助けなしにこれを書こうとは考えましたか:
#include<vector> #include<string> #include<iostream> #include<algorithm> using namespace std; int main() // 文字列を操作する小さなプログラム { 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'; }
それを最初から正しい chances のは?そして、リークがないことをどのように知るのでしょうか?明示的なメモリ管理、マクロ、キャスト、オーバーフローチェック、明示的なサイズ制限、およびポインタの欠如に注意してください。関数オブジェクトと標準アルゴリズムを使用して、イテレータへのポインタのような使用を排除できたかもしれませんが、そのような小さなプログラムには過剰なもののように思えました。
これらの技術は完璧ではなく、体系的に使用するのは常に容易ではありません。しかし、それらは驚くほど広く適用され、明示的な割り当ておよび解放の数を減らすことで、残りの例をトラッキングするのをはるかに容易にします。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 は暗黙に削除される }
メモリだけでなく、リソース全般について考えてください。
もしこれらの技術の体系的な適用があなたの環境では不可能である場合(他のコードを使用しなければならず、プログラムの一部をネアンダータール人が書いた、etc.)、標準開発手順の一部としてメモリリーク検出器を使用するか、あるいはガーベジコレクタープラグインしてください。
なぜ void から T に変換するためにキャストを使う必要があるのですか?**
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() よりも追加の利点を提供します:
- new は誤って正しい量のメモリを割り当てることができません
- new は暗黙的にメモリ枯渇をチェックします;および
- new は初期化を提供します
例えば:
typedef std::complex<double> cmplx;
/* C style: */ cmplx* p = (cmplx*)malloc(sizeof(int)); /* エラー:誤ったサイズ */ /* p==0 のテストを忘れた */ if (*p == 7) { /* ... */ } /* 悪い:*p を初期化することを忘れた */ // C++ style: cmplx* q = new cmplx(1,2); // メモリが枯渇した場合 bad_alloc を投げるでしょう if (*q == 7) { /* ... */ }
C スタイルと C++ スタイルの割り当て/解放を混合できますか?
はい、その意味では malloc() と new を同じプログラムで使用できます。いいえ、malloc() でオブジェクトを割り当てて delete で解放できません。また、new で割り当てて free() で削除することもできず、new によって割り当てられた配列に realloc() を使用することもできません。
C++ オペレーター new および delete は適切な構築と破壊を保証します;コンストラクタまたはデストラクターが呼び出される必要がある場所ではそれらが呼び出されます。C スタイル関数 malloc(), calloc(), free(), および realloc() は保証しません。さらに、new および delete が使用して生むリソースを解放するメカニズムが malloc() および free() と互換性を持つとは保証されていません。混合スタイルがシステム上で動作する場合、あなたは単に「幸運」だったということです;少なくとも一時的には。
realloc() の必要性を感じる場合(多くの人にそう),標準ライブラリーベクトルを使用することを検討してください。例えば
// 入力から単語をストリングのベクトルに読み取る:
vector<string> words;
string s;
while (cin>>s && s!=".") words.push_back(s);
ベクトルは必要に応じて拡大します。「Standard C++ を新しい言語として学ぶ」の例と議論も参照してください;これは私の公開リストからダウンロード可能です。
なぜ delete はそのオペランドを 0 にしないのですか?
考慮してください
delete p; // ... delete p; もし...部分で p を触らなければ、2 つ目の "delete p;" は重大なエラーであり、C++ 実装はそれを効果的に自己保護することはできません(例外的配慮なし)。ゼロポインタを削除するのは定義上無害なので、単純な解決策は"delete p;"が何らかの必要事項を処理した後、"p=0;"を行うことです;しかし C++ はそれを保証しません。
理由の一つは、delete のオペランドは lvalue でない必要があるためです。考慮してください:
delete p+1;
delete f(x);
ここで、delete の実装にはゼロを割り当てることができるポインタを持っていません。これらの例は稀かもしれませんが、それらは「削除されたオブジェクトへの任意のポインタが 0 である」ことを保証することは不可能であることを意味します。「ルール」を取り逃すためにより簡単な方法は、オブジェクトへの 2 つのポインタを持つことです:
T* p = new T;
T* q = p;
delete p;
delete q; // うっ!
C++ は明示的に delete の実装が lvalue オペランドをゼロ化することを許可しています;私は実装者がそれを行うことを望んでいましたが、そのアイデアは実装者の中で人気がなくなったようです。
ポインタのゼロ化を重要と考えられる場合、destroy 関数を使用することを検討してください:
template<class T> inline void destroy(T*& p) { delete p; p = 0; }
new および delete の明示的な使用を最小限にするために、標準ライブラリーコンテナ、ハンドルなどを頼るための理由のもう一つの例を検討してください。
ポインタを参照(ポインタをゼロ化できるように)として渡すことには、destroy() が rvalue に対して呼び出されることを防ぐという追加の利点があります:
int* f();
int* p;
// ...
destroy(f()); // エラー:非 const 引用で rvalue を渡そうとしています
destroy(p+1); // エラー:非 const 引用で rvalue を渡そうとしています
クラス内の定数をどのように定義すればよいですか?
定数式で使用可能な定数(例えば配列境界)を持つ定数を望む場合、2 つの選択肢があります:
class X { static const int c1 = 7; enum { c2 = 19 }; char v1[c1]; char v2[c2]; // ... };
一見すると、c1 の宣言の方が清潔に思えますが、クラス内初期化構文を使用するには、定数はintegral またはenumeration タイプで静的 const である必要があります。これは非常に制限的です:
class Y { const int c3 = 7; // エラー:static でない static int c4 = 7; // エラー:const でない static const float c5 = 7; // エラー:integral でない };
私は「enum trick」を使用する傾向があります;それは移植可能であり、クラス内初期化構文の非標準拡張を使用することに誘発しないためです。
なぜこれらの不都合な制限が存在するのか?クラスは通常ヘッダーファイルで宣言され、ヘッダーファイルは通常多くの翻訳単位に含められます。しかし、複雑なリンカー規則を避けるために、C++ はオブジェクトが一意の定義を持つことを要求します;それはメモリ上オブジェクトとして保存される必要がある実体へのクラス内定義を許可した場合にそのルールが破られるでしょう。D&E で 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 は lvalue でない const int* p2 = &AE::c7; // OK // ... }
なぜスコープの末尾でデストラクタが呼ばれないのですか?
簡単な答えは「もちろん呼ばれているのです!」ですが、よくあるこの質問に付随する例を見てみましょう:
void f() { X* p = new X; // use p }
すなわち、「new」で作成されたオブジェクトが関数の末尾で破棄されるという(誤った)仮定があったのです。基本的に、「new」を使用するのは、作成したスコープの寿命を超えてオブジェクトを存続させたい場合に限ります。そうした場合、「delete」を使用してそれを破棄する必要があります。例えば:
X* g(int i) { /* ... */ return new X(i); } // 作成した関数 g() の呼び出し後にも X は存続する
void h(int i) { X* p = g(i); // ... delete p; }
スコープ内にのみオブジェクトの寿命を望む場合は、「new」を使用せず、単に変数を定義してください:
ClassName x;
// use x
変数はスコープの末尾で自動的に破棄されます。new を使用してオブジェクトを作成し、その後同じスコープの末尾で delete するコードは醜く、エラーを起こしやすく、非効率です。例えば:
void fct() // 醜く、エラー起こりやすく、非効率 { X* p = new X; // use p delete p; }
エクセプション
なぜエクセプションを使用すべきですか?
エクセプションの使用が私にどのようなメリットがあるのでしょうか?基本的な答えは:エラー処理のためにエクセクションを使用すると、コードをシンプルでクリーンにし、エラーを見逃す可能性を低くします。しかし「古い errno および if 文」はどうでしょうか?基本的な答えは:それらを使用する場合、エラー処理と通常のコードが密接に絡み合っています。そのため、コードが乱雑になり、すべてのエラーに対処したことを保証するのが困難になります(スパゲッティコードやネズミの巣のようなテスト)。
まず第一に、エクセプションなしでは正しいことができないものがあります。コンストラクター内で検出されたエラーを考えてみてください;エラーをどのように報告するのでしょうか?例外を投げます。それは RAII(リソース獲得は初期化)の基礎であり、いくつかの有効な現代 C++ 設計技法の基礎です:コンストラクターの仕事はクラスの不変量を確立すること(メンバー関数が動作する環境を作成すること)であり、これは通常リソース(メモリ、ロック、ファイル、ソケット等)の取得を必要とします。
想像してください;例外がない場合、コンストラクター内で検出されたエラーに対処する方法はどうでしょう?コンストラクターはしばしば変数内のオブジェクトを初期化/構築するために呼び出されますことを思い出してください:
vector<double> v(100000); // メモリを割り当てる必要がある
ofstream os("myfile"); // ファイルを開く必要がある
vector または ofstream(出力ファイルストリーム)コンストラクターは、変数を"bad"状態に設定することを選択できます(ifstream がデフォルトで行うように);その後すべての操作が失敗します。これは理想的ではありません。例えば、ofstream の場合、開き操作の成功を確認することを忘れた場合、出力は単に消えます。多くのクラスではその結果は悪くなります。少なくとも、以下のように書く必要があります:
vector<double> v(100000); // メモリを割り当てる必要がある
if (v.bad()) { /* エラー処理 */ } // vector は実際に bad() を持っていません;例外に依存します
ofstream os("myfile"); // ファイルを開く必要がある
if (os.bad()) { /* エラー処理 */ }
これはオブジェクトごとに追加のテスト(書き込むために、または忘れるために)です。それは特にサブオブジェクトが互いに依存する場合に複雑になります。詳細については『The C++ Programming Language』第 8.3 章、第 14 章、および付録 E または(より学術的な)論文「Exception safety: Concepts and techniques」を参照してください。
したがってコンストラクターを書くことは例外なしではトリッキーですが、単なる古い関数はどうでしょうか?エラーコードを返すか、非ローカル変数を設定する(例えば errno)。グローバル変数を設定する方法はよく機能しません;即時にテストするか(または他の関数がそれを再設定したかもしれない)、その方法を考えてはいけません。もし複数のスレッドがグローバル変数にアクセスする可能性がある場合は特にそうではありません。戻り値のトラブルは、エラー返り値を選択するには巧妙さが必要で、不可能であることです:
double d = my_sqrt(-1); // エラーの場合 -1 を返す
if (d == -1) { /* エラー処理 */ }
int x = my_negate(INT_MIN); // うっ?
my_negate() が返す値は存在しません;各 int はある int に対する正しい答えであり、2 の補数の表現で最も負の数字には正解がありません。そのような場合、値ペアを返す必要があります(そして通常テストする必要があります)。詳細な説明については私のプログラミング入門書の例と説明を参照してください。
エクセクション使用に対する一般的な反対意見:
- ただしエクセプションは高価です!: 本当にそうではありません。現代 C++ 実装は例外使用のオーバーヘッドを数%(例えば 3%)に削減し、それはエラー処理なしと比較しています。エラー返りコードおよびテストでコードを書くのも無料ではありません。経験則として、例外を投げていない場合、例外処理は非常に安価です。いくつかの実装では何のコストもありません。すべてのコストは例外を投げた場合に発生します;すなわち「通常のコード」はエラー返りコードおよびテストを使用するコードよりも高速です。エラーがある場合にのみコストがかかります。
- あなた自身がエクセプションを明確に禁止しました!: JSF++ はハードリアルタイムおよび安全性クリティカルアプリケーション(飛行制御ソフトウェア)向けです。計算に時間がかかりすぎる場合、誰かが死ぬ可能性があります。その理由から、私たちは応答時間を保証する必要があり、現在のツールレベルでは例外にはそれをできません。そのような文脈では、even free store allocation も禁止されます!実際、JSF++ のエラー処理に関する勧告は、例外を使用することをシミュレートしており、道具を使いこなす日の anticipation です。
- ただし new で呼ばれたコンストラクターから例外を投げるとメモリリークを引き起こします!: 無意味!それは一つのコンパイラのバグによって引き起こされた古い老婆の話です;そのバグはすぐに一昔前に修正されました。
キャッチした後に再開できませんか?
他の言葉で、なぜ C++ が例外が投げられた点に戻りそこから継続を実行するための原語を提供しないのですか?基本的に、エクセプションハンドラーから再開する人々は、throw の後からのコードが何事も発生しなかったように実行が継続されるように書かれたことを決して保証できません。エクセクションハンドラーは再開前に「正しく」しなければならないコンテキストの量を知ることはできません。そのようなコードを正しくするためには、throw の書き手と catch の書き手が互いのコードおよびコンテキストに親密な知識を持つ必要があります。これは複雑な相互依存性を生じさせ、許可されているあらゆる場所で深刻なメンテナンス問題を招いています。
私は C++ エクセクションハンドリング機構を設計した際に再開の可能性を真剣に検討しましたが、この問題は標準化中にかなり詳細に議論されました。『The Design and Evolution of C++』のエクセプションハンドリング章を参照してください。
問題が発生する前にチェックしたい場合、問題が局所処理できない場合にのみチェックしその後 exceptions を投げない関数を呼び出してください。new_handler はその例です。
なぜ C++ には realloc() の同等物が存在しないのですか?
望む場合はもちろん realloc() を使用できます。ただし、realloc() は malloc()(および類似の機能)によって割り当てられた配列でのみ動作することが保証されており、ユーザー定義のコピーコンストラクターを有さないオブジェクトを含んでいます。また、naive な期待に反して、realloc() は稀に引数配列をコピーすることを覚えておいてください。
C++ では、リアルロケーションの対処に対するより良い方法は、標準ライブラリーコンテナ(例えば vector)を使用し、それらが自然に成長させることです。
なぜコンストラクターから例外を投げられないのですか?デストラクターから?
はい:適切に初期化(構築)できないオブジェクトがあるたびに、コンストラクターから例外を投げるべきです。throw によってコンストラクターを終了する代わりに満足な代替案はありません。
いいえ:デストラクター内で例外を投げることができますが、それはデストラクターから離れるべきではありません;デストラクターが throw で終了した場合、標準ライブラリーおよび言語自体の基本的規則違反を引き起こす可能性のあるさまざまな悪いことが起こり得ます。それをやめてください。
例と詳細な説明については、『The C++ Programming Language』の付録 E を参照してください。
留意点:エクセプションはいくつかのハードリアルタイムプロジェクトで使用できません。例えば、JSF 航空機 C++ コーディング基準を参照してください。
エクセプションを使用すべきでないものは何ですか?
C++ エクセプションはエラー処理をサポートするように設計されています。throw をエラー信号にのみ使用し、catch をエラー処理アクションにのみ指定してください。他の言語で人気のある他のエクセプションの使用もありますが、それは C++ でのidiomatic ではなく、意図的に C++ 実装によって良くサポートされていません(それらの実装は例外がエラー処理のために使用されるという前提に基づいて最適化されています)。
特に、throw は単に関数から値を返す別の方法ではありません(return に似ています)。そうすると遅くなり、多くの C++ プログラマーを混乱させます;彼らは例外をエラー処理専用として見ていることに慣れています。同様に、throw はループからの脱出の好い方法ではありません。
なぜ C++ には"finally"構文が提供されないのですか?
C++ は常にほぼ優れている代替案をサポートします:「リソース獲得は初期化」技法(TC++PL3 section 14.4)。基本的なアイデアは、リソースをローカルオブジェクトとして表すことであり、その結果ローカルオブジェクトのデストラクターがリソースを解放することです。それによって、プログラマがリソースを解放することを忘れることはありません。例えば:
class File_handle { FILE* p; public: File_handle(const char* n, const char* a) { p = fopen(n,a); if (p) throw Open_error(errno); } File_handle(FILE* pp) { p = pp; if (p) 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"構建の使用よりも少ないコードを生じます。
また、『The C++ Programming Language』付録 E のリソース管理例も参照してください。
エクセプションをどのように使用すればよいですか?
『The C++ Programming Language』第 8.3 章、第 14 章、および付録 E を参照してください。付録は要求の高いアプリケーションで例外安全なコードを書くための技法に焦点を当てており、新人向けには書かれていません。
C++ では、ローカルで処理できないエラー(例えばコンストラクターでのリソース獲得の失敗)を信号するためにエクセプションを使用します。例えば:
class Vector { int sz; int* elem; class Range_error { }; public: Vector(int s) : sz(s) { if (sz<0) throw Range_error(); /* ... */ } // ... };
エクセプションを単に別の関数から値を返す方法として使用しないでください。大多数のユーザーは言語定義が鼓励他们するように——例外処理コードはエラー処理コードであると仮定し、実装はその仮定を反映するために最適化されています。
重要な技法はリソース取得は初期化(RAII と略称)であり、それはデストラクターを持つクラスを使用してリソース管理に順序をもたらします。例えば:
void fct(string s) { File_handle f(s,"r"); // ファイルハンドルのコンストラクターが"s"と呼ばれるファイルを開く // f を使用 } // ここで File_handle のデストラクターはファイルを閉じる
もし fct() の"use f"部分が例外を投げた場合、デストラクターもまだ呼び出され且つファイルは適切に閉じられます。これは一般的な不安全な使用と対照的です:
void old_fct(const char* s) { FILE* f = fopen(s,"r"); // ファイル"s"を名づけて開く // use f fclose(f); // ファイルを閉じる }
もし old_fct の"use f"部分が例外を投げた場合——または単に return をした場合——ファイルは閉じられません。C プログラムでは longjmp() も追加の危険です。
その他の言語機能
"void main()"を書けますか?
定義
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 は、プログラムからそのシステムに値を返す方法です;そのようなファシリティを提供しないシステムでは戻り値は無視されますが、それは"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() の戻り値の型が欠落しています。
なぜ dot, ::, sizeof などをオーバーロードできませんか?
ほとんどのオペレーターはプログラムによってオーバーロードできます。例外は
. (dot) :: ?: sizeof です。:?のオーバーロードを禁止する根本的な理由は存在しません;私は単に三元演算子の特別ケースを導入する必要性が見えなかっただけです。関数が 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) 大きいはずである
したがって、プログラマがプログラムによって新しい異なる意味を賦与することは不可能であり;それは基本的な言語規則に違反します。
N::m では N または m は値を持つ式ではありません;N および m はコンパイラに知られた名称であり、:: は式評価ではなく(コンパイル時)スコープ解決を実行します。x::y をオーバーロードすることを許可することを想像できますが、x は名前空間またはクラスではなくオブジェクトである場合でも、それは新しい構文を導入することになるでしょう(expr::expr を許容するため)。そのような複雑化がもたらす利益は明らかではありません。
オペレーター . (dot) は原則として->で使用されているのと同じ技法を使用してオーバーロードできます。しかし、そうすることは、. をオーバーロードするオブジェクトのために操作が意図されているのか、.によって参照されるオブジェクトのために操作が意図されているのかという疑問を導く可能性があります。例えば:
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 またはエラー? }
この問題はいくつかの方法で解決できます。標準化の時点で、どの方法が最良であるか明らかではありませんでした。詳細については D&E を参照してください。
独自のオペレーターを定義できますか?
申し訳ありません;いいえです。その可能性は何度か検討されましたが、毎回私たちが可能性のある問題の可能性のある利益を上回ると判断しました。それは言語技術の問題ではありません。私が 1983 年に最初に行った場合でも、それをどのように実装するかを知っていました。しかし、私の経験は、最も自明の例を超えると、人々はオペレーターの使用の"明白な"意味について微妙に異なる意見を持っているように見えることです。古典的な例は abc です。が累乗を表すように設定されていると仮定してください。今 abc は (ab)c または a(b**c) を意味すべきでしょうか?私は答えが明白だと考え、友人が同意しました;そして私たちは明白な解決が一致していないことに気づきました。私の推測は、そのような問題は微妙なバグに導くでしょう。
"const"をタイプの前または後に置くべきですか?
私は前になりますが、それは好みの問題です。「const T」と「T const」は-そして今も(どちらも)許可され同等です。例えば:
const int a = 1; // OK
int const b = 2; // または OK
私の推測は、最初のバージョンを使用すると fewer プログラマーを混乱させるでしょう(「よりidiomatic」)。
なぜ?私が"const"を発明したとき(最初は"readonly"と呼ばれ対応する"writeonly"がありました)、私はタイプの前または後に置くことを許可しました;それは曖昧さなしにできたからです。プレスタンダードな C および C++ は指定子の順序に少数(もしあれば)の順序規則を課していました。
当時の深い思考や議論については記憶していません。いくつかの初期ユーザー—特に私—は単に"const int c = 10;"の方が"int const c = 10;"の方が良いためにその時代が好きでした。私は最も早い例が"readonly"を使用して書かれていたという事実の影響を受けたかもしれません;
readonly int c = 10; は int readonly c = 10; よりも良く読みます。最初(C または C++)の"const"を使用するコードは、私によって作成された"readonly"へのグローバル置換で作成されたように思えます。私は syntax alternates をいくつかの人々と議論しました—Dennis Ritchie を含む—;しかし、その時点で私がどの言語を見たか記憶していません。
const ポインタでは、「const」は常に"*"の後にきます。例えば:
int *const p1 = q; // 定数の int 変数へのポインタ
int const* p2 = q; // 定数 int へのポインタ
const int* p3 = q; // 定数 int へのポインタ
static_cast は何のメリットがありますか?
キャストは一般的に避けるべきです。dynamic_cast の例外を除き、その使用は型エラーの可能性や数値値の切断を示唆します。無害に見えるキャストであっても、開発またはメンテナンス中に関与するタイプのいずれかが変更される場合、重大な問題になる可能性があります。例えば、これは何を意味しますか?
x = (T)y; 私たちは分かりません;それは T タイプおよび x および y のタイプに依存します。T はクラスの名称、typedef またはテンプレートパラメータの名称かもしれません。おそらく x および y はスカラー変数であり、(T) は値変換を表します。おそらく x は y クラスから派生したクラスで (T) はダウンキャストです。おそらく x および y は関係のないポインタタイプです。C スタイルのキャスト (T) が多くの論理的に異なる操作を表現するために使用できるため、コンパイラーは誤用を検出する機会が最小限だけです。同じ理由から、プログラマは exactly 何をするか分からない場合があります。これは新人プログラマによって時々有利と見なされ、新人が間違った場合微妙なエラーの源です。
「新しいスタイルのキャスト」はプログラマに意図をより明確に述べることおよびコンパイラーに多くのエラーを検出する機会を与えるために導入されました。例えば:
int a = 7;
double* p1 = (double*) &a; // OK(ただし a は double でない)
double* p2 = static_cast<double*>(&a); // エラー
double* p2 = reinterpret_cast<double*>(&a); // OK:私は本当にそう意味します
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); // 私は本当にそう意味します
アイデアは、static_cast によって許可される変換は reinterpret_cast を必要とするよりもエラーを引き起こす可能性が低いということです。原則的には static_cast の結果を元のタイプにキャストせずして使用することもできます;reinterpret_cast の結果を使用する前に常にその元のタイプに戻す必要があります;それは移植性を保証するためです。
新しいスタイルのキャストを導入するための二次的な理由は、C スタイルのキャストはプログラムで非常に目立たないことです。例えば、通常のエディターまたはワープロソフトウェアを使用して CAST を検索することはできません。この C スタイルのキャストの近視的不見え方は彼らが潜在的に損害を与えるため特に不幸です。醜い操作には醜い構文形式を持つべきです;その観察は新しいスタイルのキャストの構文を選択するための理由の一部でした。さらに、新しいスタイルのキャストはテンプレート記法と一致するようにする理由は、プログラマが独自の CAST を書くことができます;特に動的にチェックされるキャストです。
もしかすると static_cast が醜くそして比較的入力しにくいので、使用する前に一度考える可能性が高いでしょうか?それは良いことです;キャストは現代 C++ で本当にほとんど回避可能です。
なぜ空クラスのサイズがゼロでないのですか?
2 つの異なるオブジェクトのアドレスが異なっていることを保証するためです。同じ理由で、"new"は常に別のオブジェクトへのポインタを返します。考慮してください:
class Empty { }; void f() { Empty a, b; if (&a == &b) cout << "impossible: report error to compiler supplier"; Empty* p1 = new Empty; Empty* p2 = new Empty; if (p1 == p2) cout << "impossible: report error to compiler supplier"; }
空の基底クラスは別々のバイトで表される必要がないという興味深い規則があります:
struct X : Empty { int a; // ... }; void f(X* p) { void* p1 = p; void* p2 = &p->a; if (p1 == p2) cout << "nice: good optimizer"; }
この最適化は安全であり、最も有用です;それはプログラマが空のクラスを使用して非常に単純な概念をオーバーヘッドなしで表現することを可能にします。いくつかの現在のコンパイラーはこの「空の基底クラス最適化」を提供します。
なぜ C++ ではいくつかのものが未定義のままにされているのですか?
マシンが異なり、C が多くのものを未定義にしたためです。詳細については、用語「未定義」、「未指定」、「実装で定義」、「適切形成」の定義を含む 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++ は互換性の理由および C++ が直接アセンブラー(OS、組み込みシステム、およびいくつかの数値計算分野)で競合するため C を追跡しました。範囲チェックが必要であれば、適切なチェック済みクラス(vector, smart pointer, 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++) のようなコードに対して警告しないことに失望しています。同様に、引数の評価順序も未指定です。
私の意見では、あまりにも多くの「もの」が未定義、未指定、実装で定義されています;しかし、それは言いかえれば容易であり例を与えることも簡単ですが、修正するのは困難です。また、ほとんどの問題を避けて移植可能なコードを生産するのはそれほど困難ではないことも注目に値します。
入力から文字列をどのように読み取ればよいですか?
単一の空白で終結された単語を読み込むことができます:
#include<iostream> #include<string> using namespace std; int main() { cout << "Please enter a word:\n"; string s; cin>>s; cout << "You entered " << s << '\n'; }
注意: