LoginSignup
0
2

More than 3 years have passed since last update.

Effective C++(第3版)まとめ

Last updated at Posted at 2020-09-13

諸言

 学習履歴全体のリンク
 https://qiita.com/shigural/private/444b991293e5ab1b40af

本文

Effective C++(第3版)

1.C++に慣れよう

1.C++を複数の言語の連合とみなそう

C++は4つのサブセットからなる
・C
 基本的文法

・オブジェクト指向C++
 クラス付きC、古典的オブジェクト指向

・テンプレートC++
 テンプレートやTMP、ただしTMPにはほとんど関わらない

・STL
 独自の方法論を持つ、コンテナや反復子などのライブラリ

2.#defineよりconst, enum, inlineを使おう

・プリプロセッサによる処理である#defineはデバッグしづらい
 →代わりにconstを使う

・クラス内に配列を使う場合、enumハックを用いる

enum{NumTurns=5};
int scores[NumTurns];

・defineマクロはインクリメントなどをするときに不具合を起こしやすい
→代わりにインライン関数を用いる。

3.可能ならいつでもconstを使おう

・関数の戻り値をconstにする
 Ex) *の戻り値をconstにすると、(a*b)=cに対してコンパイルエラーが出てくれる

・メンバ関数にconstを使う
 引数や返り値の参照にconstを使うと書き換え対策となる。

・const違いの関数を共通化する
 const有と非constなメンバ関数で本質的に同じ実装が必要な場合には非constなメンバ関数からconstなメンバ関数を呼ぶと良い

4.オブジェクトは使う前に初期化しよう

C++はある条件下で自動初期化が行われるが、ルールが複雑なので手動初期化が安全である

2.コンストラクタ、デストラクタ、コピー代入演算子

5.C++が自動で書き、自動で呼び出す関数を知ろう

コンパイラはデフォルトコンストラクタ、コピーコンストラクタ、コピー代入演算子、デストラクタを暗黙に生成する

6.コンパイラが自動生成することを望まない関数は使用を禁止しよう

次のいずれかの方法を利用する。方法1は手軽だがリンクエラーであり、方法2はコンパイルエラーであるため、できれば2が望ましい
1.コンパイラの自動生成がされるが使わせたくない関数はprivateに宣言し、定義を書かない。
2.Uncopyableのようなクラスをprivate継承する(Boostライブラリのnoncopyableクラス)

class Uncopyable{
 protected:
  Uncopyable(){}
  ~Uncopyable(){}
 private:
  Uncopyable(const Uncopyable&);
  Uncopyable& operator =(const Uncopyable&);
}

class test:private Uncopyable{//このクラス内でコピーコンストラクタやコピー代入演算子を宣言するとコンパイルエラー

}

7.ポリモーフィズムのための基底クラスには仮想デストラクタを宣言しよう

ポリモーフィズム:派生クラスのオブジェクトを基底クラスから操作するような設計
クラスA(変数a)を継承したクラスB(変数b)に対してのデストラクタで、bも開放するためにはAのデストラクタを仮想関数にすればよい。
また、Bを指すポインタをAにキャストしてデストラクタを使用されることの対策として、Aのデストラクタは純粋仮想関数にすればよい。

class A{
 virtual ~A()=0;
 ...
};
class B:public A{
 virtual ~B();
};

※6節のUncopyableのように基底クラスが派生クラスのオブジェクトを操作しない場合、ポリモーフィズムでないので仮想デストラクタは不要

8.デストラクタから例外を投げないようにしよう

デストラクタ内で例外が発生すると、解放なしにプログラムが終了してしまう。この対策として原則、デストラクタで例外は投げない。
やむを得ず例外を投げる場合には、デストラクタでabort関数を呼んでプログラムを終了するか、デストラクタでtry-catchを行い、失敗を飲み込むと良い

他に、デストラクタの代わりに例外を投げうる関数を予め実行するデストラクタ以外の関数を作っておいても良い

9.コンストラクタをやデストラクタ内では決して仮想関数を呼び出さないようにしよう

コンストラクタ・デストラクタから仮想関数を呼び出すと、生成中もしくは破棄中のオブジェクトの仮想関数が呼び出されてしまう

10.代入演算子は*thisへの参照を戻すようにしよう

代入演算子はa=b=cのように並べて使えるように、*thisへの参照を戻すようにする

