Taro3

View on GitHub

Effective C++ メモ

第 3 章 リソース管理

13 項 リソース管理にオブジェクトを使おう

下記のようなファクトリ関数があるとします。

Investment* createInvestment(); // Investment の派生クラスのオブジェクトを
                                // 生成し、そのオブジェクトへのポインタを返す
                                // そのオブジェクトは呼び出し元で破棄する
                                // (引数は簡単にするために省略)

これを下記のように使うとします。

void f()
{
    Investment *pInv = createInvestment();  // ファクトリ関数の呼び出し

    ...                                     // pInv を使う

    delete pInv;                            // オブジェクトを破棄
}

このコードは安全そうですが、必ず pInv が delete されるとは限りません。… の部分の処理中に return してしまったり、例外が発生する可能性があるためです。

このような場合、リソースをオブジェクトの中に置き、デストラクタで破棄します。 標準ライブラリの unique_ptr は、そのようなメモリ操作を行うためのスマートポインタです。 unique_ptr を使用すると下記のようになります。

void f()
{
    std::unique_ptr<Investment> pInv(createInvestment());   // ファクトリ関数の呼び出し

    ...                                                     // 前と同様に pInv を使う

}                                                           // 自動的に pInv を unique_ptr の
                                                            // デストラクタで破棄

これは、リソース管理を行うオブジェクトの、2 つの重要な事実を示しています。

1 つのリソースを指し示すために、複数の unique_ptr は使用できません。

std::unique_ptr<Investment>                 // pInv1 は createInvestment が生成した
    pInv1(createInvestment());              // オブジェクトを指し示すようになる

std::unique_ptr<Investment> pInv2(pInv1);   // pInv2 がそのオブジェクトを指し示すように
                                            // なり、pInv1 はヌルになる

pInv1 = pInv2;                              // 今度は、pInv1 がそのオブジェクトを指し示し、
                                            // pInv2 がヌルになる

1 つのリソースを複数から指し示したいときは、shared_ptr を使用します。

void f()
{
    ...
    std::shared_ptr<Investment>
        pInv(createInvestment());   // ファクトリ関数の呼び出し

    ...                             // 前と同じように pInv を使う

}                                   // pInv の指すオブジェクトは
                                    // shared_ptr デストラクタで
                                    // 自動的に破棄される

上記のコードは、unique_ptr と殆ど同じに見えますが、次のようなコードを書くことができます。

viod f()
{
    ...
    std::shared_ptr<Investment>     // pInv1 は createInvestment の
        pInv1(createInvestment());  // 生成したオブジェクトを指す

    std::shared_ptr<Investment>     // これで pInv1 と pInv2 が同じ
        pInv2(pInv1);               // オブジェクトを指すようになる

    pInv1 = pInv2;                  // これは何も変わらない
    ...
}                                   // pInv1 と pInv2 は破棄され、
                                    // これらが指していたオブジェクトも
                                    // 自動的に破棄される

ここでのアドバイスは、「リソースを開放するコードを直接書かなければならないなら(つまり、リソース管理オブジェクト以外の場所に delete 文を書かなければならないなら)、何かが間違っている」ということです。 ここでの、createInvestment のように、生のポインタを返す関数は、クライアント側でリソース漏れを起こすコードにつながりやすいと指摘しておきます。

覚えておくこと

14 項 リソース管理クラスのコピーの振る舞いはよく考えて決めよう

下記のように使用する Mutex クラスを考えてみます。

void lock(Mutex *pm);   // pm の指すミューテックスをロックする
void unlock(Mutex *pm); // pm の指すミューテックスをアンロックする

ここで、アンロックを忘れないように、ロック管理を行うクラスを RAII で作るとします。

class Lock {
public:
    explicit Lock(Mutex *pm)
    : mutexPtr(pm)
    { lock(mutexPtr); }             // リソースの確保(ミューテックスのロック)
    ~Lock() { unlock(mutexPtr); }   // リソースの開放(ミューテックスのアンロック)
private:
    Mutex *mutexPtr;
}

