C++編【標準ライブラリ】 第16章 auto_ptr

先頭へ戻る

このエントリーをはてなブックマークに追加

この章の概要

この章の概要です。

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 を積極的に使いましょう(【言語解説】第18章)。

コンテナとの相性

通常、コピー操作が成功した場合、コピー元とコピー先は完全に同じ状態になることを期待します。 これは、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 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