11.operator=の実装では自己代入に備えよう

自己代入とはw=wのようなコードである。自己代入で問題が起こる例を示す

class Widget{
 ...
 pricvate:
  Bitmap *pb;
}
Widget::operator=(const Widget& rhs){
 delete pb;
 pb=new Bitmap(*rhs.pb);

 return *this;
}

この対策は大きく3つある
1.アドレスを比較する方法

if(this==&rhs)return *this;//自己代入の場合は何もしない

2.値を記録する方法

Bitmap *pOrig=pb;
pb=new Bitmap(*rts.pb);
delete pOrig;

3.渡す前にデータをコピーする方法→低速である

12.コピーするときはオブジェクトの全体をコピーしよう

複雑な理由により、コピー代入演算子の中でコピーコンストラクタを呼び出してはならない。
コピーする処理はすべての変数に対してコピーする処理を書いた関数

void init(PriorityCustomer& rhs){
 //コピー処理
}

を実装して、それをコピー代入演算子、コピーコンストラクタのそれぞれから呼び出す。
つまり、クラス内に変数が増えたときにはinitに処理を書き足す必要がある。

3.リソース管理

13.リソース管理にオブジェクトを使おう

リソース管理にはスマートポインタshared_prtを利用する。これによりリソースの開放漏れがなくなる。
なお、shared_prtは配列を表現できないが、配列表現が必要な場合にはvectorやstringを使う。どうしても配列に対してshared_prtが使いたければBoostのshared_arrayを利用する

スマートポインタのようにコンストラクタでリソースを受け取り、デストラクタで開放するクラスをRAIIといい、一般的に有用である

 //Investment:クラス名。これがオブジェクトを生成する
 //plnv:スマートポインタの指すオブジェクト名
 //createInvestment:関数名
 std::tr1::shared_prt<Investment> plnv(createInvestment())

 //つまりInvestment*型のオブジェクトを生成するcreateInvestment関数を指すポインタがplnvである

14.リソース管理クラスのコピーの振る舞いはよく考えて決めよう

自作の型に対してshared_prtは使えないため、自作する必要がある。このとき、オブジェクトがコピーされて(リソースは共通)どれか1つのオブジェクトが開放されると他のオブジェクトが指すリソースが不定となる。これを対策する方法がいくつかある
1.コピーを禁止する
2.コピーされた回数を数えておき、すべてのオブジェクトがなくなったときのみ開放する処理を実行する(shared_prtで利用)
3.それぞれのオブジェクトが別のリソースを指すようにする(つまりそれだけメモリは消費してしまう)
4.コピーが行われた場合、コピー元のオブジェクトを破壊する(auto_prtで利用)

15.リソース管理クラスには、リソースそのものへのアクセス方法をつけよう

リソース管理クラス(13節の例ではInvestment)には変数取得のためのget関数を作っておく必要がある。
また、暗黙的に変換する方法もある。
例えばリソース管理クラスFontがint型のFontHandleを返すとすると

//1.明示的な取得方法
//checkFont(f.get())のように利用する
Font::get(){
 return f;
}

//2.暗黙的な取得方法
//checkFont(f)のように利用する
operator FontHandle() const{
 return f;
}

といった実装ができる。2は便利であるが、安全性に欠ける面もある。

16.対応するnewとdeleteは同じ形のものを使おう

オブジェクトをnew[]として生成したならdeleteでも[]を使おう。[]を使っていない場合も同様である

17.newで生成したオブジェクトをスマートポインタに渡すのは独立したステートメントで行うようにしよう

例外が投げられた場合のリソース漏れ対策として、「newによる生成と、それをステートメントで渡す処理」は例外を投げ得る処理と独立したステートメントで行う必要がある

//万一priorityが例外を投げたらnew Widgetの戻すポインタが開放されない
processWidget(shared_prt<Widget>(new Widget),priority());

//安全
shared_prt<Widget> pw(new Widget);
processWidget(pw,priority());

4.デザインと宣言

18.インタフェースは正しく使うときには使いやすく、間違った使い方では使いにくいものにしよう

インタフェイスに新しい方を与えることでミスを減らすことができる

struct Month{
 explicit Month(int m)
 :val(m){}
 int val;
}