Lock の利用者は、RAII の使用方法にに従って Lock を使います。

Mutex m;            // これから使うミューテックスの定義
...
{                   // クリティカルセクションのブロックを生成
    Lock ml(&m);    // ミューテックスをロック
    ...             // クリティカルセクションの実行
}                   // ブロックの最後で自動的にミューテックスを開放

ここまでは問題ありませんが、ここでミューテックスがコピーされたらどうなるでしょうか?

Lock ml1(&m);   // m をロック
Lock ml2(ml1);  // ml2 を ml1 にコピー。すると、どうなるでしょう

ここでは狭い意味のコピーが発生します。「RAII オブジェクトがコピーされるとき、どうすべきか」という問題が発生します。 たいていは、以下の中の 1 つを選ぶことになります。

コピー関数(コピーコンストラクタとコピー代入演算子)は、コンパイラが自動的に生成することもありますが、自分が期待したものではない場合は、独自に実装する必要があるわけです。

覚えておくこと

15 項 リソース管理クラスには、リソースそのものへのアクセス方法を付けよう

下記のようなスマートポインタがあった場合

std::shared_ptr<Investment> pInv(createInvestment());

これを使う関数は次のような感じです。

int dayHeld(const Investment *pi);  // 投資されてから経った日数を返す

すると、次のように使いたくなります。

int days = daysHeld(pInv);  // エラー

このようなときのために、uniqut_ptr も shared_ptr も、内部で保持しているポインタを取り出す get というメンバ関数を持っています。

int days = daysHeld(pInv.get());  // 問題なし
                                  // pInv が保持するポインタを daysHeld に渡している

ほとんどのスマートポインタは(unique_ptr や shared_ptr も)、ポインタの逆参照演算子(-> 演算子と * 演算子)をオーバーロードしています。 これにより次のような使い方ができます。

class Investment {                                      // すべての「投資」を表すクラスの基底クラス
public:
  bool isTaxFree() const;
  ...
};

Investment* createInvestment();                         // ファクトリ関数

std::shared_ptr<Investment>                             // shared_ptr でリソース管理
  pi1(createInvestment());
bool taxable1 = !(pi1->isTaxFree());                     // -> 演算子を通してリソースにアクセス
...
std::unique_ptr<Investment> pi2(createInvestment());    // unique_ptr でリソース管理
bool taxable2 = !((*pi2).isTaxFree());                   // * 演算子を通してリソースにアクセス
...

RAII クラスの中には、内部のリソースにアクセスするための暗黙の型変換を持つものがあります。

FontHandle getFont();             // C スタイルの関数
                                  // 簡単にするため、引数は省略

void releaseFont(FontHandle fh);  // 同じく C スタイルの関数

class Font {                      // RAII クラス
public:
  explicit Font(FontHandle fh)    // リソースの確保
    : f(fh)                       // C スタイルの API を使うため値渡し
    {}
    ~Font() { releaseFont(f); }   // リソースの開放
private:
  FontHandle f;                   // 生のフォントリソース
};

フォントを扱うために FontHandle を使う C スタイルの関数がたくさんある場合、Font オブジェクトを頻繁に FontHandle に変換する必要が出てきます。そのために、明示的なリソースアクセスのための get を、Font クラスに定義することもできます。

class Font {
public:
  ...
  FontHandle get() const { return f; }  // 明示的にアクセスを与える関数
  ...
};

しかし、これだと、C スタイルのフォント関数を呼び出すたびに、get を使わなければなりません。

void changeFontSize(FontHandle f, int newSize); // C スタイルの関数

Font f(getFont());
int newFontFize;

...
changeFontSize(f.get(), newFontSize);           // Font から FontHandle を得るため get を使う

