Effective C++ 入門
〜 第4章 クラスと関数:その設計と宣言 〜


□ 18項 □ クラスインターフェースが完全かつ最小限になるまでがんばろう
■ 課題

クラスインターフェースとは、そのクラスを使うプログラマがアクセスできるインターフェースのこと。インターフェースは、特に理由がない限り関数となる。大きなインターフェースは以下のような問題を抱える。

・ 関数の数が多ければ多いほど、クライアントにとっては理解し難いものになる。

・ 保守が難しくなる。重複コードの保守、インターフェースの一貫性の維持、ドキュメント化が大変になる。

・ 大きなインターフェースは、ヘッダファイルが長くなり、コンパイル時間に大きな影響を与える。

■ 解決

クラスインターフェースはできる完全かつ最小限になるまで、よく設計を吟味する。

■ 補足

私見だが、この当たり前のようなことをできるプログラマは残念ながら、かなり少ない。
□ 19項 □ メンバ関数、非メンバ関数、friend関数を使い分けよう
■ 課題

クラス内のメンバ関数に、operator * や、operator >>、 operator << を定義することは大きな困難が伴う。

以下例。

#include <iostream>
using namespace std;

class Rational {
public:
__Rational(int n = 0, int d = 1) : numerator(n), denominator(d) { }
__const Rational operator * (const Rational& rhs) const;
__ostream& operator << (ostream& output);
private:
__int numerator;
__int denominator;
};

const Rational Rational::operator * (const Rational& rhs) const {
__return Rational(lhs.numerator * rhs.numerator,
__________________lhs.denominator * rhs.denominator);
}

ostream& Rational::operator << (ostream& output) {
__return output << numerator << "/" << denominator;
}


int main() {
__Rational oneHalf(1, 2);
__Rational tmp;
__tmp = oneHalf * 2; // OK!コンパイラが暗黙的に2をRationalに変換
__tmp = 2 * oneHalf; // コンパイルエラー!
__cout << tmp << endl; // コンパイルエラー!
__tmp << cout; // こんなおかしな構文だとOK
}

■ 解決

・ もっとも左にある引数について型変換ができるのは、非メンバ関数のみ (上記例:tmp = 2 * oneHalf)

・ operator >> 、operator << は、メンバにできない。

#include <iostream>
using namespace std;

class Rational {
public:
__Rational(int n = 0, int d = 1) : numerator(n), denominator(d) { }
private:
__friend const Rational operator * (const Rational& lhs, const Rational& rhs);
__friend ostream& operator << (ostream& output, const Rational& v);
__int numerator;
__int denominator;
};

const Rational operator * (const Rational& lhs, const Rational& rhs) {
__return Rational(lhs.numerator * rhs.numerator,
__________________lhs.denominator * rhs.denominator);
}


ostream& operator << (ostream& output, const Rational& v) {
__return output << v.numerator << "/" << v.denominator;
}


int main() {
__Rational oneHalf(1, 2);
__Rational tmp;
__tmp = oneHalf * 2; // OK!
__tmp = 2 * oneHalf; // OK!
__cout << tmp << endl; // OK!
}

■ 補足

・ 仮想関数はクラスのメンバ関数でなければならない
□ 20項 □ データメンバを public インターフェースに入れるのはやめよう
■ 課題

データメンバをインターフェースにすると、クラスのクライアントに()が必要ないのに、()をつけてしまうというつまらない問題に悩むことになったり、また、クラス設計者は、うかつにデータメンバーの名称や、処理方法をかえたりすることができない。また、うっかりクライアントがデータメンバの値を書き換えてしまい、思わぬ不具合が発生し得る。

■ 解決

データメンバは隠蔽し、データメンバへのアクセスには、アクセサ・メソッド(getValue(), setValue(class T))を用意し、リード/ライトを制御できるようにする こと。

■ 補足

アクセサメソッドはインライン関数として定義することで効率化することができる。
□ 21項 □ 使えるときは、必ず const を使おう
■ 課題

・ 変更してはいけない値を変更しようとしていることをコンパイル時に検出したい。

・ 変更してはいけない値のときの処理と、変更可能な値のときの処理を分けたい。

■ 解決

const を使う

・ 関数に const が修飾されていた場合は、const オブジェクトは const 修飾された関数のみが使える

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

class Sample {
public:
__Sample(const char* a) : value(a) {}
__void unconst_func() { cout << value << endl; }
__void const_func() const { cout << value << endl; }
private:
__string value;
};

int main() {
__const Sample a("a");
__a.unconst_func(); // エラー
__a.const_func(); // OK
}

・ const はポインタならびに、ポインタが指すデータに適用することもできる。

以下例。({}内がconstの対象)