struct Day{
 explicit Day(int d)
 :val(d){}
 int val;
}
Data(Month(3),Day(30));//MonthとDayの順番を間違えないですむ

shared_ptrにはデリート時に関数を呼び出す「デリータの設定」を行うことができる
実装略

19.クラスのデザインを型のデザインとして考えよう

クラスは慎重にデザインされるべきである

20.値渡しよりconst参照渡しを使おう

高速化およびスライス問題の対策としてconst参照渡しをすると良い

21.オブジェクトを戻すときに参照を戻そうとしないこと

ポインタの指す先がなくなっている場合のことを想定し、returnでローカルな参照を戻してはならない

22.データメンバはprivate宣言しよう

publicにはデータメンバを置かず、全てprivateに置くことには複数のメリットが有る
1.publicを関数のみにすると一貫性があり、呼び出しミスをすることがない
2.書き込みのみ、読み込みのみの変数を生成できる
3.実装を自由に変えられる(カプセル化)
なお、これらの観点について、プログラムを修正するときに調べるべき関数の量からprotectedはpublicとそう変わらないことがわかる

23.メンバ関数より、メンバでもfriendでもない関数を使おう

カプセル化するためには、privateにアクセスできるメンバ関数を少なくすることが大切である。
この観点から、次の前者のコードよりも後者のコードのほうが優れている

//悪いコード
class Z{//メンバ関数
 A();
 B();
 C();
 D(){
  A();
  B();
  C();
 }
}

//良いコード
D(){//メンバ関数でない
 Z::A();
 Z::B();
 Z::C();
}

24.すべての引数に型変換が必要なら、メンバでない関数を宣言しよう

クラスRationalのコンストラクタを
Rational(int a=0,int b=1)
のように定義しておくと、Rational型を要求する引数にint型を与えても暗黙に型変換される。
ただし、クラス内で暗黙の型変換を利用した演算子を定義すると演算子の前後で順番を入れ替えたときに動作しないため、演算子の定義は非メンバ関数で行うと良い

class Rational{
 public:
 ...
 const Rational operator*(const Rational& rhs)const;//悪いコード(第1引数には暗黙の型変換が行われない)
};

const Rational operator*(const Rational& lhs,const Rational& rhs)const;//良いコード

25.例外を投げないswapを考えよう

難解であるため略

5.実装

26.変数の定義は可能な限り先延ばししよう

変数の生成には実行時間がかかる。このような無駄なコストをかけないために、変数の定義は使われることが確かで初期値が決まるまでは先延ばしするべきである。

//悪いコード(aを、初期化する値が決まる前に生成している)
A a;
for(...){
 a=...;
}

//良いコード
for(...){
 A a=...;
}

27.キャストは最小限にしよう

C型キャストとは
int(式)、(int)式
形式のキャストである。
また、C++には4種類のC++型キャストがある
const_cast(式):constを取り除くキャスト
dynamic_cast(式):安全なダウンキャスト(つまりあるクラスをその派生クラスにキャストする)。極めて低速である
reinterpret_cast(式):低レベルキャスト。たとえばポインタをintにキャストするときに使う
static_cast(式):明示的なキャスト

キャストはできるだけ避けるべきであり、避けられない場合キャストは関数の中に隠蔽してクライアントにはキャストを不要にするような設計をするべきである。
また、C型のキャストよりも、用途がはっきりしているC++型キャストを使うべきである。

28.オブジェクト内部のデータへの「ハンドル」を戻さないようにしよう

オブジェクト内部のハンドルを戻す関数は、カプセル性を高めるため、またハンドルが削除されたときに問題が起こるため、可能な限り避けるべきである。

29.コードを例外安全なものにしよう

例外が投げられても「リソース漏れを起こさず、データを有効に保つ」関数を例外安全な関数という。
保証をする方法の1つに、「コピーをして交換する」方法が挙げられるが、すべての関数で実際的であるわけではない

30.インラインをよく理解しよう

INライカは効率化が起こる反面、コードの肥大化やアップデートの困難性を呼ぶ。
そのため、インライン化は小さく、かつよく呼ばれる関数に限るべきである。

31.ファイル間のコンパイル依存性をなるべく小さくしよう

ソースコードは宣言に依存するように書くべきであり、またヘッダファイルには宣言のみを含むようにするべきである

6.継承とオブジェクト指向設計

