Effective C++ 読書会のメモです。原書を元に独自の付け足しも行っているため誤りが含まれている可能性があります。
Item2: Prefer consts, enums, and inlines to #defines.
「プリプロセッサよりコンパイラを使う方が良い」と言い換えても良い
理由: #defineは本来的に言語の一部として扱われないため
たとえば、
#define ASPECT_RATIO 1.653
のASPECT_RATIOというシンボルはコンパイラには見えない。(事前にプリプロセッサが数値を埋め込んでしまうため。)
従って、シンボルテーブルに入らない。
この場合、この定数に関連するコンパイルエラーが発生した場合はASPECT_RATIOは表示されず、1.653という数値のみが表示される。
シンボリックデバッガを使用するときも同様に"ASPECT_RATIO"は表示されない。
もしこの定数が外部のヘッダファイルに定義されている場合はこの定数がどこに由来するものなのか探すのが大変になる。
解決法
const double AspectRatio = 1.653;
全て大文字の表記はマクロ(#define)由来のものを通常指すので、constは一部のみ大文字にしている。
浮動小数点定数を使う場合は、コード量が小さくなるという利点もある。
(#defineを使った場合は数値がすべての箇所に埋め込まれてしまうため。)
ポインタのconstには2種類あることに注意
const char * authorName = "Scott Meyers"; // ポインタの指す先の値が定数
char const * authorName = "Scott Meyers"; // 同上
char * const authorName = "Scott Meyers"; // ポインタ自身(アドレス)が定数
const char * const authorName = "Scott Meyers"; // 両方定数
char const * const authorName = "Scott Meyers"; // 同上
しかし文字列の場合はchar*よりもstd::stringを使うほうが良い
const std::string authorName("Scott Meyers");
クラス内定数
class GamePlayer {
private:
static const int NumTurns = 5; // constant declaration
int scores[NumTurns]; // use of constant
...
};
ソース内で実体を常に1つだけにする(オブジェクトごとに生成されないようにする)ためにはstaticをつける
上記はdeclaration(宣言)であることに注意。
定義はクラスの外に書く。
const int GamePlayer::NumTurns;
ただし、組み込み型の定数で、かつ定数のアドレスを使用しない場合は省略できる
これの詳細を調べるため実験。
ヘッダファイル"test.h"
#include <iostream>
class GamePlayer {
public:
static const int NumTurns = 5; // constant declaration
private:
int scores[NumTurns]; // use of constant
public:
GamePlayer(int n = 0) {std::fill_n(scores, NumTurns, n);}
void print() const {
std::cout << "NumTurns = " << NumTurns << std::endl;
}
void printAddress() const {
std::cout << "Address of NumTurns = " << &NumTurns << std::endl;
}
void printScores() const {
for(int i=0; i<NumTurns; ++i) std::cout << scores[i] << " ";
std::cout << std::endl;
}
};
// printAddress()を呼び出すにはこれが必要。そうでなければ不要
const int GamePlayer::NumTurns;
class Game {
public:
static const GamePlayer player;
public:
void print() const {player.print();}
void printScores() const {player.printScores();}
};
// クラス内定数がユーザー定義クラスオブジェクトの場合は
// 定義時にコンストラクタ引数を与える
const GamePlayer Game::player(7);
ソースファイル
#include "test.h"
int main(int argc, char** argv) {
GamePlayer player;
player.print();
player.printAddress();
Game game;
Game::player.printScores();
}
実行結果
NumTurns = 5
Address of NumTurns = 0x8048918
7 7 7 7 7
当然だが、#defineにはスコープが無いのでクラス内定数は実現できない。
つまり、定数のカプセル化ができない。
*古いコンパイラの場合
クラス内で
static const int NumTurns = 5;
として初期値を与えるとコンパイルエラーになる。
その場合は以下のように記述する。
class CostEstimate {
private:
static const double FudgeFactor; // declaration of static class
...
};
const double CostEstimate::FudgeFactor = 1.35; // definition of static class
しかし、配列で定数を使用する場合は下記のように書くとエラーになる。
class GamePlayer {
private:
static const int NumTurns; // constant declaration
int scores[NumTurns]; // use of constant
...
};
const int GamePlayer::NumTunrs = 5;
このような場合はenumハックを使用する。
class GamePlayer {
private:
enum { NumTurns = 5 }; // “the enum hack” — makes
// NumTurns a symbolic name for 5
int scores[NumTurns]; // fine
...
};
enumハックには以下の理由で知っておく価値がある。
- constはアドレス(ポインタと参照)を取れるがenumは取れない
- enumはメモリを確保しないことが保証されている
(const定数もアドレスを使用しなければメモリは確保されないが、しょぼいコンパイラの場合はメモリを確保することがあり得る)
ちなみにenumハックはテンプレートメタプログラミングの基本テクでもある。
#defineの悪い点に戻る。
下記マクロは難点が多く、有害である。
// call f with the maximum of a and b
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
マクロは引数にカッコをつけなければ式を代入した場合に演算順序が狂って正常に動かない上、
下記コードは意図に反する動作をする。
int a = 5, b = 0;
CALL_WITH_MAX(++a, b); // a is incremented twice
CALL_WITH_MAX(++a, b+10); // a is incremented once
このようなナンセンスなマクロは、下記インライン関数を用意すれば使わずに済む。
template<typename T> // because we don’t
inline void callWithMax(const T& a, const T& b) // know what T is, we
{
f(a > b ? a : b); // pass by reference-to-const - see Item 20
}
インライン関数は関数であり、スコープやアクセスルールに従うのでカプセル化が可能。
インライン関数を使えばもはや(あの忌まわしき)マクロに関わることなんてなくなる。
以上のように、const, enum, inlineを使えばプリプロセッサの使用を減らせる。
しかし、なくなるわけではない。
#includeは必要だし、#ifdef/#ifndefはコンパイルを制御する重要な役割を持ち続けるだろう。
Thints to Remember
- 単純な定数には#defineよりもconstオブジェクトやenumを使うのが良い
- 関数ライクなマクロについては、#defineよりもインライン関数を使うのが良い