「C++の基礎 - スマートポインタ(unique ptr)」の版間の差分
219行目: | 219行目: | ||
<code>std::vector</code>クラスを使用する方法は、より柔軟性が高く、一般的に推奨される。<br> | <code>std::vector</code>クラスを使用する方法は、より柔軟性が高く、一般的に推奨される。<br> | ||
一方で、<code>std::unique_ptr</code>クラスを使用する方法は、パフォーマンスが重要で、配列サイズが固定の場合に適している。<br> | 一方で、<code>std::unique_ptr</code>クラスを使用する方法は、パフォーマンスが重要で、配列サイズが固定の場合に適している。<br> | ||
<br> | |||
==== 例 : Linuxのグループの取得 ==== | |||
<syntaxhighlight lang="c++"> | |||
#include <iostream> | |||
#include <vector> | |||
#include <string> | |||
#include <cstring> | |||
#include <sstream> | |||
#include <memory> | |||
#include <grp.h> | |||
#include <pwd.h> | |||
#include <unistd.h> | |||
#include <sys/types.h> | |||
#include <errno.h> | |||
std::vector<std::string> getUserGroups(std::string& errorMessage) | |||
{ | |||
std::vector<std::string> groups; | |||
errorMessage.clear(); | |||
uid_t uid = getuid(); | |||
struct passwd *pw = getpwuid(uid); | |||
if (!pw) { | |||
std::stringstream ss; | |||
ss << "Failed to get user info: " << strerror(errno); | |||
errorMessage = ss.str(); | |||
return groups; | |||
} | |||
int ngroups = 0; | |||
if (getgrouplist(pw->pw_name, pw->pw_gid, nullptr, &ngroups) == -1) { | |||
errorMessage = "Failed to get group list size"; | |||
return groups; | |||
} | |||
if (ngroups > 0) { | |||
auto gids = std::make_unique<gid_t[]>(ngroups); | |||
if (getgrouplist(pw->pw_name, pw->pw_gid, gids.get(), &ngroups) == -1) { | |||
errorMessage = "Failed to get group list"; | |||
return groups; | |||
} | |||
for (int i = 0; i < ngroups; i++) { | |||
errno = 0; | |||
struct group *gr = getgrgid(gids[i]); | |||
if (gr) { | |||
groups.push_back(gr->gr_name); | |||
} | |||
else { | |||
std::cerr << "Failed to get group name for gid " << gids[i] << ": " << strerror(errno) << std::endl; | |||
} | |||
} | |||
} | |||
else { | |||
errorMessage = "User is not a member of any groups"; | |||
} | |||
return groups; | |||
} | |||
</syntaxhighlight> | |||
<br><br> | <br><br> | ||
2024年7月28日 (日) 20:52時点における最新版
概要
C++11では、unique_ptr<T>、shared_ptr<T>、weak_ptr<T>の3種のスマートポインタが追加された。
これらのスマートポインタは、メモリの動的確保の利用の際に生じる多くの危険性を低減する目的で使用されるが、
それぞれ独自の考え方と機能を持っている。
3種のスマートポインタを適切に使い分けることで、安全性と開発速度の向上が見込めるだけでなく、
プログラマの意図に合わせてポインタを記述し分けることができる、非常に強力なツールとなる。
ここでは、スマートポインタについて初めて学ぶ人を対象に、
C++11で追加された3種のスマートポインタの機能と使い方、および3種をどのように考えて使うかについて、初歩的な解説を行う。
unique_ptrとは
unique_ptr<T>は、あるメモリに対する所有権を持つポインタが、ただ一つであることを保証するようなスマートポインタである。
テンプレート引数で保持するポインタ型を指定し、スマートポインタが破棄される際にディストラクタにおいて自動的にメモリを開放する。
unique_ptr<T>は、以下の様な特徴を持っている。
・あるメモリの所有権を持つ unique_ptr<T>は、 ただ一つのみである。
・コピーが出来ない。代わりに、C++11で新たに追加されたムーブによって、所有権を移動することができる。
・通常のポインタに匹敵する処理速度。
・配列を扱うことができる。
・deleter(後述)を指定することができる。
#include<iostream>
#include<memory> // スマートポインタを使用する時に指定する
class hoge
{
private:
std::unique_ptr<int> ptr;
public:
hoge(int val_) : ptr(new int(val_)){}
int getValue()const{return *ptr;}
};
int main()
{
// hogeのコンストラクタでint型を動的に確保しunique_ptrに委ねる
hoge Hoge(10);
// コンストラクタの引数として、動的確保したメモリのアドレスを指定
std::unique_ptr<int> ptr(new int(10));
// reset関数を使用して後から代入できる
std::unique_ptr<int> ptr2;
ptr2.reset(new int(10));
// C++14以降では、make_unique関数が使用できる
std::unique_ptr<int> ptr3 = std::make_unique<int>(10);
// unique_ptrはコピーコンストラクタで作成しようとすると、コンパイルエラーになる
//hoge Hoge2(Hoge);
// 明示的にmoveするならOK
hoge Hoge2(std::move(Hoge));
return 0;
}
unique_ptrの使い方
詳しい使い方を下記に示す。
尚、使用の際には#include <memory>を指定する必要がある。
// まず、unique_ptr<T>にメモリの所有権を委ねるには、コンストラクタで指定するかreset(pointer)を使う。
// C++14以降では、make_unique<T>関数を使って作成することができる。
// コンストラクタの引数として、動的確保したメモリのアドレスを指定
std::unique_ptr<int> ptr(new int(10));
// reset関数を使って、後から代入することもできる
std::unique_ptr<int> ptr2;
ptr2.reset(new int(10));
// C++14以降であれば、make_unique関数を使うこともできる
std::unique_ptr<int> ptr3=std::make_unique<int>(10);
// unique_ptr<T>は、コピーは禁止されているが、move関数は使用することができる。
std::unique_ptr<int> ptr(new int(10));
// コピーコンストラクタや、コピー代入演算子はエラー
//std::unique_ptr<int> ptr2(ptr); //===ERROR===
//std::unique_ptr<int> ptr2;
//ptr2 = ptr;
// ムーブコンストラクタや、ムーブ代入演算子はOK
// この時、所有権が移動する
std::unique_ptr<int> ptr3(std::move(ptr)); // ptrの所有権がptr3に移動する
std::unique_ptr<int> ptr4;
ptr4 = std::move(ptr3); // ptr4の所有権がptr5に移動する
// メモリの解放は、ディストラクタや reset(pointer)を使う。
// 引数なしやnullptrを引数としてreset関数を呼んでも、明示的に解放できる
std::unique_ptr<int> ptr(new int(10));
ptr.reset();
// 生のポインタが欲しい時は、get()かrelease()を使う
// get()は、生ポインタを得るだけで、ポインタの所有権はunique_ptr<T>が保持し続ける
int *pint;
pint = ptr.get();
// release()は、ポインタの所有権自体も放棄するため、メモリの開放は自分で行う必要がある
pint = ptr.release();
delete pint;
// unique_ptr<T[]>のように指定すれば、配列を扱うこともできる
// 配列型の場合、operator[](size_t)を使用することができる
// int型配列の要素数10を宣言
std::unique_ptr<int[]> ptrArray(new int[10]);
// または
std::unique_ptr<int[]> ptrArray = std::make_unique<int[]>(10);
for(int i = 0; i < 10; i++)
{
ptrArray[i] = i;
}
// ポインタの保持するメモリにアクセスするには、通常のポインタ同様に、operator*()やoperator->()が使用できる
std::unique_ptr<std::string> pStr(new std::string("test"));
// operator*()でstring型を参照(testと表示される)
std::cout << *pStr << std::endl;
// operator->()で、string型のsize関数を呼び出せる
unsigned int StrSize = pStr->size();
スマートポインタの使いどころ
関数の引数
関数を通して所有権を渡す時は、std::unique_ptr
またはstd::shared_ptr
を使用する。
std::unique_ptr
は完全に所有権を譲渡、std::shared_ptr
は共有する。
それ以外は、生ポインタ(または参照)を使用する。
関数の戻り値
ファクトリ関数(オブジェクトを生成する関数)の戻り値は、原則、std::unique_ptr
を使用する。
std::shared_ptr
で扱いたい場合、受け取ったstd::unique_ptr
をstd::shared_ptr
に代入するとよい。
(std::shared_ptr
のコンストラクタにおいて、std::unique_ptr&&
を取るものが存在する)
// factory methodで作成したオブジェクト
std::unique_ptr<Foo> CreateFoo();
// std::unique_ptrをstd::shared_ptrに代入する
std::shared_ptr<foo> sp = CreateFoo();
オブジェクト内のメンバへのポインタを返す場合等は、生ポインタまたは参照でよい。
変更が不要な場合、constキーワードを付加すること。
また、オブジェクトの生存期間には注意が必要である。
class Foo
{
private:
Bar bar;
public:
const Bar& GetBar() const { return bar; }
};
クラスのメンバ変数
所有権を持っている場合、std::unique_ptr
またはstd::shared_ptr
を使用する。
所有権を持っていない場合、生ポインタまたは参照を使用する。
この時、参照しているオブジェクトの生存期間に注意が必要である。
生存期間の制御が難しい場合、std::weak_ptr
を使用することもある。
std::shared_ptr
とstd::weak_ptr
は必要な場合のみ使用する。
std::shared_ptr
を使用することで、オブジェクトの生存期間の制御が難しくなる。
同様に、std::weak_ptr
も無闇に使用しない。
複数の場所で使用されるオブジェクトの生存期間を制御することが難しい場合や、循環参照の解決には便利である。
また、所有権を持たないオブジェクトへの参照は全てstd::weak_ptr
にする、といったことは行わないこと。
スマートポインタのコスト
std::unique_ptr
は、オーバーヘッドはほぼ無い。
メモリ使用量、コードサイズ、実行速度の全てにおいて、生ポインタをnew
およびdelete
する場合に負けることはない。
std::shared_ptr
およびstd::weak_ptr
は、オーバーヘッドがある。
コントロールブロックと呼ばれる別のメモリ領域がヒープから取られることで、格納するオブジェクトと合わせて2回のメモリ確保が行われる。
また、コピー時には、リファレンスカウントの操作が行われる。
これは、オブジェクトの生成・破棄に比べれば遥かに軽い操作であるが、スレッドセーフにするために排他されていたりもするので0ではない。
std::vectorクラスとの比較
std::vector
クラスとstd::unique_ptr
クラスにおいて、両方のアプローチにはそれぞれ利点がある。
- std::vectorを使用する方法
- サイズの動的な変更が容易である。
- STLアルゴリズムとの互換性が高い。
- メモリ割り当てと解放が自動的に行われる。
- std::unique_ptrを使用する方法
- 生の配列に近い性能を維持しつつ、安全なメモリ管理が可能である。
- サイズが固定の場合に適している。
どちらの方法も、以前の実装と比べてメモリリークのリスクが低く、より現代的なC++のスタイルに則っている。
プロジェクトの要件や好みに応じて、適切な方法を選択すること。
std::vector
クラスを使用する方法は、より柔軟性が高く、一般的に推奨される。
一方で、std::unique_ptr
クラスを使用する方法は、パフォーマンスが重要で、配列サイズが固定の場合に適している。
例 : Linuxのグループの取得
#include <iostream>
#include <vector>
#include <string>
#include <cstring>
#include <sstream>
#include <memory>
#include <grp.h>
#include <pwd.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
std::vector<std::string> getUserGroups(std::string& errorMessage)
{
std::vector<std::string> groups;
errorMessage.clear();
uid_t uid = getuid();
struct passwd *pw = getpwuid(uid);
if (!pw) {
std::stringstream ss;
ss << "Failed to get user info: " << strerror(errno);
errorMessage = ss.str();
return groups;
}
int ngroups = 0;
if (getgrouplist(pw->pw_name, pw->pw_gid, nullptr, &ngroups) == -1) {
errorMessage = "Failed to get group list size";
return groups;
}
if (ngroups > 0) {
auto gids = std::make_unique<gid_t[]>(ngroups);
if (getgrouplist(pw->pw_name, pw->pw_gid, gids.get(), &ngroups) == -1) {
errorMessage = "Failed to get group list";
return groups;
}
for (int i = 0; i < ngroups; i++) {
errno = 0;
struct group *gr = getgrgid(gids[i]);
if (gr) {
groups.push_back(gr->gr_name);
}
else {
std::cerr << "Failed to get group name for gid " << gids[i] << ": " << strerror(errno) << std::endl;
}
}
}
else {
errorMessage = "User is not a member of any groups";
}
return groups;
}