そこで、Font に、FontHandle への暗黙の型変換をもたせる方法もあるのです。

class Font {
public:
  ...
  operator FontHandle() const // 暗黙の型変換
  { return f; }
  ...
};

この演算子があると、C スタイルの「FontHandle を引数に取る関数」を、以下のように簡単に呼び出すことができるようになります。

Font f(getFont());
int newFontSize;
...
changeFontSize(f, newFontSize); // Font が FontHandle に自動的に変換される

この方法の欠点は、暗黙の型変換はエラーを引き起こしやすいことです。

Font f1(getFont());
...
FontHandle f2 = f1; // おっと!Font オブジェクトをコピーするつもり
                    // だったのに、f1 を FontHandle オブジェクト
                    // に変換してからコピーしてしまった

RAII オブジェクトの内部リソースへのアクセスは、カプセル化を破壊するという意見もありますが、適材適所でしょう。

覚えておくこと

16 項 対応する new と delete は同じ型のものを使おう

以下のコードは間違いです。

std::string *stringArray = new std::string[100];
...
delete stringArray;

配列を new で作成しているのに、delete は単一のオブジェクトに対して行われているためです。このときの動作は未定義になります。

std::string *stringPtr1 = new std::string;
std::string *stringPtr2 = new std::string[100];
...
delete stringPtr1;                              // 単独オブジェクトを破棄
delete [] stringPtr2;                           // オブジェクトの配列を破棄

上記のように、単独のオブジェクトと配列で deletedelete [] を使い分ける必要があります。 必ず対になるように使用しなければなりません。

また、typedef を使用するときも気をつけなければなりません。

typedef std::string AddressLines[4];  // string が 4 つの配列

上記のような typedef をした場合

std::string *pal = new AddressLines;  // new AddressLines は string* を戻す
                                      // これは new string[4] と同じ意味になる

この new に対する delete は同じ形式のものでなければなりません。

delete pal;     // 未定義!
delete [] pal;  // 問題なし

このような混乱を避けるため、配列を typedef することは避けたほうが良いでしょう。 代わりに、string や vector などで動的配列を確保しましょう。 今の例では、AddressLines は string の vector 、つまり、 vector<string> になります。

覚えておくこと

17 項 new で生成したオブジェクトをスマートポインタに渡すのは、独立したステートメントで行うようにしよう

整数を返す、priority という関数があり、動的に確保した Widget オブジェクトと priority の戻り値を引数に取る processWidget が下記のようにあった場合

int priority();
void processWidget(std::shared_ptr<Widget> pw, int priority);

ここでは、processWidget は、スマートポインタを受け取るようになっています。 ここで、processWidget は次のようには呼び出すことはできません。(コンパイルできません)

processWidget(new Widget, priority());

std::shared_ptr の「ポインタを受け取るコンストラクタ」は explicit で宣言されているので、new Widget で生成されるポインタが暗黙の型変換で std::shared_ptr に変換されることはないからです。 しかし、次のようなコードならコンパイルは通ります。

processWidget(std::shared_ptr<Widget>(new Widget), priority());

しかし、上記のコードではリソース漏れの可能性があります。 コンパイラが、引数を評価する順番はコンパイラに任せられているため、new Widget のあとに priority() が呼び出され、priority() で例外が発生した場合、new Widget で生成されたポインタが行方不明になってしまうからです。

これを避けるのは簡単で、Widget を生成したポインタをスマートポインタに引き渡す処理を、独立したステートメントに分ければよいのです。

std::shared_ptr<Widget> pw(new Widget); // 独立したステートメントで
                                        // オブジェクトのポインタを
                                        // スマートポインタに渡す

processWidget(pw, priority());          // これでリソース漏れは起きない

これで、必ず new Widget で生成されたポインタが、スマートポインタに引き渡される処理と、priority() の呼び出しが分けられるため、ポインタが行方不明になることがなくなるためです。

覚えておくこと


戻る