Edited at

Effective C++(項1〜5)解説

More than 1 year has passed since last update.


この記事に関して

Scott Meyers著のEffective C++の解説をやっていきます。


Effective C++とは



Scott Meyers著のC++でソフトウェアを書くときのコツをまとめた本です。

55項のコツがまとめられており読むたびに新しい発見がある奥の深い本です。

昨今はC++の変化が激しいために版によって内容が大きく異なります。

第2版で見られた内容が第3版にはないということもしばしば。

この記事は第3版の英語で書かれた原著を参考にして書いています。

記事を書くにあたって参考にした本はAmazonより購入できます。

作者のScott Meyersはアメリカのソフトウェアコンサルタントです。



25年にもわたる長い開発経験に基づきこの本を執筆しています。

初心者にとっては「そんなこと考えもしなかった!」というようなことまで深く考察しているから、C++の達人ということが誰にでもわかります。

このScott Meyersは他にも多くの書籍を執筆していてどれもとても参考になるものばかり。

詳しくはScott Meyersのホームページを見てみてください。

それでは、各項を解説していきます。


項1 C++を複数の言語の集合体と考えよう。(View C++ as a federation of languages)

C++は複数の言語の集合体と考えることができます。


  1. C

  2. オブジェクト指向言語としてのC++

  3. テンプレートC++

  4. STL

C++を取り扱う際のルールはC++のどの部分を取り扱っているかによって変わってきます。


項2 #defineよりもconst、enum、inlineを使おう(Prefer const, enums and inline to #define)

Cプログラムを読んでいる時に出会うこのようなコード。


example.cpp

#define BETA 0.28016


マクロを使って定数を定義しています。

しかし、このコードには大きな問題があります。

なぜなら、マクロはプログラムをコンパイルする直前にソースコードにある指定された文字列を置き換えているに過ぎないからです。このためバグが発生します。


example2.cpp

#include <stdio.h>


#define SIX 1 + 5
#define NINE 8 + 1

int main(void)
{
printf( "What you get if you multiply six by nine: %d\n", SIX * NINE );
return 0;
}

これを解決するには単純に#defineを使うのをやめるべきです。

定数を定義する際にはconstを使いましょう。

constを使うことで#defineが担っていた定数の定義を行うことができます。

また、複数回呼ばれる定数でも#defineとは違って実体が一つだけなので生成されるオブジェクトコードのサイズが小さくなるという利点もあります。

同様に関数を#defineを使って定義するとバグを防ぐことができます。

下のコードを見てください。


example.cpp

#define SquareMulti(x, y) x * x * y


一見、問題ないように見えます。

しかし、使う際にこのようにすると予想外の挙動を示します。


example.cpp

SquareMulti(++a, b)


マクロは置き換えるだけなのでxが++aになってしまい求めていた値が得られなくなります。

この問題に対処するためにinlineを代わりに使いましょう。


項3 できるだけconstを使おう

constで修飾して定数を定義することによって2つの利点があります。

一つは間違った使い方を防げることです。


example.cpp

double velocity = 0.0 // これは変数

const double mass = 1.0 // 定数です。変更しちゃだめよ。

力学計算上では質量は変更しない(例外はありますが)という制約をコード上で明示することができます。

用法の制限のためにconstを使用する際に注意しなければならないのは定数ポインタです。


example.cpp

double velocity = 1.0;

const double* pvelocity = &velocity; // 指している先は変更可
// データは変更不可
double* const pvelocity2 = &velocity; // 指している先は変更不可
// データは変更可
const double* const pvelocity3 = &velocit; // 両方共変更不可

定数ポインタの用法を確認しておきましょう。

STLのイテレータも同様に理解しにくいので注意が必用です。


example.cpp

// 指している先が変更不可

// データは変更可能
const std::vector<int>::iterator it = objects.begin();

// 指している先は変更可
// データは変更不可
std::vector<int>::const_iterator c_it= objects.begin();

// 両方変更不可
const std::vector<int>const_iterator IT = objects.begin();


関数の返り値はconstで宣言することは推奨されていません。

しかし、演算子の返り値をconstで宣言することで以下のようなミスを防ぐことができます。


example.cpp

const Object operator+(const Object& lhs, const Object& rhs)

{
/* 省略 */
}

{
Object a, b, c;
if(a+b=c) // a+b==cのつもりで間違えたX(
{
/* 省略 */
}
}


constメンバ関数も同様に理解が難しいです。

constメンバ関数はオブジェクトがconstなときに呼び出される関数です。


example.cpp

const Object a;

Object b;
a.nodge(); // void Object::nodge() constが呼び出される
b.nodge(); // void Object::nodge()が呼び出される。

constメンバ関数内ではクラスメンバを変更することができません。

しかし、例外的にconstメンバ関数の中でクラスメンバを変更したい時があります。

その際はmutable修飾子をつけてメンバを宣言します。


example.cpp

class Object

{
mutable double color;
public:
void paint() const; // colorを内部で変更可能
}

