auto_ptr | Programming Place Plus C++編【標準ライブラリ】 第16章

トップページC++編

C++編で扱っている C++ は 2003年に登場した C++03 という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14 -> C++17 -> C++20 と更新されており、今後も 3年ごとに更新されます。
なかでも C++11 での更新は非常に大きなものであり、これから C++ の学習を始めるのなら、C++11 よりも古いバージョンを対象にするべきではありません。特に事情がないなら、新しい C++ を学んでください。 当サイトでは、C++14 をベースにした新C++編を作成中です。

この章の概要

この章の概要です。


auto_ptr

この章で解説する auto_ptr は、C++11 で非推奨となっています。C++11 以降が使える環境では、unique_ptr や shared_ptr といった、新しい仕組みを使用してください。

auto_ptr は、new によって動的に確保されたメモリ領域の解放忘れを防ぐクラステンプレートです。使用する際には、<memory> という標準ヘッダのインクルードが必要です。

解放忘れを防ぐ仕組みは単純で、new で確保されたメモリ領域を指すポインタを、auto_ptr に渡しておけば、 auto_ptr のデストラクタ内で delete してくれるというだけです。そのため、new [] や std::malloc などといった、new 以外で確保された領域の解放には対応していません

new によって確保されたメモリ領域は、ポインタによってアクセスするので、auto_ptr もポインタのように振る舞えるように設計されています。auto_ptr のように、ポインタに何らかの機能を付加したものをスマートポインタと呼びます。

使用例は次のようになります。

#include <iostream>
#include <memory>

int main()
{
    std::auto_ptr<int> p(new int(100));

    std::cout << *p << std::endl;
}  // auto_ptr のデストラクタが呼び出されて、delete される

実行結果

100

メモリ領域などのリソース(資源)の確保と解放を、auto_ptr のようなクラスに任せるスタイルは、C++ では常識的なものです。生のポインタを用いずに、必ずスマートポインタを利用するようにしてください。


初期化

auto_ptr のコンストラクタは、引数がないものと、生のポインタを渡すものとがあります。

#include <iostream>
#include <memory>

int main()
{
    std::auto_ptr<int> p1;
    std::auto_ptr<int> p2(new int(100));
}

引数無しの場合や、ヌルポインタを渡した場合、auto_ptr は何も管理していない状態で初期化されます。つまり、ヌルポインタ相当な状態になります。

auto_ptr のコピーコンストラクタは特殊なので、注意が必要です。次の2つが定義されています。

auto_ptr(auto_ptr& rhs);

template <typename U>
auto_ptr(auto_ptr<U>& rhs);

前者は、auto_ptr自身と同じ型を指定し、後者はテンプレート仮引数が異なる別の auto_ptr型を指定するコンストラクタテンプレート(【言語解説】第26章)です。

特殊なのは、両者とも、引数に「const」が付いていない点です。普通、コピーコンストラクタは、コピーを行うものなので、コピー元は書き換わりませんし、それを示すように「const」が付加されますが、auto_ptr の場合は、コピー元が変化します。これが auto_ptr の最大の特徴です。この点については、後の項であらためて取り上げます。

破棄

デストラクタでは、管理中のポインタに対して delete を行います。ヌルポインタに対する delete は何も起こらない(【言語解説】第14章)ので、管理しているポインタがなくても問題ありません。

ポインタ操作

auto_ptr は、ポインタと同じように振る舞えるように設計されています。

*演算子を使って間接参照したり、->演算子を使ってメンバをアクセスしたりできます。いずれも、管理中のポインタがない場合の挙動は未定義であることに注意してください

有効なポインタを管理しているかどうかを知りたければ、getメンバ関数を使います。この関数は、管理中のポインタを返しますが、管理されていなければ NULL が返されます。

#include <iostream>
#include <memory>

int main()
{
    std::auto_ptr<int> p1;
    std::auto_ptr<int> p2(new int(100));

    int num = *p2;  // OK
    num = *p1;      // 未定義

    // 安全
    if (p1.get() != NULL) {
        num = *p1;
    }
}

getメンバ関数で管理中のポインタを得られますが、これはヌルチェックのためか、引数の型が生のポインタになっているような、C言語的な関数にポインタを渡さないといけない場合以外には、使わないようにすべきです。スマートポインタが管理してくれているポインタを、生のポインタ変数で扱うと、思わぬバグの原因になります。

なお、++、–、+、-、+=、-= といった演算子を使って、ポインタが指す位置を移動させることはできません。また、-演算子を使った、auto_ptr どうしの差の計算もできません

所有権

auto_ptr の挙動を理解するには、所有権という考え方が重要です。

所有権を言い換えると、「ポインタの解放を行う責任を持っているか」ということです。もし、1つのポインタに対する所有権を、2つ以上のオブジェクトが持っていたら、両者が解放を行おうとして、二重解放の問題が起こります。

1つの auto_ptr は、0個か1個のポインタの所有権を持ちます。auto_ptr が所有権を持っているポインタを、他のオブジェクトが所有していてはいけません。ややこしい話のようですが、new で手に入れたポインタを即座に auto_ptr に渡すようにしていれば、まずは問題ありません

std::auto_ptr<int> p(new int(100));  // new の結果は即座に auto_ptr に渡すべき

// 以下のように、いったんどこかで受け取るのは良くない方法
int* n = new int(100);
std::auto_ptr<int> p(n);

