オブジェクトの広場は株式会社オージス総研グループのエンジニアによる技術発表サイトです

プログラミング

C++ クラス設計に関するノート

株式会社 オージス総研
オブジェクトテクノロジー ソリューション部
伊藤 喜一
2002年10月24日

C++が他のオブジェクト指向言語と比べて難しいのは、やはりメモリ管理をプログラマが自分でしなければいけない点だと思います。よくよく注意しないと、削除し忘れたり、同じオブジェクトを2度削除してしまうというエラーが発生します。このノートでは、オブジェクトを「値オブジェクト」と「参照オブジェクト」というカテゴリに分け、詳細設計の段階で注意すべき点を整理しておきたいと思います。

0. はじめに

私自身今までいくつかのプログラミング言語を使ってきましたが、C++ が他のオブジェクト指向言語と比べて難しいのは、やはりメモリ管理をプログラマが自分でしなければいけない点だと思います。例えば、

Person* person = new Person();

と生成したオブジェクトは、使い終わったら次のように削除しなければなりません。

delete person;

生成してすぐ削除するなら簡単なのですが、実際にはよくよく注意しないと、削除し忘れたり、同じオブジェクトを2度削除してしまうというエラーが発生します。このノートでは、オブジェクトを「値オブジェクト」と「参照オブジェクト」というカテゴリに分け、詳細設計の段階で注意すべき点を整理しておきたいと思います。

対象読者としては、ある程度オブジェクトのメモリイメージが理解できる人を想定しています。また、参考文献にあげた「Effective C++」で書かれている内容は、詳しく説明しませんので、まだ読まれていない方は併せてご参照いただければと思います。

1. 値オブジェクト

1.1. 値オブジェクトとは

まず言葉の定義ですが、次のような特徴をもったオブジェクトを「値オブジェクト」といいます。

  • インスタンス自体よりも、保持するデータ値が重要な意味をもつ。
  • インスタンスが異なっても、同じ値を保持していれば同じオブジェクトと見なす。
  • 必要に応じて自由にコピーを作ることができる。
  • intdouble などの基本データ型と同じセマンティックスで使用される。

日付や時間、お金、数量などは一般に「値オブジェクト」です。標準ライブラリでは、stringvectormap などの STL コンテナがこのカテゴリに入ります。

1.2. 値オブジェクトの設計

「名前(Name)」クラスを例に考えましょう。ソースコードは以下のようになります。

Name.h

#ifndef Name_h
#define Name_h

#include <iostream>

class Name
{
  public:
    // デフォルトコンストラクタ.
    Name();

    // 引数を1つとるコンストラクタ.
    explicit Name(const char* value);

    // コピーコンストラクタ.
    Name(const Name& other);

    // デストラクタ.
    ~Name();

    // 代入演算子.
    Name& operator=(const Name& other);

    // アクセッサ.
    const char* getValue() const;
    void setValue(const char* value);

    // 等価演算子.
    bool operator==(const Name& other) const;
    bool operator!=(const Name& other) const;

  private:
    char* m_value;
};

// 出力演算子.
std::ostream& operator<<(std::ostream& lhs, const Name& rhs);

#endif /* !Name_h */

Name.cpp

#include "Name.h"
#include <cstring>

using namespace std;

// デフォルトコンストラクタ.
Name::Name()
    : m_value(0)
{
}

// 引数を1つとるコンストラクタ.
Name::Name(const char* value)
    : m_value(0)
{
    if (value != 0) {
        m_value = new char[strlen(value) + 1];
        strcpy(m_value, value);
    }
}

// コピーコンストラクタ.
Name::Name(const Name& other)
    : m_value(0)
{
    if (other.m_value != 0) {
        m_value = new char[strlen(other.m_value) + 1];
        strcpy(m_value, other.m_value);
    }
}

// デストラクタ.
Name::~Name()
{
    delete[] m_value;
}

