Effective C++ 入門
〜 第5章 クラスと関数:その実装 〜


□ 29項 □ 内部データのハンドルを返すのはやめよう
■ 課題

オブジェクトの内部データのハンドルを返すと、クライアントが自由にオブジェクト内のデータハンドルを書き換えすることができるため、クラスで指定した、const 指定や、アクセススコープがまったく意味をなさなくなる。

#include <iostream>
using namespace std;

class String {
public:
__String(const char* value) {
____int len = strlen(value);
____if (len) {
______data = new char[len+1];
______strncpy(data, value, len);
______*(data + len) = '\0';
____} else {
______data = new char[1];
______*data = '\0';
____}
__}
__~String() {
______delete [] data;
__}
__operator char*() const { return data; }
private:
__char* data;
};

int main() {
__String s("Hello World");
__char* S = s;
__strcpy(S, "Hell 2 World"); // privateなメンバを書き換えられる
__cout << s << endl; 
}


■ 解決

オブジェクトの内部データのハンドルを返す場合は、const をつける。

const をつけることで、内部データのハンドルが変更されること防ぐ。

.... (snip) ....
class String {
public:
__.... (snip) ....
__operator const char*() const { return data; }
private:
__char* data;
};

int main() {
__String s("Hello World");
__char* S = s;
__strcpy(S, "Hell 2 World"); // コンパイルエラー!
__cout << s << endl;
}

■ 補足

型変換キャスト演算子も内部データのハンドルを返すことが多いため注意すること。

たとえば以下例のように、キャストした瞬間にオブジェクトのデストラクタが呼ばれ、内部データが無効となる場合がある。

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

class String {
public:
__String(const char* value) {
____int len = strlen(value);
____if (len) {
______data = new char[len+1];
______strncpy(data, value, len);
______*(data + len) = '\0';
____} else {
______data = new char[1];
______*data = '\0';
____}
__}
__~String() {
____cout << __PRETTY_FUNCTION__ << endl;
____int len = strlen(data);
____memset(data, 0, len); // デストラクタでメモリの内容を初期化
____delete [] data;
__}
__operator char*() const { return data; }
private:
__char* data;
};

String func() {
__return String("CRASH?");
}

int main() {
__char* pc = crashFunc();
__// const char* に代入直後に、Stringのデストラクタが呼ばれ、
__// dataの内容は消えてしまう。
__cout << pc << endl;
}
□ 30項 □ メンバ関数は、自分よりもアクセス制限がきついメンバへの非constポインタや参照を返さないようにしよう
■ 課題

29項のように、private のハンドルを返すのも問題だが、private の関数を返すような荒技もある。

以下例。このようなことは普通してはいけないが、普通しないだろう。

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

class Person;

typedef string& (Person::*pointerPrivateFunc)();

class Person {
public:
__Person(const char* value) : name(value) {}
__~Person() {}
__static pointerPrivateFunc privateFunction() {
____return &Person::privateFunc;
__}
private:
__string& privateFunc() { return name; }
__string name;
};

int main() {
__Person p("Hello World");
__pointerPrivateFunc ppf = p.privateFunction();
__string rs = (p.*ppf)();
__cout << rs << endl;
}

■ 解決

メンバを private や protected にするのは、アクセスを制限するため。不特定のクライアントに制限付きのメンバに対する自由なアクセスを与えるような関数を書くのはまったく意味がないこと。

クライアントへ内部データの情報へアクセスする関数は、値渡しか、const にして返そう。(ポインタメンバの場合は、オーバーヘッドの少ない const が望ましい)

■ 補足

29項21項も参照すること
□ 31項 □ 関数の中で new で初期化したポインタの参照先を返してはいけない
■ 課題

関数の中で、new で初期化したポインタのリファレンスを返すと、クライアント側は、delete が必要なオブジェクトなのか不要なオブジェクトなのか判別することができない。

さらに、意識せずともメモリーリークを発生させる可能性がある。以下例で、プログラムをコンパイルし、コンストラクタとデストラクタの数を比較してみよ。

#include <iostream>
using namespace std;