auto_ptr は、コンストラクタで生のポインタを渡されると、そのポインタの所有権を得ます。それ以外にも、resetメンバ関数でも所有権を得ます。

#include <iostream>
#include <memory>

int main()
{
    std::auto_ptr<int> p(new int(100));

    std::cout << *p << std::endl;

    p.reset(new int(200));

    std::cout << *p << std::endl;
}

実行結果

100
200

resetメンバ関数の場合、すでに所有しているポインタがあれば、まず delete による解放を行います。ですから、やはり解放忘れは起きません。また、前述したアドバイスは resetメンバ関数でも同様で、new で得たポインタを即座に渡すべきです。

resetメンバ関数にヌルポインタを渡した場合、あるいは実引数を省略した場合には、すでに所有しているポインタがあれば解放を行い、結果として何も所有していない状態になります。

また、releaseメンバ関数を使うと、所有しているポインタの所有権を手放せます。

#include <iostream>
#include <memory>

int main()
{
    std::auto_ptr<int> p(new int(100));

    int* n = p.release();

    if (p.get() == NULL) {
        std::cout << "NULL" << std::endl;
    }
    else {
        std::cout << *p << std::endl;
    }

    std::cout << *n << std::endl;

    delete n;  // p はもう管理していないので、自力で解放する必要がある
}

実行結果

NULL
100

auto_ptr の所有権に関してはさらに重要なポイントがあります。この点について、「破壊的コピー」の項で説明します。


破壊的コピー

auto_ptr の性質として非常に重要なポイントがあります。それは、コピーがコピーでないことです。これは、コピーコンストラクタと、代入演算子のどちらでも当てはまります。

具体的には、コピー操作を行うと、コピー元の auto_ptr は管理下のポインタの所有権を失い、コピー先の auto_ptr へ引き継がれます。結果、コピー元の auto_ptr は、何も所有していない状態、つまりヌルポインタ相当な状態になります。

#include <iostream>
#include <memory>

int main()
{
    std::auto_ptr<int> p1(new int(100));
    std::auto_ptr<int> p2;

    p2 = p1;  // 所有権が p2 へ移り、p1 はヌルポインタになる

    if (p1.get() == NULL) {
        std::cout << "NULL" << std::endl;
    }
    std::cout << *p2 << std::endl;
}

実行結果

NULL
100

このようなコピーの特性から、破壊的コピーと呼ばれます。

破壊的コピーの特性があることで、次のような関数は問題なく動作できます。

#include <iostream>
#include <memory>

std::auto_ptr<int> GetPointer(int num)
{
    std::auto_ptr<int> p(new int(num));
    return p;
}

int main()
{
    std::auto_ptr<int> p = GetPointer(100);

    std::cout << *p << std::endl;
}

実行結果

100

GetPointer関数内のローカル変数として auto_ptr を使用しているので、関数を抜けると、デストラクタが呼ばれて解放されてしまいそうですが、そうではありません。

戻り値を返すために、p をコピーして、一時オブジェクトが作られます。このとき、破壊的コピーにより、p の管理下にあったポインタの所有権が、一時オブジェクトの方へ引き継がれ、p は所有権を失います。

もし、GetPointer関数の呼び出し側で戻り値を受け取っていれば、受け取り側の auto_ptr へ所有権が移動します。受け取っていない場合は、一時オブジェクトが所有権を持ったまま破棄されることになるので、デストラクタによって delete され、やはり問題ありません。

【上級】コンパイラによる最適化によって、この流れの一部は省略されるかもしれません(【言語解説】第17章)。

auto_ptr 自体を const にすることで、破壊的コピーを避けることができるという点は、テクニックとして知っておくと良いでしょう。

#include <iostream>
#include <memory>

int main()
{
    const std::auto_ptr<int> p1(new int(100));
    std::auto_ptr<int> p2;

    p2 = p1;  // コンパイルエラー

    std::cout << *p2 << std::endl;
}

所有権を手放すつもりがない場合には、こうして const にしておけば安全です。

auto_ptr に限らず、書き換えるつもりがないときは、const を積極的に使いましょう(【言語解説】第15章)。

コンテナとの相性

通常、コピー操作が成功した場合、コピー元とコピー先は完全に同じ状態になることを期待します。これは、STLコンテナが、そこに格納される要素へ期待している要件の1つでもあります(第4章)。auto_ptr は、この要件を満たしていないので、STLコンテナの要素に、auto_ptr を渡すことは避けなければなりません

逆に、auto_ptr がコンテナ型のポインタを管理することには問題はありません。

配列の管理

auto_ptr のデストラクタでしているのは、delete を適用することなので、new [] で確保された領域を指すポインタの管理には使用できません。使用できないといっても、コンパイルは通ってしまうので注意してください。

動的に確保された配列を管理したい場合は、auto_ptr ではなく vector(文字列なら basic_string)を使ってください(第5章)。


練習問題

問題① 次のプログラムを、auto_ptr を使って書き直してください。

#include <iostream>

void Print(const int* n)
{
    if (n == NULL) {
        std::cout << "NULL" << std::endl;
    }
    else {
        std::cout << *n << std::endl;
    }
}

int main()
{
    const int* const n = new int(100);

    Print(n);

    delete n;
}


解答ページはこちら

参考リンク


更新履歴

’2016/1/31 新規作成。



前の章へ (第15章 ユーティリティ)

次の章へ (第17章 例外クラス)

C++編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る