// 代入演算子.
Name& Name::operator=(const Name& other)
{
    if (this != &other) {
        setValue(other.m_value);
    }
    return *this;
}

// アクセッサ.
const char* Name::getValue() const
{
    return m_value;
}

void Name::setValue(const char* value)
{
    if (m_value == value) return;

    delete[] m_value;

    if (value == 0) {
        m_value = 0;
    } else {
        m_value = new char[strlen(value) + 1];
        strcpy(m_value, value);
    }
}

// 等価演算子.
bool Name::operator==(const Name& other) const
{
    if (m_value == other.m_value) {
        return true;
    }
    if (m_value == 0 || other.m_value == 0) {
        return false;
    }
    return strcmp(m_value, other.m_value) == 0;
}

bool Name::operator!=(const Name& other) const
{
    return !(*this == other);
}

// 出力演算子.
ostream& operator<<(ostream& lhs, const Name& rhs)
{
    const char* value = rhs.getValue();
    lhs << (value == 0 ? "(none)" : value);
    return lhs;
}

設計のポイントを確認していきます。

メモリを動的に確保している場合は、デストラクタで必ず解放(削除)する。

名前クラスの場合、コンストラクタの中で、new でメモリを動的に確保しているので、デストラクタで解放する必要があります。(「Effective C++」6項参照)

メモリを動的に確保している場合は、コピーコンストラクタと代入演算子を明示的に定義する。

「コピーを作ることができる」「基本データ型と同じセマンティックス」ということから、次のような使われ方が想定されます。

Name name1("Kudou Shinichi");
Name name2("Mouri Ran");

// オブジェクトのコピー.
Name name3(name1);
// オブジェクトの代入.
name3 = name2;

ソースコードにコピーコンストラクタと代入演算子が明示的に定義されていない場合、C++ のコンパイラは次のようなコードを勝手に生成します。

Name::Name(const Name& other)
    : m_value(other.m_value)
{
}

Name& Name::operator=(const Name& other)
{
    m_value = other.m_value;
    return *this;
}

クラス内でメモリを動的に確保していなければこれで充分なのですが、メモリを動的に確保している場合、このままだとどこからも指されていないメモリや、複数箇所から指されているメモリができ、エラーが発生してしまいます。したがって、コピーコンストラクタと代入演算子を明示的に定義する必要があります。(「Effective C++」11項参照)

等価演算子を定義することが望ましい。

このクラスのインスタンスを次のように生成した場合、

Name name1("Kudou Shinichi");
Name name2("Kudou Shinichi");

2つのインスタンスは実体は異なりますが、同じ値をもっているので同じオブジェクトと見なすことができます。名前クラスの場合、getValue() でとってきた値を比較しても良いですが、上のコードのように、等価演算子を定義しておくとオブジェクト同士を直接比較することができます。

if (name1 == name2) {
    // do something...
}

なお、等価演算子は、クラスの外で次のような形式で定義することもできます。

bool operator==(const Name& lhs, const Name& rhs);
bool operator!=(const Name& rhs, const Name& rhs);

メンバ関数の const の指定を厳密に行う。

データ値の保証をするために const Name として使えるよう、状態を変更しないメンバ関数にはシグニチャの後に const を指定します。(「Effective C++」21項参照)

すべてのメンバ関数は非仮想関数。

ポリモフィズム的な使用を想定していないので、メモリ効率とパフォーマンスを重視し、すべての関数は非仮想関数( virtual をつけない)とします。

その他の注意事項

値オブジェクトに限りませんが、引数を1つとるコンストラクタは通常 explicit をつけて暗黙の型変換を禁止し、以下のようなコードが通るのを防止します。

Name name;

// explicit がないとこれが通る.
name = "Edogawa Conan";

また、出力演算子( operator<< )も定義しておくと、次のように使えて便利でしょう。

cout << name << endl;

1.3. 値オブジェクトの利用