基本的にはconstメンバ関数にオーバロードした非constメンバ関数は別の処理を定義します。

しかし、時にはconstメンバ関数と非constメンバ関数で同じ処理をしたい時があります。

そのような場合は非constメンバ関数内でconstメンバ関数を呼び出します。

しかし、constメンバ関数はオブジェクトがconstの時に呼び出される関数です。

非constメンバ関数内で呼び出すにはthisポインタをキャストする必要があります。


example.cpp

class Object

{
double data;
public:
const double& nudge() const;
double& nudge()
{
return const_cast<double&>(
static_cast<const Object&>(*this).nudge());
}
};


項4 オブジェクトが使用される前に確実に初期化されるようにしよう(Make sure that objects are initialized before they're used)

いわゆるRAII(Resource acquisition is initialization)のことです。

初期化されてない変数を使用すると不定状態になることが多々あります。(例:初期化されていないポインタへの代入など)

可能な限り変数の宣言時に初期化されるようにしましょう。

クラスの初期化といえばコンストラクタですね。

この時、注意しなければならないのは代入と初期化を混同している人が多いということです。


example.cpp

class Object

{
Feature feature;

public:
Object(const Feature& feature)
{
this->feature = feature;
return;
}
};


これは初期化ではなくコンストラクタ内で代入を行っています。

実際の処理の流れを詳しく見せると


  1. プライベートメンバのfeatureをデフォルトコンストラクタで初期化

  2. コンストラクタの引数のfeatureをプライベートメンバのfeatureに代入

1番の処理はいらない処理です。

このムダを避けるために初期化リストを使いましょう。

上のコードを初期化リストを使って改善したコードを下に示します。


example.cpp

class Object

{
Feature feature;
public:
Object(const Feature& feature)
: feature(feature) // Feature::Feature(const Feature& rhs)で初期化
{
return;
}
};

非ローカルな静的オブジェクトの初期化の順番は未定義なので注意が必要です。

例を上げて説明します。2つのソースファイルにそれぞれ非ローカルな変数(グローバル変数や静的クラス変数)が含まれているとします。

ちょうど下のようなコードです。


filesystem.hpp

class FileSystem

{
public:
...
std::size_t numDisks() const;
...
};


a.cpp

extern FileSystem theFileSystem; // どこか他のソース内で定義されている


ここで、filesystem.hppとa.cppを他のソースから呼び出すb.hppとc.cppがあるとします。


b.hpp

//////////////////

// b.hppファイル //
//////////////////
class Directory
{
public:
...
Directory();
...
};


b.cpp

////////////////////////////////////////////////

///////////// In b.cpp file ////////////////////
////////////////////////////////////////////////
Directory::Directory()
{
...
std::size_t disks = theFileSystem.numDisks();
}

この条件下で、下のようなコードでDirectoryクラスのインスタンスを生成するとします。

Directory tempDir;

コード上ではtheFileSystemが先に初期化されているように見えますが、実際は初期化されるまえにtempDirのコンストラクタがtheFileSystemにアクセスしてしまいます。

ソースコードを読んでも見つけられないタチの悪いバグです。

ひとつの解決方法としては非ローカルな静的オブジェクトをローカルな静的オブジェクトに置き換えることです。

ローカルな静的オブジェクトとは関数内で定義される静的オブジェクトを指します。

関数が初めて呼び出される際にローカルな静的オブジェクトは初期化されるというきまりがあるため確実に初期化させることができます。


filesystem.hpp

class FileSystem

{
public:
...
std::size_t numDisks() const;
static FileSystem& theFileSystem();
...
};


filesystem.cpp

FileSystem& theFileSystem() // this replaces the theFileSystem object

{
static FileSystem fileSystem; // define and initialize a local static object
return fileSystem;
}


b.hpp

class Directory

{
public:
...
Directory();
static Directory& tempDir();
...
};


b.cpp

Directory::Directory()

{
...
std::size_t disks = FileSystem::theFileSystem().numDisks();
...
}
Directory& tempDir() // 非ローカルなtempDirオブジェクトの代わりにこの関数を使います。
{
static Directory temp; // ローカル静的オブジェクトとして定義
return temp;
}


項5 C++が自動生成・呼び出しするクラスメンバを知っておこう(Know what functions C++ silently writes and calls)

クラスを定義した時に、次のメンバ関数が無かった場合にコンパイラは自動でデフォルトのメンバ関数を定義します。


  1. コンストラクタ、デストラクタ

  2. コピーコンストラクタ

  3. コピー代入演算子

以下のような空のクラスがあるとします。


example.cpp

class Empty

{
};

このクラスは以下のクラスと等価です。


example.cpp

class Empty

{
public:
Empty(); // デフォルトコンストラクタ
Empty(const Empty&); // デフォルトコピーコンストラクタ
Empty& operator=(const Empty&); // コピー代入演算子
~Empty(); // デストラクタ
};

予想外の使用や挙動を防ぐために確実にこれらの関数を自分の手で書くようにしましょう。


参考文献