c++ オタク向けの話です。
is-A 関係の shared_ptr (継承子クラスの話) は出てくるのに has-A が出てこないのでメモ。
-
std::shared_ptr<C> cptr
がある。 - C クラスにはメンバ型 M の ポインタをくれるような関数がある。
- こいつを使って
std::shared_ptr<M> mptr
を受ける関数を呼びたい。 -
mptr
がいつ消えるかはわからないので、mptr
が消えるまでcptr
をキープしたい。
まずは書いて見るわけです。
#include <memory>
#include <iostream>
using M = int;
class C {
public:
C(M m) : m_(m) { std::cout << "Hey I'm C, created " << m_ << std::endl; }
~C() { std::cout << "Hey I'm C, deleted " << m_ << std::endl; }
M *GetMPointer() { return &m_; }
private:
M m_;
};
int main() {
std::shared_ptr<M> mptr;
{
std::shared_ptr<C> cptr = std::make_shared<C>(3);
mptr = std::shared_ptr<M>(cptr->GetMPointer()); // Wrong!
} // ここで cptr が消滅
std::cout << "M value is " << *mptr << std::endl;
return 0;
}
実行すると死ぬ。というか環境によっては死なないかもしれないが use-after-free のあと double delete してる。
わからなければ -fsanitize=address -O0 -g
つけてコンパイルしてみるとよい。
cptr が消滅するところで C のデストラクタが呼ばれて、そのあと mptr はありえない場所を指している。
明らかに // Wrong!
の行がまずい。own してないポインタから shared_ptr を作ってはいけません。
ついついここで
mptr = std::make_shared<M>(*cptr->GetMPointer()); // Not wrong, but..
とかやってしまいがちだが、これは M のコピーを作っている。
コピーを作っていいんなら別にいいし、そのほうが絶対いいと思いますが、コピーを作りたくないこともあるわけです。
delete の部分を置き換えたければcustom deleter を使うという手があります。
cptr が必ず mptr を outlive することが保証されているなら empty deleter を渡せばいいわけです。
lambda が使えるようになって empty deleter が簡単になりました。
mptr = std::shared_ptr<M>(cptr->GetMPointer(), [](M *){});
これで、mptr が削除されるときに delete が呼ばれなくなります。
ですが、ただ double delete されないというだけで、上の例で削除後のポインタにアクセスしていることはかわりません (cptr より mptr が長生きしてるから)。こういうことをしているといつか変なメモリエラーを起こしてデバッグで一日消えます。
問題はいつまで mptr が生きているかわからないので、それまで cptr を生かしておきたいわけです。
更新 10/23
コメントで正しい指摘が来たので... shared_ptr のコンストラクタでそのものずばりの機能がありました。
mptr = std::shared_ptr(cptr, cptr->GetMPointer());
とすれば、所有権を共有する shared_ptr が作れます。検索してもなかなか出てこない (検索キーワードがないんですよ) ので気づきませんでした。
@yuki12 さんありがとうございました。解決。
オリジナル
まあもとの文章も間違っているわけではない (まわりくどいだけ) ので、以下に残しておきます。
実はこれも custom deleter + lambda で解決するのでした。
mptr = std::shared_ptr<M>(cptr->GetMPointer(), [cptr](M *){});
なんでこれが動くのか一見よくわかりません。渡しているのは、キャプチャがある空の lambda 関数です。
種明かしすると、deleter も仕事が終わったらデリートされるわけです。この deleter は cptr を値渡しでキャプチャしてるので、deleter の中で shared_ptr の変数が一個作られて、cptr のコピーを保持しています。関数本体では使ってないんですが、この deleter が終わったときに cptr のコピーが削除されてくれるわけです。
問題の行を最後の例に変えて実行すると、ほらちゃんと M の値を使ってから C が delete されているのが見えます。
Hey I'm C, created 3
M value is 3
Hey I'm C, deleted 3
以上。