続いて「値オブジェクト」を利用する側の注意点を確認しておきます。

変数の宣言と初期化は具象クラスの値で行う。

「値オブジェクト」の宣言と初期化は以下のように行います。

Name name1;
Name name2("Kudou Shinichi");

まれにポインタで宣言することがあるかもしれませんが、ポリモフィズム的な使用を想定して設計されていないので、子クラスのインスタンスを親クラスのポインタで受けてはいけません。

class ExtendedName : public Name ...

// これをやってはいけない.
Name* name = new ExtendedName("Edogawa Conan");

// こう宣言する.
ExtendedName name1("Edogawa Conan");
ExtendedName* name2 = new ExtendedName("Edogawa Conan");

関数の(入力)引数として使用する場合は、const 参照で渡す。

次のように「値オブジェクト」を関数へ参照やポインタで渡すと、関数側で呼び出し側の変数を変更できてしまい、「値オブジェクト」にとって重要な「値」が保証されなくなってしまいます。

// これをやってはいけない.
void useAsParam(Name& name)
{
    // 呼び出し側の変数 name を変更できてしまう.
    name = "Mouri Kogorou";
}

基本型と同じセマンティックスという原則なので、

// これでも良いが...
void useAsParam(Name name)
{
    // ここで見える name は呼び出し側とは別の実体.
}

このように宣言しても良いのですが、オブジェクトの無駄なコピーが発生するため、通常は以下のように const 参照で渡します。

// この形式がベター.
void useAsParam(const Name& name)
{
    // name の状態は変更できない.
}

このようにすると、関数内では name の状態を読み出すことはできますが、変更することはできなくなります。(「Effective C++」22項参照)

関数の戻り値として使用する場合は、値で返す。

「値オブジェクト」を戻り値として使用する場合は、値で返します。(「Effective C++」23項参照)

Name useAsReturn()
{
    Name name;
    // do something...
    return name;
}

メンバ変数として使用する場合。

他のクラスのメンバ変数として使用する場合は、通常次の形式になります。

class Client
{
  public:
    // アクセッサ.
    Name getName() const;
    void setName(const Name& value);

  private:
    // メンバ変数.
    Name m_name;
};

2. 参照オブジェクト

2.1. 参照オブジェクトとは

次のような特徴をもったオブジェクトを「参照オブジェクト」といいます。

  • インスタンス自身が重要な意味をもつ。
  • 同じデータを持っていても、インスタンスが異なれば違うオブジェクトと見なす。
  • 通常コピーは作らない。

分析段階で抽出される、いわゆる普通の「オブジェクト」のことです。標準ライブラリでは、iostream がこのカテゴリに入ります(このノートとは多少使い方が異なりますが)。

2.2. 参照オブジェクトの設計

「人(Person)」クラスを例に考えましょう。ソースコードは以下のようになります。

Person.h

#ifndef Person_h
#define Person_h

#include <iostream>

class Person
{
  public:
    // デフォルトコンストラクタ.
    Person();

    // 引数を1つとるコンストラクタ.
    explicit Person(const char* name);

    // virtual なデストラクタ.
    virtual ~Person();

    // アクセッサ.
    const char* getName() const;
    void setName(const char* value);

    // 内容を出力する.
    virtual void print(std::ostream& out) const;

  private:
    // コピーコンストラクタは無効に.
    Person(const Person& other);

    // 代入演算子も無効に.
    Person& operator=(const Person& other);

  private:
    char* m_name;
};

// 出力演算子.
std::ostream& operator<<(std::ostream& lhs, const Person& rhs);
std::ostream& operator<<(std::ostream& lhs, const Person* rhs);

#endif /* !Person_h */

Person.cpp

#include "Person.h"
#include <cstring>

using namespace std;

// デフォルトコンストラクタ.
Person::Person()
    : m_name(0)
{
}

