スマートポインタでコードを保護する
先ほど説明したコードは完全に機能していますが、特に AlbumDao::Albums() 関数を強化することができます。この関数では、データベースの行を反復処理し、リストを埋めるために新しい Album を作成します。この特定のコード部分を拡大してみましょう。
QVector<Album*> list;
while (query.next()) {
Album* album = new Album();
album->setId(query.value("id").toInt());
album->setName(query.value("name").toString());
list.append(album);
}
return list;
例えば、nameカラムの名前がtitleに変更されたとします。query.value(“name”)の更新を忘れた場合、トラブルに巻き込まれる可能性があります。Qt フレームワークは例外に依存していませんが、これはすべての API に当てはまるわけではありません。ここでの例外例外が発生するとメモリリークが発生します。Album* album 関数はヒープに割り当てられていますが、解放されていません。これを処理するには、危険なコードを try catch 文で囲み、例外が発生した場合は album パラメータを解放しなければなりません。たぶん、このエラーが出るはずです。したがって、try catch文はメモリリークの可能性を処理するためだけに存在しています。あなたの目の前にスパゲッティコードが織り成す様子を想像できますか?
ポインタの本当の問題は、その所有権の不確実性です。一度割り当てられたら、ポインタの所有者は誰でしょうか?オブジェクトを解放する責任は誰にあるのでしょうか? ポインタをパラメータとして渡した場合、呼び出し元が所有権を保持するのか、呼び出し元に解放するのか?
C++11以降、メモリ管理において大きな節目を迎えました。スマート ポインタ機能が安定化し、コードの安全性を大幅に向上させることができます。その目的は、シンプルなテンプレート セマンティクスによってポインタの所有権を明示的に示すことです。スマートポインタには3つのタイプがあります。
- unique_ptr ポインタは、そのポインタの所有者が唯一の所有者であることを示します。
- shared_ptr ポインタは、ポインタの所有権が複数のクライアント間で共有されていることを示します。
- weak_ptr ポインタは、そのポインタがクライアントに属していないことを示します。
今のところ、スマートポインタの仕組みを理解するために、unique_ptrポインタに注目します。
unique_ptr ポインタは、単にスタック上に割り当てられた変数で、あなたが提供したポインタの所有権を引き継ぎます。この意味合いでAlbumに割り当ててみましょう。
#include <memory>
void foo()
{
Album* albumPointer = new Album();
std::unique_ptr<Album> album(albumPointer);
album->setName("Unique Album");
}
スマートポインタのAPI全体は、memoryヘッダーで利用可能です。私たちが album を unique_ptr として宣言したとき、私たちは二つのことをしました。
- スタック上にunique_ptr<Album>を確保しました。unique_ptrポインタは、コンパイル時にポインタ型の有効性をチェックするためにテンプレートに依存しています。
- albumPointerメモリの所有権をalbumに付与しました。この時点から、albumがポインタの所有者となります。
このシンプルな行には重要な意味があります。何よりもまず第一に、ポインタのライフサイクルについてはもう心配する必要がありません。なぜなら、unique_ptr ポインタはスタック上に確保されているので、スコープから外れるとすぐに破棄されるからです。この例では、foo() を終了すると album はスタックから削除されます。unique_ptr の実装は、Album デストラクタの呼び出しとメモリの解放を行います。
第二に、コンパイル時にポインタの所有権を明示的に示すことです。彼らが自発的にあなたの unique_ptr ポインタをいじらなければ、誰も albumPointer コンテンツを解放することはできません。あなたの仲間の開発者もまた、誰があなたのポインタの所有者であるかを一目で知ることができます。
注意してほしいのは、album が unique_ptr<Album> の型であるにもかかわらず、Album 関数 (例えば album->setName() など) を -> 演算子を使って呼び出すことができるということです。これは、この演算子のオーバーロードのおかげです。unique_ptr ポインタの使用法は透過的になります。
まあ、このユースケースはいいのですが、 ポインタの目的はメモリの塊を確保して共有することです。foo() 関数が album unique_ptr ポインタを確保し、その所有権を bar() に転送するとしましょう。これは次のようになります。
void foo()
{
std::unique_ptr<Album> album(new Album());
bar(std::move(album));
}
void bar(std::unique_ptr<Album> barAlbum)
{
qDebug() << "Album name" << barAlbum->name();
}
ここでは、std::move()関数を紹介します。その目的は、unique_ptr関数の所有権を移転することです。bar(std::move(album))が呼ばれると、albumは無効になります。あなたは単純なif文でそれをテストすることができます。if (album) { … }。
これ以降、bar() 関数は、スタック上に新しい unique_ptr を割り当てることで (barAlbum を通して) ポインタの所有者になり、その終了時にポインタを解放します。これらのオブジェクトは非常に軽量で、アプリケーションのパフォーマンスに影響を与えることはほとんどないので、unique_ptr ポインタのコストを心配する必要はありません。
繰り返しになりますが、bar() のシグネチャは、この関数が渡された Album の所有権を取得することを期待していることを開発者に伝えています。move() 関数を使用せずに unique_ptr を渡そうとすると、コンパイルエラーになります。
もう一つ注意しなければならないのは、unique_ptr ポインタを使用しているときの . (ドット) と -> (矢印) の意味の違いです。
- ->演算子はポインタのメンバを参照し、実際のオブジェクトに対して関数を呼び出すことができます。
- .演算子を使用すると、unique_ptr オブジェクトの関数にアクセスすることができます。
unique_ptrポインタは様々な関数を提供します。中でも重要なものは以下の通りです。
- get() 関数は、生のポインタを返します。album.get() は Album* 値を返します。
- release() 関数は、ポインタの所有権を解放し、生のポインタを返します。album.release() 関数は Album* 値を返します。
- reset(pointer p = pointer()) 関数は、現在管理されているポインタを破棄し、与えられたパラメータの所有権を取得します。例として、barAlbum.reset() 関数は、現在所有している Album* を破棄します。パラメータがあれば、barAlbum.reset(new Album()) もまた、所有しているオブジェクトを破棄し、与えられたパラメータの所有権を取ります。
最後に、*操作でオブジェクトを派生させることができます、つまり*albumはAlbum&の値を返します。この派生参照は便利ですが、スマートポインタを使えば使うほど、その必要性が減ることがわかります。ほとんどの場合、生ポインタを以下の構文で置き換えることになります。
void bar(std::unique_ptr<Album>& barAlbum);
私たちは unique_ptr を参照で渡しているので、bar() はポインタの所有権を取得せず、終了時にポインタを解放しようとしません。これにより、foo() で move(album) を使う必要はありません。bar() 関数は album パラメータに対する操作を行うだけで、 所有権は取得しません。
では、shared_ptrについて考えてみましょう。shared_ptr ポインタはポインタ上に参照カウンタを保持します。shared_ptrポインタが同じオブジェクトを参照するたびに、カウンタはインクリメントされます。この shared_ptr ポインタがスコープ外に出ると、カウンタはデクリメントされます。カウンタがゼロになると、オブジェクトは解放されます。
foo()/bar() の例を shared_ptr ポインタで書き換えてみましょう。
#include <memory>
void foo()
{
std::shared_ptr<Album> album(new Album()); // ref counter = 1
bar(album); // ref counter = 2
} // ref counter = 0
void bar(std::shared_ptr<Album> barAlbum)
{
qDebug() << "Album name" << barAlbum->name();
} // ref counter = 1
ご覧のように、構文は unique_ptr ポインタと非常によく似ています。参照カウンタは、新しい shared_ptr ポインタが割り当てられ、同じデータを指すたびにインクリメントされ、関数の終了時にデクリメントされます。album.use_count()関数を呼び出すことで、現在のカウントを確認することができます。
最後に扱うスマートポインタは weak_ptr ポインタです。その名の通り、これは所有権を取らず、参照カウンタをインクリメントすることもありません。関数がweak_ptrを指定した場合、それは呼び出し元に対して、それが単なるクライアントであってポインタの所有者ではないことを示しています。weak_ptr ポインタで bar() を実装すると、以下のようになります。
#include <memory>
void foo()
{
std::shared_ptr<Album> album(new Album()); // ref counter = 1
bar(std::weak_ptr<Album>(album)); // ref counter = 1
} // ref counter = 0
void bar(std::weak_ptr<Album> barAlbum)
{
qDebug() << "Album name" << barAlbum->name();
} // ref counter = 1
ここで話が止まってしまうと、weak_ptr対生ポインタを使うことに興味がなくなってしまいます。weak_ptr は、ダングリングポインタの問題に大きな利点を持っています。キャッシュを構築している場合、一般的にはオブジェクトへの強い参照を保持したいとは思いません。一方で、オブジェクトがまだ有効であるかどうかを知りたいと思います。weak_ptrを使用することで、オブジェクトがいつ解放されたかを知ることができます。次に、生のポインタのアプローチを考えてみましょう。ポインタは無効かもしれませんが、メモリの状態はわかりません。
C++14 で導入されたもう一つのセマンティックがあります。make_unique です。このキーワードは、new キーワードを置き換えて、例外が発生しない方法で unique_ptr オブジェクトを構築することを目的としています。このように使用されます。
unique_ptr<Album> album = make_unique<Album>();
make_uniqueキーワードは_new_キーワードをラップして、特にこの状況で例外セーフにします。
foo(new Album(), new Picture())
このコードは以下の順番で実行されます。
- アルバム関数を割り当てて構築します。
- ピクチャ関数を割り当てて構築します。
- foo()関数を実行する。
new Picture() が例外を投げると、new Album() が確保したメモリがリークしてしまいます。これは make_unique キーワードを使用することで修正されます。
foo(make_unique<Album>(), make_unique<Picture>())
make_unique キーワードは、unique_ptr ポインタを返します。C++標準委員会は、make_sharedという形でshared_ptrの等価なものも提供していますが、これも同じ原理に従っています。
これらの新しい C++ セマンティクスはすべて、new と delete を取り除こうと必死になっています。しかし、すべての unique_ptr や make_unique を書くのは面倒かもしれません。アルバムの作成では、autoキーワードが助けになってくれます。
auto album = make_unique<Album>()
これは、一般的なC++の構文からの根本的な出発です。変数の型は推論され、明示的なポインタはなく、メモリは自動的に管理されます。スマートポインタを使うようになってしばらくすると、コードの中で生ポインタを見る機会がどんどん減っていきます(そして、deleteの数が減っていくのはとても助かります)。残りの生ポインタは、単にクライアントがポインタを使用しているが、そのポインタを所有していないことを示しているだけです。
全体的に見ると、C++11 と C++14 のスマート ポインターは C++ コードの書き方の真のステップアップになります。それ以前は、コード ベースが大きくなればなるほど、メモリ管理に不安を感じていました。私たちの脳は、このようなレベルの複雑さを適切に把握するのが下手なだけなのです。スマートポインタは、単に自分が書いたものを安全に感じるようにしてくれます。一方で、メモリの完全な制御を保持します。パフォーマンスが重要なコードについては、いつでも自分でメモリを処理することができます。それ以外のすべての場合、スマートポインタはオブジェクトの所有権を明示的に示し、心を解放するエレガントな方法です。
これで、AlbumDao::Albums()関数の中のちょっと安全ではないスニペットを書き換えることができるようになりました。AlbumDao::Albums()をこのように更新します。
// In AlbumDao.h
std::unique_ptr<std::vector<std::unique_ptr<Album>>> albums() const;
// In AlbunDao.cpp
std::unique_ptr<std::vector<std::unique_ptr<Album> > > AlbumDao::albums() const
{
QSqlQuery query("SELECT * FROM albums", mDatabase);
query.exec();
unique_ptr<vector<unique_ptr<Album>>> list(new vector<unique_ptr<Album>>());
while (query.next()) {
unique_ptr<Album> album(new Album());
album->setId(query.value("id").toInt());
album->setName(query.value("name").toString());
list->push_back(move(album));
}
return list;
}
うわー!album()関数のシグネチャは、非常に奇妙なものになっています。 スマートポインタは、あなたの生活を楽にするためのものですよね?スマートポインタの大きなポイントをQtで理解するために分解してみましょう。コンテナの挙動です。
書き換えの最初の目的は、albumの作成を確実にすることでした。我々はalbumの明示的な所有者であるlistを望んでいます。これは、私たちのlist型(つまりalbums()の戻り値の型)をQVector<unique_ptr<Album>>に変更したでしょう。しかし、list型が返されると、その要素はコピーされます(覚えておいてください、以前に戻り値の型をQVector<Album>に定義したことを)。これを回避する自然な方法は、QVector<unique_ptr<Album>>* 型を返して Album 要素の一意性を保持することです。
見よ、ここに大きな痛みがあります。QVectorクラスはコピー演算子をオーバーロードします。したがって、list型が返されたときに、コンパイラはunique_ptr要素の一意性を保証することができず、コンパイルエラーが発生します。そのため、標準ライブラリからのvectorオブジェクトに頼ってlong型を書く必要があります。unique_ptr<vector<unique_ptr<Album>>>
Info
Qt コンテナの unique_ptr ポインタのサポートに関する公式の回答を見てみましょう。疑いの余地を超えて明らかになっています。http://lists.qt-project.org/pipermail/interest/2013-July/007776.html。短い答えは: いや、それは決して行われないだろう。それについて言及することさえしないでください。絶対に。
この新しい albums() の署名を平易な英語に翻訳すると、次のようになります。 album() 関数は Album のベクタを返します。このベクトルは、それが含む Album の要素の所有者であり、あなたがそのベクタの所有者となります。
この albums() の実装をカバーするために、list 宣言に auto と make_unique キーワードを使用していないことに気づくかもしれません。私たちのライブラリは第 5 章「モバイル UI を支配する」でモバイルで使用されますが、C++14 はこのプラットフォームではまだサポートされていません。そのため、コードをC++11に制限する必要があります。
また、list->push_back(move(album))という命令の中で、move関数の使用にも遭遇します。その行まで、albumはwhileスコープによって “所有 “され、移動はリストに所有権を与えます。最後の命令である return list では、move(list) と書くべきでしたが、C++11 は直帰を受け入れ、自動的に move() 関数を作成してくれます。
ここで取り上げたのは、AlbumDaoクラスがPictureDaoで完全にマッチしているということです。PictureDaoクラスの完全一致実装については、本章のソースコードを参照してください。