web-dev-qa-db-ja.com

raw、weak_ptr、unique_ptr、shared_ptrなど...それらを賢く選択する方法は?

C++には多くのポインターがありますが、C++プログラミング(特にQtフレームワーク)では5年ほど正直に言うと、古いrawポインターのみを使用します。

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回目は、雇用主がコードを読む場合、彼は私が使用するポインターの種類に厳格になるでしょうか?

35
CheshireChild

「生の」ポインタはunmanagedです。つまり、次の行:

_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
}
_

そのような_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を削除すると、それへの循環参照があります。参照カウントがゼロになることはないため、deletedになることはありません。

この問題を解決するには、 _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<>_は、参照先のオブジェクトから遠く離れた参照カウントを保持します。これはcanは2番目の割り当てを意味します)。それは通常望ましくありません。

この問題を解決するために、C++ 11は std::make_shared<>() を提供し、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には独自のガベージコレクションモデルがあることに注意してください。多くのQObjectsには、必要なく適切に破棄されるメカニズムがあります。ユーザーにdeleteしてもらいます。

C++ 11マネージポインターで管理されている場合のQObjectsの動作がわからないので、_shared_ptr<QDialog>_が適切であるとは言えません。確かに言うにはQtの十分な経験がありませんが、私はbelieve Qt5がこの使用例に合わせて調整されていることを確信しています。

72
greyfade