Effective C++ 入門
〜 第7章 その他 〜


□ 45項 □ C++がどんな関数を黙って書き、呼び出しているか、知っておこう
■ 課題

代入演算子や、コピーコンストラクタを定義していないのにコンパイルが通ってしまう。

■ 解決

空のクラスでもC++コンパイラが自動的にコンストラクタ、デストラクタ、コピーコンストラクタ、代入演算子等を生成する。

たとえば以下のようなコードもコンパイルが通る。

#include <iostream>
using namespace std;

class Number {
public:
__int data;
};


int main() {
__Number n1;
__n1.data = 10;
__Number n2(n1);
__n2 = n1;
__cout << n1.data << endl;
__cout << n2.data << endl;

__Number *pn2 = &n2;
__const Number *pn1 = &n1;
__cout << pn1->data << endl;
__cout << pn2->data << endl;
}

すなわち、C++コンパイラによって、Number クラスは以下のように展開されている。(コンパイラは、クライアントコード内で使用したものしか生成しない)

class Number {
public:
__Number() {}
__Number(const Number& rhs) : data(rhs.data) {}
__~Number() {}
__Number& operator=(const Number& rhs) {
____data = rhs.data;
____return *this;
__}
__Number* operator&() { return this; }
__const Number* operator&() const { return this; }
__int data;
};

自動生成されるコピーコンストラクタと代入演算子は、そのクラスの 非static のデータメンバのメンバごとのコピー構築、代入を行う

したがって、上記のように正しくメンバ変数(data) はコピーされる。

しかし、ポインタメンバの場合はアドレスがコピーされるだけのため、注意が必要。詳細については、11項を参照すること。

■ 補足

継承が、代入演算子に与える影響については、16項を参照すること。
□ 46項 □ ランタイムエラーよりは、コンパイル時やリンクエラーのほうがいい
■ 課題

バグの多いソフトをリリースしてしまう。

■ 解決

コンパイル時、もしくはリンク時にエラーを検出できるように工夫しよう。

ランタイムチェックがなければ、プログラムはずっと小さく、速くなる。

一つの考え方として、入力に対するチェックをランタイムチェックにまかせるのではなく、あらかじめ制限をしてしまうという手がある。たとえば以下のような列挙形クラスを使うことで未初期化パラメータを渡すことを防ぐことができる。

class Color {
public:
__static const Color Black() { return Color(0,0,0); }
__static const Color White() { return Color(255,255,255); }
__static const Color Red() { return Color(255,0,0); }
__static const Color Green() { return Color(0,255,0); }
__static const Color Blue() { return Color(0,0,255); }
__static const Color Yellow() { return Color(255,255,0); }
__static const Color Cyan() { return Color(0,255,255); }
__static const Color Magenta() { return Color(0,0,255); }
__unsigned char red() const { return red; }
__unsigned char green() const { return green; }
__unsigned char blue() const { return blue; }
private:
__Color(unsigned char r, unsigned char g, unsigned char b) :
____red(r), green(g), blue(b) {}
__Color(const Color&); // コピー禁止
__const unsigned char red;
__const unsigned char green;
__const unsigned char blue;
};

詳細は、Effective C++ 46項参照すること。

■ 補足

実際のところ、ランタイムエラーを予防することは難しい。ランタイムエラーは、非同期のユーザー操作や、システムからの割り込みなど、プログラムの設計上、予測するのが難しい状況において発生することが多いためである。そのようなバグを抽出するためには、ブラックボックステストはとても有効的な手段である。そこで、ランタイムエラーをいかに補足するかがプログラマに課せられた課題となる。そのための手段の一つがASSERTによるランタイムエラーの検出である。ASSERT の有効活用については、「ライティングソリッドコード」が詳しい。

