raw、weak_ptr、unique_ptr、shared_ptrなど…賢明な選択方法は?


33

C ++には多くのポインタがありますが、C ++プログラミング(具体的にはQt Frameworkを使用)で5年ほど前に正直に言うと、古い生のポインタのみを使用します。

SomeKindOfObject *someKindOfObject = new SomeKindOfObject();

私は他にも多くの「スマート」ポインターがあることを知っています。

// shared pointer:
shared_ptr<SomeKindofObject> Object;

// unique pointer:
unique_ptr<SomeKindofObject> Object;

// weak pointer:
weak_ptr<SomeKindofObject> Object;

しかし、私はそれらをどうするか、生のポインタの比較で彼らが私に何を提供できるのかについて少しも考えていません。

たとえば、次のクラスヘッダーがあります。

#ifndef LIBRARY
#define LIBRARY

class LIBRARY
{
public:
    // Permanent list that will be updated from time to time where
    // each items can be modified everywhere in the code:
    QList<ItemThatWillBeUsedEveryWhere*> listOfUselessThings; 
private:
    // Temporary reader that will read something to put in the list
    // and be quickly deleted:
    QSettings *_reader;
    // A dialog that will show something (just for the sake of example):
    QDialog *_dialog;
};

#endif 

これは明らかに網羅的ではありませんが、これらの3つのポインターのそれぞれについて、それらを「生」のままにしておくことは問題ありませんか、より適切なものを使用する必要がありますか?

そして2度目に、雇用主がコードを読む場合、彼は私がどのような種類のポインターを使用するか厳しくなりますか?


このトピックはSOに非常に適しているようです。これは2008年でした。そして、ここで私はどの種類のポインターを使用しますか?。もっと良いマッチを見つけることができると確信しています。これらは私が最初に見たもの
でし

これは、これらのクラスの概念的な意味/意図に関するものであり、それらの動作と実装の技術的な詳細に関するものであるため、この境界線です。受け入れられた答えは前者に傾いているので、これがそのSO質問の「PSEバージョン」であることを嬉しく思います。
Ixrec

回答:


70

「生の」ポインタは管理されていません。つまり、次の行:

SomeKindOfObject *someKindOfObject = new SomeKindOfObject();

...付随するものdeleteが適切な時間に実行されない場合、メモリがリークします。

auto_ptr

これらのケースを最小限にするためstd::auto_ptr<>に導入されました。ただし、2011標準以前のC ++の制限により、auto_ptrメモリリークは非常に簡単です。ただし、次のような限られた場合には十分です。

void func() {
    std::auto_ptr<SomeKindOfObject> sKOO_ptr(new SomeKindOfObject());
    // do some work
    // will not leak if you do not copy sKOO_ptr.
}

最も弱いユースケースの1つはコンテナです。これは、のコピーauto_ptr<>が作成され、古いコピーが慎重にリセットされない場合、コンテナがポインタを削除してデータを失う可能性があるためです。

unique_ptr

代替として、C ++ 11が導入されましたstd::unique_ptr<>

void func2() {
    std::unique_ptr<SomeKindofObject> sKOO_unique(new SomeKindOfObject());

    func3(sKOO_unique); // now func3() owns the pointer and sKOO_unique is no longer valid
}

そのようなa unique_ptr<>は、関数間で渡された場合でも、正しくクリーンアップされます。これは、ポインタの「所有権」を意味的に表すことによってこれを行います-「所有者」がそれをクリーンアップします。これにより、コンテナでの使用に最適です。

std::vector<std::unique_ptr<SomeKindofObject>> sKOO_vector();

とは異なりauto_ptr<>unique_ptr<>ここでは行儀がよく、vectorサイズ変更時に、vectorバッキングストアのコピー中にオブジェクトが誤って削除されることはありません。

shared_ptr そして weak_ptr

unique_ptr<>確かに便利ですが、コードベースの2つの部分で同じオブジェクトを参照し、ポインターをコピーしながら、適切なクリーンアップを保証したい場合があります。たとえば、次を使用する場合、ツリーは次のようになりますstd::shared_ptr<>