例1)
const
{ char * p } = "Hello"; // データ (*p) が const
char * const
{ p } = "Hello"; // ポインタ (p) が const
const
{ char * const { p } }= "Hello"]; // データ (*p) もポインタ (p) も const

・ 戻り値にconstを使うと、問題のある関数の安全性や効率を改善できる

例2)
const Rational operator* (const Ratioanl& lhs, const Rationa& rhs);

(a * b) = c のようなコードは不正だが、const 指定すればコンパイル時エラーとなる。
(a * b) = c; これを展開すると、const Rational (a * b) = Rational c となりエラー。

例3)
operator [] は、const 版と非const版と、二つ定義する。

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

class String {
public:
__String(const char* d);
__~String() { delete [] data; }
__char& operator[](int i) { return *(data + i); }
__const char& operator[] (int i) const { return *(data + i); }
__const char* const ptr() const { return data; }
private:
__char* data;
};
String::String(const char* d) {
__int len = strlen(d);
__if (len) {
____data = new char[len+1];
____strncpy(data, d, len);
____*(data + len) = '\0';
__} else {
____data = new char[1];
____
data = '\0';
__}
}

int main() {
__String unconst_str("Hello");
__const String const_str("World");

__cout << unconst_str[0] << endl; // const版呼び出し
__cout << const_str[0] << endl; // const版呼び出し

__unconst_str[0] = 'h'; // 非const版呼び出し
__cout << unconst_str.ptr() << endl;

__const_str[0] = 'w'; // const版呼び出し コンパイルエラー!
__cout << const_str.ptr() << endl;
}

・ 非staticデータメンバに mutable 修飾子をつけると、constメンバ関数内でも変更することができる。

■ 補足

・ 組み込み型を返す関数の戻り値を書き換えるのは常に不正
・ const には、ビット的定数性、概念的定数性という考え方がある詳細については、『Effective C++』参照。
□ 22項 □ 値渡しよりも、リファレンス渡しを使おう
■ 課題

C言語では、関数への引数はすべて値渡し(コピーを渡す)という仕様となっている。C++でもその仕様はデフォルトの規約としている。

しかし、オブジェクトの関数への値渡しは、その都度、コピーコンストラクタ・デストラクタが起動し効率が悪い。

以下の例では、コンストラクタが18回、デストラクタが18回呼ばれる。

#include <iostream>
#include <string>