32.public継承はis-a関係を表すようにしよう

public継承はis-a関係を表している。(飛べない)penguinをBirdから派生し、Birdにfly関数を実装したり、(幅と高さを独立に変えられない)SquareをRectangleクラスから派生することは誤りである

33.継承した名前を隠蔽しないようにしよう

派生クラスで基底クラスと同じ名前の関数を宣言すると、基底クラスの関数が呼び出されなくなる。
この時、「引数が違う」だけの理由であればオーバーロードも行われない。派生クラスで基底クラスと同じ名前の関数を宣言する場合、次のように分けられる
・取る引数が違う場合
派生クラスのpublic宣言の直後に
using Base::function();
のように記述し、基底クラスの関数を隠蔽しないようにする
・取る引数が同じ場合
基底クラスでの関数を仮想関数にしておき、overrideする
なお、既定クラスの関数から一部の関数のみを継承したければprivate継承を使うべきである

34.インタフェースの継承と実装の継承の区別を仕様

継承には3つのパターンが有る
インタフェースのみ:純粋仮想関数
インタフェースとデフォルトの実装:仮想関数
変えてはならない実装:非仮想関数

35.仮想関数の代わりになるものを考えよう

GameCharacterの健康度を戻す関数healthCalueを考える。これは派生クラスで書き換えて良いとすると

class gameCharacter{
 public:
  virtual int healthValue() const;
  ...
};

という実装が考えられる。これを仮想関数以外の実装方法でどのように実装可能か考える

・非仮想のインタフェースを使うテンプレートパターン
 NVIイディオムと呼ばれる、publicな非仮想関数からprivateもしくはprotectedな仮想関数を呼び出すデザイン

【長所】
事前処理・事後処理を書きやすい

class gameCharacter{
 public:
  int healthValue() const{
   ...//事前処理
   int retVal=doHealthValue()//実際の仕事
   ...//事後処理
  };
 private:
  virtual int doHealthValue() const{//派生クラスで書き換え可能な関数
   ...//デフォルトの実装
  }
  ...
};

・関数ポインタによるストラテジパターン
コンストラクタに関数ポインタを指定する方法

【長所】
便利であり、同じGameCharacterに対して異なる関数を設定することもできる

【短所】
publicでない関数にアクセスできない。そのため今までは必要のなかった関数を追加する必要があったり、friendにする必要があったりしてカプセル化を弱める

class GameCharacter;
int defaulHealthCalc(const GameCharacter& gc);

class GameCharacter{
  typedef int (*HealthCalcFunc)(const GameCharacter&)
  explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc):healthFunc(hcf){}
  int healthValue() const{return healthFunc(*this);}
  ...
 private:
  HealthCalcFunc healthFunc;
}

・tr1::functionによるストラテジパターン
関数ポインタによるストラテジパターンでfunctionにしたものである。関数ポインタと異なり、シグネイチャに変換できればなんでも引数に与えられる

【長所】
便利であり、同じGameCharacterに対して異なる関数を設定することもできる
 関数ポインタと比較して柔軟である

【短所】
publicでない関数にアクセスできない。そのため今までは必要のなかった関数を追加する必要があったり、friendにする必要があったりしてカプセル化を弱める

class GameCharacter;
int defaulHealthCalc(const GameCharacter& gc);

class GameCharacter{
  //HealthCalcFuncは引数として「const GameCharacter&型のものを受け取れ」、intに変換可能なものであれば何でも良い
  typedef std::tr1::function<int(const GameCharacter&)>HealthCalcFunc;
  explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc):healthFunc(hcf){}
  int healthValue() const{return healthFunc(*this);}
  ...
 private:
  HealthCalcFunc healthFunc;
}

std::tr1::functionを対象シグネイチャと言う。これは関数に限らず、クラスなども扱うことができる。

※難解であるため利用例略(std::bindで引数を減らした関数を実現できる など)

・古典的ストラテジパターン
 別のクラスに処理を書いておく方法

【長所】
簡単に利用できる

class GameCharacter;

class HealthCalcFunc{
 public:
  ...
  virtual int calc(const GameCharacter& gc)const{...}
  ...
}

HealthCalcFunc defaultHealthCalc;