class Data {
public:
__Data(int value) : data (value) {
____cout << __PRETTY_FUNCTION__<<"(" << data << ")" << endl;
__}
__~Data() {
____cout << __PRETTY_FUNCTION__ << "(" << data << ")" << endl;
__}
__Data(const Data& d) {
____cout << __PRETTY_FUNCTION__ << "(" << d.data << ")" << endl;
____data = d.data;
__}
__int get() const { return data; }
private:
__int data;
};

const Data& operator*(const Data& lhs, const Data& rhs) {
__Data *data = new Data(lhs.get()*rhs.get());
__return *data;
}

Data func() {
__Data one(1), two(2), three(3);
__// 以下掛け算で、*tmp12 でリークが発生
__// *tmp12 = new Data(one.get() * two.get());
__// *tmp123 = new Data(tmp12.get() * three.get());
__// return *tmp123;
__return one*two*three;
}

int main() {
__// func関数の戻り値は一時オブジェクトに格納され、コピーコンストラクタ
__// によりresultにコピーされるため、一時オブジェクトはリークとなる
__Data result = func();
}

■ 解決

関数内で new したオブジェクトのリファレンスを返してはいけない。

クライアントは、関数の定義から、delete すべきかどうかも判別は難しい上、定義の仕方によっては、表面化しにくいメモリーリークの要因となる。

■ 補足

23項も参照
□ 32項 □ 変数の定義は、できるだけ後回しにしよう
■ 課題

C言語では、ブロックの先頭で変数を定義すべきというガイドラインは忘れよう。

C++では変数の定義はあとにできるのであれば、そのようにしたほうがよい。

たとえば以下のようなプログラムを考えると、必要もないのに、string クラスのコンストラクタを起動し、無駄な処理を行うことになる。

例)
string encryptPasswd(const char* passwd) {
__string encrypted; // パスワードが短いと、string のコンストラクタの処理は無駄になる
__if (strlen(passwd) < MIN_PASSWD_LENGTH) {
____throw logic_error("passwd too short.");
__}
__.... (snip) ....
__return encrypted;
}


■ 解決

オブジェクトの生成はコストが高くつく。

初期化用の引数が準備できるまで、オブジェクトの生成は後回しにすることで、無駄な処理を最小限に抑えることができる。上記例では、以下のように、string の初期化をできる限り後ろにもっていけば、無駄な処理は最小限に抑えられる。

詳細については、『Effective C++』32項参照のこと。

例)
string encryptPasswd(const char* passwd) {
__if (strlen(passwd) < MIN_PASSWD_LENGTH) {
____throw logic_error("passwd too short.");
__}
__string encrypted(passwd);
__.... (snip) ....
__return encrypted;
}

■ 補足

変数の定義を使うときにだけ、後回しにすることで、変数の説明を無駄にする必要がなく、プログラムの文脈はより明確になる。
□ 33項 □ 関数はよく考えてからインライン化しよう
■ 課題

インライン関数は、関数コールのオーバーヘッドをなしに使うことができ、処理の効率を向上させることができる。

しかし、インライン関数を多用すると、インライン関数はその関数を使用をしているオブジェクトのコードサイズを膨れ上がらせることになる。さらに、膨れ上がったコードによりキャッシュミスを引き起こしやすくなり、逆に効率低下を招くことがある。

■ 解決

インライン関数にするべき関数の指標は、関数コールのオーバヘッドよりも小さいようなコードを選ぶ。

そうすれば、コードサイズにも貢献する可能性が高い。基本的にはC言語でのマクロの代わり程度。関数のインライン化を検討する場合は、まず全ての関数を非インラインにし、プロファイラで計測したのちに必要な関数をインライン化することが賢明。

■ 補足

インラインのパフォーマンスに与える影響については、『Efficient C++』に詳しい。
□ 34項 □ コンパイルするファイル間の依存性はできるだけ減らそう
■ 課題

C++プログラマは、外部に公開しているヘッダーファイルを不用意に編集すると、プロジェクト全体がリビルドがかるということをしばしば経験する。

これは、C++が実装とインターフェースをうまく分離できていない ため。

C++の実装とインターフェースが分離できていないヘッダーファイルの例は以下。

#include "String.h" // m_name のために include
#include "Date.h" // m_date のために include
#include "Address.h"
// m_address のために include
#include "Country.h" // m_country のために include