// 以下のStringクラスは11項でとりあげたコード
class String {
public:
__String();
__String(const char* value);
__String(const String& s);
__String& operator=(const String& s);
__~String();
__const char* const ptr() const { return data; }
private:
__char* data;
};
String::String() : data(new char[1]) {
__*data = '\0';
__std::cout << __PRETTY_FUNCTION__ << std::endl;
}
String::String(const char* value) {
__std::cout << __PRETTY_FUNCTION__ << std::endl;
__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::~String() {
__std::cout << __PRETTY_FUNCTION__ << std::endl;
__delete [] data;
}
String::String(const String& s) : data('\0') {
__std::cout << __PRETTY_FUNCTION__ << std::endl;
__int len = strlen(s.data);
__if (len) {
____data = new char[len+1];
____strncpy(data, s.data, len);
____*(data + len) = '\0';
__} else {
____data = new char[1];
____*data = '\0';
__}
}
String& String::operator=(const String& s) {
__// std::cout << __PRETTY_FUNCTION__ << std::endl;
__if (this == &s) return *this;
__delete [] data;
__int len = strlen(s.data);
__if (len) {
____data = new char[len+1];
____strncpy(data, s.data, len);
____*(data + len) = '\0';
__} else {
____data = new char[1];
____*data = '\0';
__}
__return *this;
}

class Person {
public:
__Person() : name("Someone"), address("Somewhere") {
____std::cout << __PRETTY_FUNCTION__ << std::endl;
__}
__Person(const Person& other) {
____std::cout << __PRETTY_FUNCTION__ << std::endl;
____name = other.name;
____address = other.address;
__}
__virtual ~Person() {
____std::cout << __PRETTY_FUNCTION__ << std::endl;
__}
private:
__String name, address;
};

class Student : public Person {
public:
__Student() : schoolName("kohoku"), schoolAddress("mamedo") {
____std::cout << __PRETTY_FUNCTION__ << std::endl;
__}
__Student(const Student& other) : Person(other) {
____std::cout << __PRETTY_FUNCTION__ << std::endl;
____schoolName = other.schoolName;
____schoolAddress = other.schoolAddress;
__}
__virtual ~Student() {
____std::cout << __PRETTY_FUNCTION__ << std::endl;
__}
private:
__String schoolName, schoolAddress;
};

// 以下クライアントコード
Student function(Student s) {
__return s;
}

int main() {
__Student yohta;
__function(yohta);
}

■ 解決

関数へオブジェクトを渡すときは、リファレンスを使う。

以下のように関数定義をかえるだけで、コンストラクタは、必要最低限の6回に減らせる。(Studentコンストラクタ1回、Stringコンストラクタ2回、Personコンストラクタ1回、Stringコンストラクタ2回)

... (snip) ....
Student
& function(Student& s) {
__return s;
}

int main() {
__Student yohta;
__function(yohta);
}

■ 補足

値渡しは、効率の問題のほかに、オブジェクトのスライシングという問題が発生する。

たとえば以下のコードで、関数 function への値渡しで、Derrived の情報はすべて失われてしまう。これをスライシングという。function 内では Derrived の仮想関数 print がコールされるべきだが、Base の print がコールされてしまう。

スライシング問題の詳細については、『Effective C++』 を参照のこと。

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

class Base {
public:
__Base() : bname("Base") {}
__virtual ~Base() {}
__virtual void print() const { cout << bname << endl; }
private:
__string bname;
};

class Derrived : public Base {
public:
__Derrived() : dname("Derrived") {}
__virtual ~Derrived() {}
__virtual void print() const { cout << dname << endl; }
private:
__string dname;
};

void Base::print() const
void Derrived::print() const

// 以下クライアントコード
void function(Base b) { b.print(); }

int main() {
__Derrived d;
__function(d);
}

この問題も、リファレンス渡しにすれば、正しく Derrived の仮想関数 print が呼び出される。

.... (snip) ...

void function(Base
& b) {__b.print(); }

int main() {
__Derrived d;
__function(d);
}
□ 23項 □ オブジェクトを返さなければならないときに、リファレンスを返そうとがんばるのはやめよう
■ 課題

ローカル関数で定義した値をリファレンスで返すと、落ちてしまう。(当たり前だが・・・)

以下例。(この場合は、コンパイラがきちんと知らせてくれる)

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

const string& function(const string& first_name, const string& second_name) {
__string tmp = first_name + second_name;
__return tmp;
}

int main() {
__string fn = "Yoshinori";
__string sn = "Oota";
__string name = function(fn, sn);
__cout << name << endl;
}

■ 解決

関数内で定義したローカルオブジェクトは、関数内でしか有効でない。

関数内で定義したローカルオブジェクトは、関数のスコープを外れたとたんに破壊され有効でなくなる。

関数の返り値をを値渡しにすれば、戻り値となるオブジェクト(name)にコピーコンストラクタによってコピーされる。

... (snip) ....
string function(const string& first_name, const string& second_name) {
__string tmp = first_name + second_name;
__return tmp;
}

int main() {
__string fn = "Yoshinori";
__string sn = "Oota";
__string name = function(fn, sn); // name に対してコピーコンストラクタが働く
__cout << name << endl;
}

■ 補足

ローカル関数内で、new で生成したオブジェクトをリファレンスで返してはいけない。素直にポインタで返すこと。詳細は、31項参照
□ 24項 □ 関数のオーバーロードとデフォルト付きパラメータは、慎重に使い分けよう
■ 課題

関数のオーバーロードを使うことと、関数のパラメータにデフォルト値をつける、二つとも同じようなことができるけど、どのように使い分ければよいの?

例)
void f();
void f(int x); // 関数のオーバーロード

__f() // f() を呼び出し
__f(10) // f(int x) を呼び出し

void g(int x=0) // 関数にデフォルトパラーメータを設定

__g() // g(0)を呼び出し
__g(10)

■ 解決

・ 適切なデフォルト値を選ぶことができて、同じアルゴリズムを使う場合は、関数のパラメータにデフォルト値をつける

・ 与えられた入力(パラーメータの数)に対してアルゴリズムを使い分けなければならない場合は、関数オーバーロードを使う

そのほかの詳細については、『Effective C++』の24項を参照すること。

■ 補足

オーバーロードしている複数の関数で、共通の仕事を行う一つの関数をそれぞれ呼び出すという手法は頻繁に使われるテクニック
□ 25項 □ ポインタと数値型とにオーバーロードするのは避けよう
■ 課題

ポインタを引数とする関数 f(strint* s) と、数値型を引数とする関数 f(int) をオーバーロードをすると、0 を引数としたときに予測した動きにならないときがある。

例)
void f (string* a);
void f (int);

// クライアントコード
#define NULL 0
void f(NULL);

クライアントが期待するのは、f(string* a) だが、呼ばれるのは、f(0) となる。

■ 解決

ポインタを引数とする関数と、数値型を引数とする関数をオーバーロードしてはいけない。

■ 補足