しかし、ASSERTを使用するにあたって注意してほしいことがある。
なんちゃってプログラマが作ったライブラリには、エラー処理が必要なところも、すべてASSERTにしてしまうものがある。これは使う側にとって大きな迷惑である。ライブラリ利用者に対して、絶対にエラーがおきない使い方を強いているだけであり、勘違いもはなはだしい。本来ライブラリ側でエラー処理すべきことをクライアントに押し付けているだけなのだ。

たとえ、プログラミングに関してビギナーといえどもライブラリ設計者は、ASSERTにするべき部分と、エラー処理を行うべき部分をきちんと切り分けて考えなければならない。ASSERTは、プログラムの前提を記述するべきところであり、決してエラー処理の代用ではない。
□ 47項 □ 非ローカルな static オブジェクトは、使用前に必ず初期化されるようにしよう
■ 課題

別々の翻訳単位(ヘッダーファイル、CPPファイルが別々)の非ローカル(グローバル)な static オブジェクトに関連をもたせたら、振る舞いがおかしくなった。以下例。

#ifndef CHAP47A_HEADER_GUARD__
#define CHAP47A_HEADER_GUARD__

class GlobalObjectA {
public:
__int GetImportantInfo() const { return m_ImportantId; }
__static GlobalObjectA theObjectA;
private:
__GlobalObjectA() : m_ImportantId(10) {}
__
int m_ImportantId;
};

#endif // CHAP47A_HEADER_GUARD__
#include "chap47a.h"
GlobalObjectA GlobalObjectA::theObjectA;
#ifndef CHAP47B_HEADER_GUARD__
#define CHAP47B_HEADER_GUARD__

class GlobalObjectB {
public:
__int GetMyId() { return myId; }
__static GlobalObjectB theObjectB;
private:
__GlobalObjectB();
__int myId;
};

#endif // CHAP47B_HEADER_GUARD__
#include "chap47b.h"
#include "chap47a.h"
#include <iostream>
using namespace std;

GlobalObjectB GlobalObjectB::theObjectB;
GlobalObjectB::GlobalObjectB() {
__myId = GlobalObjectA::theObjectA.GetImportantInfo();
}
#include "chap47a.h"
#include "chap47b.h"
#include <iostream>
using namespace std;

int main() {
__cout << "theObjectA : " <<
____GlobalObjectA::theObjectA.GetImportantInfo() << endl;
__cout << "theObjectB : " <<
____GlobalObjectB::theObjectB.GetMyId() << endl; // あれ?
}


■ 解決

別々の翻訳単位(ヘッダーファイル、CPPファイルが別々)の非ローカル(グローバル)な static オブジェクトの初期化順はまったく保障されない。それを防ぐには、シングルトンを使う。以下上記プログラム改善例。

#ifndef CHAP47A_HEADER_GUARD__
#define CHAP47A_HEADER_GUARD__

class GlobalObjectA {
public:
__int GetImportantInfo() const { return m_ImportantId; }
__static GlobalObjectA& theObjectA();
private:
__GlobalObjectA() : m_ImportantId(10) {}
__int m_ImportantId;
};

#endif // CHAP47A_HEADER_GUARD__
#include "chap47a.h"

GlobalObjectA& GlobalObjectA::theObjectA() {
__static GlobalObjectA theObject;
__return theObject;
}
#ifndef CHAP47B_HEADER_GUARD__
#define CHAP47B_HEADER_GUARD__

class GlobalObjectB {
public:
__int GetMyId() { return myId; }
__static GlobalObjectB& theObjectB();
private:
__GlobalObjectB();
__int myId;
};

#endif // CHAP47B_HEADER_GUARD__
#include "chap47b.h"
#include "chap47a.h"
#include <iostream>
using namespace std;

GlobalObjectB::GlobalObjectB() {
__myId = GlobalObjectA::theObjectA().GetImportantInfo();
}

GlobalObjectB& GlobalObjectB::theObjectB() {
__static GlobalObjectB theObject;
__return theObject;
}
#include "chap47a.h"
#include "chap47b.h"
#include <iostream>
using namespace std;