template<class T>
struct Node {
    T value;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

この場合、ルートノードの複数のコピーを保持することもできます。ルートノードのすべてのコピーが破棄されると、ツリーは適切にクリーンアップされます。

これは、それぞれshared_ptr<>がオブジェクトへのポインターだけでなくshared_ptr<>、同じポインターを参照するすべてのオブジェクトの参照カウントも保持するために機能します。新しいものが作成されると、カウントが上がります。1つが破壊されると、カウントは減少します。カウントがゼロに達すると、ポインターはdeletedになります。

そのため、これにより問題が発生します。二重リンク構造は最終的に循環参照になります。parentツリーにポインタを追加するとしますNode

template<class T>
struct Node {
    T value;
    std::shared_ptr<Node<T>> parent;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

ここで、を削除するとNode、それへの循環参照があります。これは、ことは決してないだろうdeleteその参照カウントがゼロになることはありませんので、D。

この問題を解決するには、次を使用しstd::weak_ptr<>ます。

template<class T>
struct Node {
    T value;
    std::weak_ptr<Node<T>> parent;
    std::shared_ptr<Node<T>> left;
    std::shared_ptr<Node<T>> right;
};

現在、物事は正しく機能し、ノードを削除しても、親ノードへの参照がスタックしたままになりません。ただし、ツリーを歩くのは少し複雑になります。

std::shared_ptr<Node<T>> parent_of_this = node->parent.lock();

このようにして、ノードへの参照をロックできます。また、ノードを保持しているため、作業中にノードが消えないという合理的な保証がありますshared_ptr<>

make_shared そして make_unique

今、いくつかの小さな問題がshared_ptr<>あり、unique_ptr<>それを解決する必要があります。次の2行に問題があります。

foo_unique(std::unique_ptr<SomeKindofObject>(new SomeKindOfObject()), thrower());
foo_shared(std::shared_ptr<SomeKindofObject>(new SomeKindOfObject()), thrower());

場合はthrower()例外をスロー、両方のラインは、メモリリークが発生します。さらにshared_ptr<>、参照カウントを、それが指すオブジェクトから遠く離して保持します。これ、2番目の割り当てを意味します。それは通常望ましくありません。

この問題を解決するためにstd::make_shared<>()、C ++ 11が提供し、C ++ 14が提供std::make_unique<>()します。

foo_unique(std::make_unique<SomeKindofObject>(), thrower());
foo_shared(std::make_shared<SomeKindofObject>(), thrower());

これで、どちらの場合でもthrower()、例外がスローされても、メモリリークは発生しません。ボーナスとして、管理対象オブジェクトと同じメモリ空間にmake_shared<>()参照カウントを作成する機会があります。これにより、例外の安全性を保証しながら、高速で数バイトのメモリを節約できます。

Qtに関する注意

ただし、C ++ 11より前のコンパイラをサポートする必要があるQtには、独自のガベージコレクションモデルがあります。多くQObjectのsには、ユーザーがdeleteそれらを必要とせずに適切に破棄されるメカニズムがあります。

QObjectC ++ 11マネージポインターによって管理された場合のsの動作がわからないので、それshared_ptr<QDialog>は良いアイデアとは言えません。私は確かに言うことはQtとの十分な経験を持っていないが、私は信じてい Qt5は、このユースケースのために調整されていること。


1
@Zilators:Qtに関する追加コメントに注意してください。3つのポインターすべてを管理する必要があるかどうかについての質問に対する答えは、Qtオブジェクトが適切に動作するかどうかによって異なります。
-greyfade

2
「どちらもポインタを保持するために個別に割り当てます」?いいえ、unique_ptrは余分なものを割り当てません。shared_ptrのみがreference-count + allocator-objectを割り当てる必要があります。「両方の行がメモリをリークします」?いや、悪い振る舞いの保証すらしないかもしれません。
デデュプリケーター

1
@Deduplicator:私の言い回しは不明瞭だったに違いありません:edオブジェクトshared_ptrとは別のオブジェクト-別の割り当て- newです。それらは異なる場所に存在します。make_shared同じ場所にそれらをまとめる機能があり、特にキャッシュの局所性が向上します。
greyfade

2
@greyfade:ノノノ。shared_ptrオブジェクトです。オブジェクトを管理するには、(参照カウント(弱+強)+駆逐艦)オブジェクトを割り当てる必要があります。make_sharedこれと管理対象オブジェクトを1つのピースとして割り当てることができます。unique_ptrオブジェクトを使用しないため、対応する利点はありません。オブジェクトが常にスマートポインターによって所有されていることを確認します。余談です、基になるオブジェクトshared_ptr所有しnullptrnullpointerを所有せず、表現しないを表すことができます。
デュプリケータ

1
私はそれを見て、a shared_ptrが何をするのかについて一般的な混乱があるようです: 。2.ポインターが含まれています。これらの2つの部分は独立しています。make_uniqueそしてmake_shared両方のことを確認してください割り当てられたオブジェクトは、スマートポインタに安全に置かれます。さらにmake_shared、所有権オブジェクトとマネージポインターを一緒に割り当てることができます。
デデュプリケーター
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.