// 引数を1つとるコンストラクタ.
Person::Person(const char* name)
    : m_name(0)
{
    if (name != 0) {
        m_name = new char[strlen(name) + 1];
        strcpy(m_name, name);
    }
}

// デストラクタ.
Person::~Person()
{
    delete[] m_name;
}

// アクセッサ.
const char* Person::getName() const
{
    return m_name;
}

void Person::setName(const char* value)
{
    if (m_name == value) return;

    delete[] m_name;

    if (value == 0) {
        m_name = 0;
    } else {
        m_name = new char[strlen(value) + 1];
        strcpy(m_name, value);
    }
}

// 内容を出力する.
void Person::print(ostream& out) const
{
    if (m_name == 0) {
        out << "Person";
    } else {
        out << "Person(" << m_name << ")";
    }
}

// 出力演算子.
ostream& operator<<(ostream& lhs, const Person& rhs)
{
    rhs.print(lhs);
    return lhs;
}

ostream& operator<<(ostream& lhs, const Person* rhs)
{
    if (rhs == 0) {
        lhs << "(null)";
    } else {
        rhs->print(lhs);
    }
    return lhs;
}

設計のポイントを確認していきます。

デストラクタを仮想関数として定義する。

必ず具象クラスのデストラクタが呼ばれるように、デストラクタは仮想関数(virtual をつける)として定義します。メモリを動的に確保している場合は、もちろんデストラクタで解放します。(「Effective C++」14項参照)

コピーコンストラクタと代入演算子は private で宣言のみ行う。

コピーコンストラクタと代入演算子は private で宣言のみ行い、オブジェクトのコピーを禁止します。(「Effective C++」11項参照)

等価演算子は定義しない。

インスタンス自身が重要な意味を持っていますので、等価演算子は定義せずに、オブジェクトの識別はアドレスを直接用いて行います。

メンバ関数の const 指定は、あまりこだわらなくても良い。

Getter などお決まりのメンバ関数以外は、const かどうかあまりこだわらなくても良いでしょう。

必要に応じて仮想関数を使用。

必要ならばメンバ関数の宣言に virtual をつけて仮想関数にします。

その他の注意事項

引数を1つとるコンストラクタへの explicit の指定は値オブジェクトのときと同じです。出力演算子は、コード例のように仮想関数を使って実装すると良いでしょう。

2.3. 参照オブジェクトの利用

続いて「参照オブジェクト」を利用する側の注意点を確認しておきます。

変数の宣言は抽象クラスで行ってもよい。

「参照オブジェクト」の宣言と初期化は通常以下のように行います。

Person* person = new Person("Kudou Shinichi");

使用後は削除するのを忘れないようにしましょう。

delete person;

子クラスのインスタンスを親クラスのポインタで受けてもかまいません。

class ExtendedPerson : public Person ...

// こうしてもよい.
Person* person = new ExtendedPerson("Edogawa Conan");

// 親クラスのポインタ経由で削除可能.
delete person;

むしろ、インタフェースと実装を積極的に分離することで、システムの開発効率、保守性、再利用性を高めることができます。

関数の引数として使用する場合は、ポインタで渡す。

関数の中でオブジェクトの状態を変更する場合も多いと思いますので、通常 const をつけずにポインタで渡します。

void useAsParam(Person* person)
{
    // do something...
}

基本的に関数の戻り値としては使用しない。

メンバ変数の Getter や Factory などを除いて、戻り値として使用することはあまりないでしょう。

メンバ変数として使用する場合。

他のクラスのメンバ変数として使用する場合は、通常次の形式になります。

class Client
{
  public:
    // アクセッサ.
    Person* getPerson() const;
    void setPerson(Person* value);

  private:
    // メンバ変数.
    Person* m_person;
};

Client クラスが Person のライフサイクルを管理している場合は、メモリのエラーが起こらないように Setter の実装に注意してください。必要がなければ Setter は用意しない方が良いでしょう。