C言語では、#define NULL ((void*)0) が用いられるが、上記例では f(static_cast<string*>NULL) としないと用いることはできない。すなわち、C++においては、NULL は 0 として扱われる。詳細については、『プログラミング言語 C++ 第3版』 5.1.1 ゼロ の項を参照。
□ 26項 □ 潜在的な多義性に対するガードを固めよう
■ 課題

設計者が意図しない多義性が発生する可能性がある。

代表的な多義性は、引数の数値型があいまいさ。

以下例。

例)
void f (int);
void f (char);

double d = 6.20;
f (d); // エラー


上記例は、わかりやすいのでそれほど問題にならない。

もっとも陥りやすい多義性の罠は、多重継承。

以下例。

#include <iostream>
using namespace std;

class Base1 {
public:
__void doIt() { std::cout << "Base1::doIt() " << std::endl; }
};

class Base2 {
public:
__Base2() {}
private:
__void doIt() { std::cout << "Base2::doIt() " << std::endl; }
};

class Derrived : public Base1, Base2 {
};

int main() {
__Derrived d;
__std::cout << d.doIt() << std::endl;
__// Base2 の doIt が private であるにもかかわらずコンパイルエラー
__// なぜそうなるかの詳細については、『Effective C++』 26項参照
}

■ 解決

潜在的な多義性についてガードするのは、ほとんど不可能。

予防についてよく知るには、『Effective C++』 26項をよく読みましょう。


■ 補足

参考までに以下のようなプログラムを作ってみましたが、どうもコンパイルできてしまいます。どこが悪いのかな?

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

class B;

class A {
public:
__A (const B&) : name("A") {}
__const string& get() const { return name; }
private:
__string name;
};

class B {
public:
__B() : name("B") { a = new A(*this); }
__~B() { delete a; }
__operator A() const { return *a; }
__const string& get() const { return name; }
private:
__A* a;
__string name;
};

void f(const A& a) {
__cout << a.get() << endl;
}

int main() {
__B b;
__f(b);
}
□ 27項 □ 暗黙のうちに生成される不要なメンバ関数は、明示的に使用を禁止しよう
■ 課題

代入演算子(operator = )は、C++で特別扱いされており、定義しない場合は、C++のコンパイラが自動的に生成してしまう。

たとえば独自にArrayクラスを定義した場合、代入演算子を定義しないと問題となる。以下例。

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

template <class T>
class MyArray {
public:
__MyArray(unsigned int s) : size(s), data(new T[s]) {}
__~MyArray() { delete [] data; }
__T& operator [] (unsigned int index) {
____assert (index < size);
____return data[index];
__}
__unsigned int length() const { return size; }
private:
__unsigned int size;
__T* data;
};

int main() {
__MyArray<int> value1(4);
__MyArray<int> value2(4);
__for (unsigned int i = 0; i < 4; ++i) {
____value1[i] = i;
____value2[i] = i*10;
__}

__value1 = value2; // 代入!

__for (unsigned int i = 0; i < 4; ++i) {
____cout << value1[i] << endl;
____cout << value2[i] << endl;
__}
__// 一見代入はうまくいっているように見えるが、
__// デフォルトの代入演算子ではポインタをコピーしているだけのため、
__// value1 と value2 が同じデータをさすことになり、
__// value1 がもっていた data はメモリリークする
}

■ 解決

本来 C++の仕様では、配列を代入するのは不正である。したがって、上記例での、value1 = value2 のような構文は禁止すべき。

代入演算子を private に定義するだけ(実装はかかない)で、禁止することができる。

.... (snip) .....
template <class T>
class MyArray {
public:
__MyArray(unsigned int s) : size(s), data(new T[s]) {}
__~MyArray() { delete [] data; }
__T& operator [] (unsigned int index) {
____assert (index < size);
____return data[index];
__}
__unsigned int length() const { return size; }
private:
__MyArray& operator= (const MyArray&); // 実装はしない!
__MyArray(const MyArray&); // ついでにコピーコンストラクタも禁止
__unsigned int size;
__T* data;
};

■ 補足

11項も参照
□ 28項 □ グローバルな名前空間は分割しよう
■ 課題

大規模なソフトウェアプロジェクトでは、同じ定義が重なる場合がある。たとえば、const double LIB_VERSION = 1.204 など、一般的な名前では重なる可能性がたかく、問題となる。

■ 解決

名前空間を使う。以下例。

例)
namespace sdm {
__const double LIB_VERSION = 1.204;
}

クライアントは使う場合は以下のように、using namespace 宣言する。

void function() {
__double version = sdm::LIB_VERSION;
}

void function() {
__using namespace std;
__double version = LIB_VERSION;
}

■ 補足

名前空間による分割は、潜在的な多義性に対するガードの一助にもなる。

Copyright(C) 2005 Yoshinori Oota All rights reserved.

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