int main() {
__cout << "theObjectA : " <<
____GlobalObjectA::theObjectA().GetImportantInfo() << endl;
__cout << "theObjectB : " <<
____GlobalObjectB::theObjectB().GetMyId() << endl; // OK
}

非ローカルのstatic オブジェクトを static な関数内の staticオブジェクトに移すことによって、明示的に関数呼び出しで、static オブジェクトを初期化を促すことができる。

C++では、関数内の static オブジェクトは、その関数の最初の呼び出し時に初期化されることが定められている。

すなわち2度目以降はいくら呼び出しても初期化せずにすでにあるオブジェクトを返すことになる。(C++特有のシングルトン・イディオム) 

static オブジェクトの呼び出しは決して inline にしてはいけない。

ちょっと考えれば、すぐわかることだが、そのようなことをすると、いくつものstatic オブジェクトが展開され存在することになる。しかし、そのようなことを意図しないにしても、オプティマイザなどにかけると、勝手にコンパイラが inline 展開してしまう恐れがある。

そのようなリスクを払いたくない場合は、素直にシングルトンパターンで示されているイディオムに従うことが無難である。

■ 補足

相互参照するような、static オブジェクトについては、当然だがこのような方法はうまくいかないし、またデザインパターンでも解決は難しい。まず、そのようになってしまう設計をそもそも疑おう。
□ 48項 □ コンパイラの警告に注意しよう
■ 課題

コンパイラの出すwarning なんて実際の動作に関係ないさ。動けばいいんだよ。と思っているあなたに。

■ 解決

コンパイラの出す警告の意味をよく理解することが大事。たとえば、次の例を考える。

#include <cassert>
#include <iostream>
using namespace std;

class Base {
public:
__Base() {}
__virtual void f() { cout << "Base" << endl; }
};

class DerrivedA : public Base {
public:
__DerrivedA() : d(10.0) {}
__virtual void f() { cout << "DerrivedA::f" << d << endl; }
private:
__double d;
};

class DerrivedB : public Base {
public:
__DerrivedB() : l(10) {}
__virtual void f() { cout << "DerrivedB::f" << l << endl; }
private:
__unsigned long l;
};

void function(DerrivedB* db) {
__assert(db != 0);
__db->f();
}

int main() {
__Base* b = new DerrivedA;
__function((DerrivedB*)b);   // げげ、まじかよ!!
__delete b;
}

こんなコードでも、何のオプションもつけなければ、黙ってコンパイルは通ってしまう。そこで、GNUのコンパイラに次のようなオプションを設定すれば、このような悲惨なことは防げるかも知れない。

例)
g++ -Wold-style-cast chap48.cpp


もっとも、ここで static_cast<DerrivedB*> なんてやられれば同じだが、プロジェクト全体で、dynamic_cast の使用を徹底すれば、このような不幸ことは、事前に防げよう。

■ 補足

GNUコンパイラでは、Effective C++ 第48項のような警告メッセージを出すには、次のオプションを設定すればよい。

例)
g++ -Woverloaded-virtual xx.cpp

参考までに、GNUのC++の警告オプションを以下に挙げる。

-Wreturn-type Warn about inconsistent return types
-Woverloaded-virtual Warn about overloaded virtual function names
-Wno-ctor-dtor-privacy Don't warn when all ctors/dtors are private
-Wnon-virtual-dtor Warn about non virtual destructors
-Wextern-inline Warn when a function is declared extern, then inline
-Wreorder Warn when the compiler reorders code
-Wsynth Warn when synthesis behavior differs from Cfront
-Wno-pmf-conversions Don't warn when type converting pointers to member functions
-Weffc++ Warn about violations of Effective C++ style rules
-Wsign-promo Warn when overload promotes from unsigned to signed
-Wold-style-cast Warn if a C style cast is used in a program
-Wno-non-template-frien Don't warn when non-templatized friend functions are declared within a template
-Wno-deprecated Don't announce deprecation of compiler features
□ 49項 □ 標準ライブラリに精通しよう
■ 課題