3.参照型値オブジェクト

3.1. 参照型値オブジェクトとは

こういう言葉は実際にはないのですが、便宜上以下のようなオブジェクトを「参照型値オブジェクト」と仮に呼ぶことにします。

  • インスタンス自体よりも、保持するデータ値が重要な意味をもつ。
  • インスタンスが異なっても、同じ値を保持していれば同じオブジェクトと見なす。
  • 必要に応じて自由にコピーを作ることができる。

ここまでは値オブジェクトと同様なのですが、

  • オブジェクトをポリモフィックに扱いたい。

という点が値オブジェクトとは異なります。XML の DOM など Composite なデータ構造を作りたい場合などに使用します。

Composite

3.2. 参照型値オブジェクトの設計

先程の「名前(Name)」クラスを例に考えましょう。ソースコードは以下のようになります。

Object.h

#ifndef Object_h
#define Object_h

#include <cstddef>
#include <iostream>

class Object
{
  public:
    // デフォルトコンストラクタ.
    Object();

    // virtual なデストラクタ.
    virtual ~Object();

    // オブジェクトを複製する.
    virtual Object* clone() const;

    // 等価なオブジェクトか?
    virtual bool equals(const Object& other) const;

    // 内容を出力する.
    virtual void print(std::ostream& out) const;

    // メモリ管理演算子.
    static void* operator new(std::size_t size);
    static void  operator delete(void* p);

  protected:
    // コピーコンストラクタは子クラスのみ使用可.
    Object(const Object& other);

  private:
    // 代入演算子は無効に.
    Object& operator=(const Object& other);
};

// 等価演算子.
bool operator==(const Object& lhs, const Object& rhs);
bool operator!=(const Object& lhs, const Object& rhs);

// 出力演算子.
std::ostream& operator<<(std::ostream& lhs, const Object& rhs);
std::ostream& operator<<(std::ostream& lhs, const Object* rhs);

#endif /* !Object_h */

Object.cpp

#include "Object.h"

using namespace std;

// デフォルトコンストラクタ.
Object::Object() {}

// コピーコンストラクタ.
Object::Object(const Object& other) {}

// デストラクタ.
Object::~Object() {}

// オブジェクトを複製する.
Object* Object::clone() const
{
    return 0;
}

// 等価なオブジェクトか?
bool Object::equals(const Object& other) const
{
    return (this == &other);
}

// 内容を出力する.
void Object::print(ostream& out) const
{
    out << "Object";
}

// メモリ管理演算子.
void* Object::operator new(size_t size)
{
    // 実装省略.
}

void Object::operator delete(void* p)
{
    // 実装省略.
}

// 等価演算子.
bool operator==(const Object& lhs, const Object& rhs)
{
    return lhs.equals(rhs);
}

bool operator!=(const Object& lhs, const Object& rhs)
{
    return !(lhs.equals(rhs));
}

// 出力演算子.
ostream& operator<<(ostream& lhs, const Object& rhs)
{
    rhs.print(lhs);
    return lhs;
}

ostream& operator<<(ostream& lhs, const Object* rhs)
{
    if (rhs == 0) {
        lhs << "(null)";
    } else {
        rhs->print(lhs);
    }
    return lhs;
}

Name.h

#ifndef Name_h
#define Name_h

#include "Object.h"

class Name : public Object
{
  public:
    // デフォルトコンストラクタ.
    Name();

    // 引数を1つとるコンストラクタ.
    explicit Name(const char* value);

    // virtual なデストラクタ.
    virtual ~Name();

    // アクセッサ.
    const char* getValue() const;
    void setValue(const char* value);

    // オブジェクトを複製する.
    virtual Name* clone() const;

    // 等価なオブジェクトか?
    virtual bool equals(const Object& other) const;

    // 内容を出力する.
    virtual void print(std::ostream& out) const;