class Person {
public:
__Person(const String& name, const Date& birthday,
____const Address& addr, const Country& country);
__virtual ~Person();
private:
__String m_name; // 実装の詳細
__Date m_birthday; // 実装の詳細
__Address m_address; // 実装の詳細
__Country m_country; // 実装の詳細
};

■ 解決

pImplイディオム(ハンドルクラス)を使って実装とインターフェースを分離する。(ここではクラスではなく、構造体を使っている)

#ifndef PERSON_HEADER_GUARD__
#define PERSON_HEADER_GUARD__

class String;
class Date;
class Address;
class Country;

class Person {
public:
__Person(const String& name, const Date& birthday,
____const Address& addr, const Country& country);
__virtual ~Person();
__void print() const;
private:
__struct PersonImpl;
__PersonImpl* pImpl;
};

#endif // PERSON_HEADER_GUARD__
#include "Person.h"
#include "String.h"
#include "Date.h"
#include "Address.h"
#include "Country.h"
#include <iostream>

// メンバを構造体に隠蔽する
struct Person::PersonImpl {
__PersonImpl(const String& name, const Date& birthday,
________const Address& addr, const Country& country) :
______m_name(name), m_birthday(birthday),
______m_address(addr), m_country(country) {}
__String m_name;
__Date m_birthday;
__Address m_address;
__Country m_country;
};

Person::Person(const String& name, const Date& birthday,
________const Address& addr, const Country& country) :
______pImpl(new PersonImpl(name, birthday, addr, country) {}

Person::~Person() { delete pImpl; }

void Person::print() const {
__// print 処理 (省略)
}

・ オブジェクトのリファレンスやポインタで間に合うときは、オブジェクトを使わない

・ クラス宣言を使えるときは、クラス定義を使わない

・ ヘッダファイルには、コンパイルに必要なものを除いて、他のヘッダファイルをインクルードしてはいけない

もう一つのアプローチは、純粋仮想クラスを用いること(プロトコルクラス)以下例。

#ifndef PERSON_HEADER_GUARD__
#define PERSON_HEADER_GUARD__

class String;
class Date;
class Address;
class Country;

class Person {
public:
__virtual ~Person();
__virtual void print() const = 0;
__// Factory Method Pattern
__static Person* makePerson(const String& name, const Date& birthday,
______const Address& address, const Country& country);
};

#endif // PERSON_HEADER_GUARD__
#include "Person.h"
#include "RealPerson.h"
#include "String.h"
#include "Date.h"
#include "Address.h"
#include "Country.h"

Person* Person::makePerson(const String& name, const Date& birthday,
______const Address& address, const Country& country) {
__return new RealPerson(name, birthday, address, country);
}
#ifndef REALPERSON_HEADER_GUARD__
#define REALPERSON_HEADER_GUARD__

#include "Person.h"
#include "String.h"
#include "Date.h"
#include "Address.h"
#include "Country.h"

class RealPerson : public Person {
public:
__RealPerson(const String& name, const Date& birthday,
______const Address& address, const Country& country) :
____m_name(name), m_birthday(birthday),
____m_address(address), m_country(country) {}

__virtual ~RealPerson() {}
__virtual void print() const; // 実装は省略

private:
__String m_name;
__Date m_birthday;
__Address m_address;
__Country m_country;
};

#endif // REALPERSON_HEADER_GUARD__

■ 補足

・ ハンドルクラスの場合、メンバ関数は実装ポインタを介してオブジェクトのデータをアクセスする。アクセスごとに1レベルの間接参照が追加される。メモリの量には、ポインタのサイズを加える必要がある。さらに動的なメモリ割り当てに伴うオーバーヘッドが伴う(10項参照

・ プロトコルクラスの場合、すべての関数コールは仮想化されているため関数コールを行うたびに間接ジャンプのコストを払うことになる。メモリ量が増えるかどうかは、仮想関数の量に依存する。

・ ハンドルクラスでもプロトコルクラスでもインライン関数は役にたたない。

これらの詳細については、Effective C++ 34項を参照すること。

Copyright(C) 2005 Yoshinori Oota All rights reserved.

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