class GameCharacter{
  explicit GameCharacter(HealthCalcFunc *phcf = defaultHealthCalc):pHealthCalc(phcf){}
  int healthValue() const{return pHealthCalc->calc(*this);}
  ...
 private:
  HealthCalcFunc *pHealthCalc;
}

36.非仮想関数を派生クラスで再定義しないこと

デザインの矛盾を起こしてしまい、変えてはならない基底クラスの処理を隠蔽してしまうので非仮想関数を派生クラスで再定義してはならない

37.継承された関数のデフォルト引数値を再定義しない

仮想関数を利用して派生クラスの関数を定義したとき、基底クラスでのみデフォルト引数は定義でき、派生クラスの関数を呼び出した場合でも基底クラスのデフォルト引数が使用される

38.コンポジションを使ってhas-a関係、is-implemented-in-terms-of関係を作ろう

コンポジションとは、オブジェクトが別のオブジェクトを内部にもつような構造である
has-a関係(所有関係)は「人」「乗り物」などの実世界のなにかに対応するオブジェクト、すなわちアプリケーション領域にあるオブジェクトとの関係を指す
is-implemented-in-terms-of関係(実装関係)は「バッファ」「検索木」のように実装上必要だけのオブジェクト、すなわちソフトウェアの実装領域にあるオブジェクトとの関係を指す

39.private継承は賢く使おう

is-implemented-in-terms-of関係を実装するためには、コンポジションの他にprivate継承を使う方法がある。
実装は可能な限りコンポジションで行い、できないときだけprivate継承で行えば良い。
具体的にはprotectedなメンバを使う場合や仮想関数をオーバーライドする場合が挙げられる。

private継承ではたらくEBO継承の話は難解であるため略

40.多重継承は賢く使おう

多重継承は複雑になりがちでコストが増大するため、避けるべきである
しかし、基底クラスの1つをインタフェースクラスとしてpublic継承したり、実装を助けるクラスからのprivate継承したりする場合に限っては正当な使い方となる。

7.テンプレートとジェネリックプログラミング

41.暗黙のインタフェースとコンパイル時ポリモーフィズムを理解しよう

テンプレートパラメータのインタフェースは暗黙的であるが、有効な式によって表現されていれば問題ない。

42.typenameの2つの意味を理解しよう

43.テンプレート化された基底クラス内の名前へのアクセス方法を知っておこう

44.パラメータに依存しないコードはテンプレートから外に出そう

45.「すべての互換型」を受け取るためにメンバ関数テンプレートを使おう

46.型変換をさせたいなら、メンバでない関数をクラステンプレートの中で定義しよう

47.型の情報に関してはtraitsクラスを使おう

48.テンプレートメタプログラミングを意識しよう

8.newとdeleteのカスタマイズ

49.new-handlerを理解章

50.どういうときにnewとdeleteの定義を自分で書くとよいか理解しよう

51.newとdeleteの定義を自分で書く場合はコンベンションに従おう

52.プレースメントnewの定義を書いたらプレースメントdeleteの定義も書こう

9.いろいろな事

53.コンパイラの警告に注意を払おう

コンパイラの警告は最大レベルにし、その内容を受け止めるべきである

54.TR1を含む標準ライブラリになじもう

TR1には次のようなコンポーネントがある
スマートポインタ tr1::shared_ptr(一般的なスマートポインタ), tr1::weak_ptr(相互参照に対応したshared_ptr)
一般化された関数ポインタ tr1::fuction(クラスなども関数ポインタで扱える)
tr1::bind 変数を関連付けることで、あ引数を減らすことができる
ハッシュテーブル set,multiset,map,multimapの対応物がハッシュテーブルによって行える
正規表現
tr1::tuple いくつでもオブジェクトをセットにできる
tr1::array STL化した静的配列
tr1::mem_fn メンバ関数へのポインタを統一的に扱う
tr1::reference_wrapper 参照をオブジェクトのように扱う
乱数の生成
数学の特殊関数 ラーゲル多項式、ベッセル関数、完全楕円積分など
C99ごかんな 拡張
型のtraits 方に関する情報をコンパイル時に知る(TMP用)
tr1::result_of 関数呼び出しの戻り値型を知るためのテンプレート(TMP用)

55.Boostに親しもう

BoostはC++のコミュニティwebサイトであり、TR1の他にも多くのライブラリを提供している。
Boostに自作関数を入れたり、コードレビューを受けることもできる

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2