  protected:
    // コピーコンストラクタは子クラスのみ使用可.
    Name(const Name& other);

  private:
    // 代入演算子は無効に.
    Name& operator=(const Name& other);

  private:
    char* m_value;
};

#endif /* !Name_h */

Name.cpp

#include "Name.h"
#include <cstring>

using namespace std;

// デフォルトコンストラクタ.
Name::Name()
    : m_value(0)
{
}

// 引数を1つとるコンストラクタ.
Name::Name(const char* value)
    : m_value(0)
{
    if (value != 0) {
        m_value = new char[strlen(value) + 1];
        strcpy(m_value, value);
    }
}

// コピーコンストラクタ.
Name::Name(const Name& other)
    : Object(other), m_value(0)
{
    if (other.m_value != 0) {
        m_value = new char[strlen(other.m_value) + 1];
        strcpy(m_value, other.m_value);
    }
}

// デストラクタ.
Name::~Name()
{
    delete[] m_value;
}

// アクセッサ.
const char* Name::getValue() const
{
    return m_value;
}

void Name::setValue(const char* value)
{
    if (m_value == value) return;

    delete[] m_value;

    if (value == 0) {
        m_value = 0;
    } else {
        m_value = new char[strlen(value) + 1];
        strcpy(m_value, value);
    }
}

// オブジェクトを複製する.
Name* Name::clone() const
{
    return new Name(*this);
}

// 等価なオブジェクトか?
bool Name::equals(const Object& other) const
{
    if (this == &other) {
        return true;
    }

    const Name* name = dynamic_cast<const Name*>(&other);
    if (name == 0) {
        return false;
    }
    if (m_value == name->m_value) {
        return true;
    }
    if (m_value == 0 || name->m_value == 0) {
        return false;
    }
    return strcmp(m_value, name->m_value) == 0;
}

// 内容を出力する.
void Name::print(ostream& out) const
{
    out << (m_value == 0 ? "(none)" : m_value);
}

設計のポイントを確認していきます。

デストラクタを仮想関数として定義する。

参照オブジェクトと同じように、デストラクタは仮想関数(virtual をつける)として定義します。

コピーコンストラクタは protected で定義する。

コピーコンストラクタは protected で定義し、clone() からのみ使用可能にします。

仮想メンバ関数 clone() を定義する。

オブジェクトの複製は、適切な具象クラスの複製を作るために、仮想メンバ関数 clone() を定義して行います。

Object* object = new Name("Kudou Shinichi");
// Name オブジェクトとして複製される.
Object* clone = object->clone();

なお、子クラス Nameclone() の戻り値を Object から Name に変更(共変; covariant)していますが、こうすると複製時にダウンキャストしなくてすみます。

Name* name = new Name("Mouri Ran");
// ダウンキャストの必要がない.
Name* clone = name->clone();

古いコンパイラでこのコードが通らない場合は、以下のようにしておきます。

class Name : public Object
{
  public:
    // 戻り値を Object* に戻す.
    virtual Object* clone() const;

    // 代わりにこれを用意する.
    Name* cloneName() const;

    // ...
};

Object* Name::clone() const
{
    return new Name(*this);
}

Name* Name::cloneName() const
{
    return static_cast<Name*>(this->clone());
}

代入演算子は private で宣言のみ行う。

代入演算子は private で宣言のみ行い、オブジェクトの代入を禁止します。

等価演算子は仮想関数で実装。

実行するまで実際の型がわからないので、コード例のように仮想関数を使って実装します。

メンバ関数の const の指定を厳密に行う。

値オブジェクトと同じように、状態を変更しないメンバ関数にはシグニチャの後に const を指定します。

必要に応じてメモリ管理演算子を定義。

小さなオブジェクトを頻繁に newdelete することが多くなるので、パフォーマンスやメモリフラグメンテーションが問題になるようなら、基底クラス Object でメモリ管理演算子(operator newoperator delete)を定義します。