C++で書くと、ごちゃごちゃしたプログラムになってしまう。

■ 解決

標準ライブラリに精通すれば、豊富なコンテナクラス、アルゴリズムを利用できる。標準ライブラリを駆使すれば、実装するべき部分はほとんど問題領域に限定でき、余計なヘルパープログラムの記述を行う必要がなくなる。その結果、短納期で可読性の高いプログラムを記述することができる。(って、プログラマのセンスによるものが大きいですが・・)

しかし、標準ライブラリは巨大なテンプレートの集まりであることに注意しよう。

たとえば、string クラスの参照のためにクラス宣言しようと、class string と記述してもコンパイルエラーとなることがある。実際のところ、string クラスはクラスではなく 次のような template の typedef である。

teplate <class charT> struct char_traits;
template <class T> class allocator;
template <class charT, class traits = char_traits<charT>
____class Allocator = allocator<charT>> class basic_string;
typedef basic_string<char> string;

このような時、<iosfwd> というヘッダファイルで上記の定義を行っているので、それをインクルードすれば、class string と同じ効果を得ることができる。以下例。

#ifndef CHAP49_HEADER_GUARD__
#define CHAP49_HEADER_GUARD__
#include <iosfwd>

class Person {
public:
__Person(const char* name);
__~Person();
__void print() const;
private:
__std::string* pName;
};

#endif // CHAP49_HEADER_GUARD__
#include "chap49.h"
#include <string>
#include <iostream>
using namespace std;

Person::Person(const char* name) : pName(new string(name)) {}
Person::~Person() { delete pName; }
void Person::print() const {
__cout << *pName << endl;
}

int main() {
__Person p("yohta");
__p.print();
}

詳細については、EffectiveC++ の第49項を参照すること。標準ライブラリの概要が記述されており、参考になると思います。

■ 補足

C++の標準ライブラリについては、「プログラミング言語 C++ 第3版」に詳細が記述されている。C++を使用するにあたっては、必ず傍らにおいておきたい一冊です。
□ 50項 □ C++をもっと理解するために
■ 課題

C++なんざ、Cに比べれば効率悪いよ!と思っているあなたに。

■ 解決

C++は、Cと同じくらいの性能、遅くなったとしてもせいぜい5%程度。

(私は、C++のプログラム記述の効率性、そしてSTLの効率のよさを考えると、C言語よりもその優位性ははるかに高いと思っています)

しかし、現実の問題に対応できることを優先したため、その仕様は必ずしも、とっかかりのよいものではない。正しく使うためには、C++をよく理解する必要がある。

ところで、Effective C++ で紹介している参考書籍「The Design and Evolution of C++」「注解 C++リファレンスマニュアル」は、参考にはなるとは思うがすでに時代遅れで実業務に役立つほど実用的ではない。

現在の C++プログラミングの必携図書は以下。

・ プログラミング言語 C++ 第3版

・ Effective C++ (言うまでもないが・・)

この2冊の内容を頭にたたき込めば(とは言うもののすごい量ですが)、実用的なプログラムを記述するには、ほとんど不自由ない。

さらに、C++をよりスマートに実装したい という人は、以下の図書が役にたつ。

・ More Effective C++

・ Exceptional C++

さらに、テンプレートについて極めたい という人がいたら、以下の図書に目を通すと面白い。オブジェクト指向プログラミングとはまた違った世界を味わうことができる。

・ Modern C++ Design

C++をもっと効率的に書きたい と思う人は、以下の図書が役に立つ。特に組み込み系の人にはお勧めです。

・ Efficient C++

最後に、C++の設計者である Stroustrup さんのホームページを紹介して終わります。C++に関するFAQやリンクが充実しており、非常に参考になると思います。

・ Bjarne Stroustrup's homepage

Copyright(C) 2005 Yoshinori Oota All rights reserved.

本ホームページのリンクは自由です。複製、転載される場合は、必ず著者までご連絡をください。