その他の注意事項

引数を1つとるコンストラクタへの explicit の指定、出力演算子の実装は、参照オブジェクトのときと同じです。

3.3. 参照型値オブジェクトの利用

最後に「参照型値オブジェクト」を利用する場合の注意点を確認しておきます。

変数の宣言は抽象クラスで行ってもよい。

「参照型値オブジェクト」の宣言と初期化は以下のように行います。

Name name1("Kudou Shinichi");
Name* name2 = new Name("Mouri Ran");

子クラスのインスタンスを親クラスのポインタで受けることもできます。

// こうしてもよい.
Object* object = new Name("Edogawa Conan");

オブジェクトの複製は clone() を用いて行う。

設計のポイントで書いたように、コピーコンストラクタや代入演算子ではなく、clone() を用いてオブジェクトを複製します。

ポインタで宣言する場合は、auto_ptr を使用する。

ローカル変数をポインタで宣言する場合は、削除し忘れや例外発生時の対応を考慮し、auto_ptr を使用した方がよいでしょう。

auto_ptr<Name> name2(new Name("Mouri Ran"));
auto_ptr<Object> object(new Name("Edogawa Conan"));

関数の(入力)引数として使用する場合は、const 参照で渡す。

値オブジェクトと同じように、関数へは const 参照で渡します。実行時の型がわからないので、値渡しをしてはいけません。

// こうする.
void useAsParam(const Object& object)
{
    // object の状態は変更できない.
}

// これはダメ.
// Name を渡すとコンパイルエラーになるはず.
void useAsParam(Object object)
{
    // do something...
}

関数の戻り値として使用する場合は、ポインタで返す。

実行時の型がわからないので、戻り値を値で返すことができません。しかたがないのでポインタで返しますが、オブジェクトのライフサイクルの管理の責任がどちらにあるかはっきりさせる必要があります。

// この場合は呼び出し側で削除する必要がある.
Object* useAsReturn()
{
    Object* object;
    object = new Name("Haibara Ai");
    return object;
}

// 呼び出し側のコード.
auto_ptr<Object> result(useAsReturn());

メンバ変数として使用する場合。

他のクラスのメンバ変数として使用する場合は、通常次の形式になります。

class Client
{
  public:
    // デストラクタ.
    ~Client() {}

    // アクセッサ.
    Object* getValue() const;
    void setValue(Object* value);
    void setValue(const Object& value);

  private:
    // メンバ変数.
    Object* m_value;
};

Object* getValue() const
{
    return m_value;
}

void setValue(Object* value)
{
    m_value = value;
}

void setValue(const Object& value)
{
    m_value = value.clone();
}

Getter は内部状態を返していますので、長期的に値を保持したい場合は呼び出し側で複製を作った方がよいでしょう。

Client client;

auto_ptr<Object> result(client.getValue()->clone());

ポインタ版の setValue() は、呼び出し側で新しいオブジェクトを new してセットし、const 参照版の setValue() では、Setter 側で複製を作ります。

Client client;

// ポインタ渡し.
client.setValue(new Name("Kojima Genta"));

Name name("Tuburaya Mitsuhiko");
// const 参照渡し.
client.setValue(name);

4. まとめ

以上、主にメモリ管理の観点から、C++ によるシステムの詳細設計を行う上での注意点を整理してみました。リファレンスとしてより充実した内容にしていきたいと思いますので、ご意見・ご質問等ありましたらどうぞお寄せください。

《参考文献》

  • Scott Meyers「Effective C++(改訂2版)」(アスキー)
  • Bjarne Stroustrup「プログラミング言語 C++(第3版)」(アスキー)
  • Joshua Bloch「Effective Java」(ピアソン・エデュケーション)
  • Martin Fowler「リファクタリング」(ピアソン・エデュケーション)
改訂履歴
  • 初版: 2002/10/24
  • 修正: 2002/11/05