78
75

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

C++ オブジェクトのコレクション

Posted at

最近、数年ぶりに C++ を書いてますが、C++11 すてきですね。ってことで、C++ オブジェクトをコレクション (std::vector など) に格納する方法について書いてみます。なお、ここで言うオブジェクトとは、POD でないクラスのインスタンスを指します。

(C++ の記事は、自作クラスの作り方と、コレクションの使い方と、スマートポインタの使い方を、それぞれ別々に記述したものが多いけど、それらを適切に組み合わせる方法の方が重要だと思ってます。)

オブジェクトを「そのまま」格納する?

C++ において、(POD でない) オブジェクトを「そのまま」取り扱うなどということは幻想に過ぎません。常に T, T*, T&, const T&, T&& の違いを意識する必要があります。

コレクションには参照 (T&) は格納できないので、値またはポインタで格納することになりますが、目的に応じて生のポインタ (T*) とスマートポインタ (std::unique_ptr および std::shared_ptr) を使い分けることが重要になります。

オブジェクトのコピーを格納する

std::vector<T> v; と書いた場合、POD ではないオブジェクトに関しては、値そのものを格納するのではなく、オブジェクトのコピーを格納すると考えるべきです。

v.emplace_back(引数); などと書くことで、コレクションの中に直接 (コピーではなく) オブジェクトを生成できるように見えますが、コレクションのリサイズなどの際にオブジェクトが移動されるので、やはりオブジェクトのコピーを格納しているという意識が重要です。

特に、デストラクタで資源を解放する場合、コピーコンストラクタで資源をコピーしたり、資源をコピーできない (すべきでない) 場合にはコピーコンストラクタを禁止して、適切なムーブコンストラクタを書く必要があります。

#include <vector>

// 管理する資源
typedef int ResourceId;
constexpr ResourceId INVALID_ID = 0;
ResourceId acquireResource(const char* name);
void releaseResource(ResourceId id);

// よくない例 (暗黙のコピーコンストラクタを持つ)
class Bad {
public:
    Bad(const char* name) : id(acquireResource(name)) {}
    ~Bad() { releaseResource(id); }

private:
    ResourceId id;
};

std::vector<Bad> v1;
v1.emplace_back("abc"); // コレクションの中に直接オブジェクトを生成してるので、コピーは発生しない筈?
v1.emplace_back("def"); // 複数オブジェクトを生成すると、コレクション内部でオブジェクトの移動が発生し、コピーコンストラクタ (id をコピー) とデストラクタ (資源を削除!) が呼ばれる可能性がある

// 良い例 (コピーコンストラクタを禁止し、ムーブコンストラクタを書く)
class Good {
public:
    Good(const char* name) : id(acquireResource(name)) {}
    Good(const Good&) = delete;
    Good(const Good&& other) : id(other.id) { other.id = INVALID_ID; }
    ~Good() { if (id != INVALID_ID) releaseResource(id); }

    // もちろん、代入演算子も禁止して、ムーブ代入演算子も書く (または禁止する) べき。

private:
    ResourceId id;
};

std::vector<Good> v2;
v2.emplace_back("xyz"); // オブジェクトの移動にはムーブコンストラクタが使われるので、いくつ追加しても大丈夫

(なお、ムーブ後にもデストラクタは呼ばれるので、無効を表す値が必要になります。)

生のポインタを格納する

コレクションの外側でオブジェクトの寿命を管理する場合、生のポインタ (T* または const T*) を格納するという選択肢もあります。

例えば以下の Body クラスの場合、parts の各要素は Body に埋め込まれているので、コレクション側で各要素の寿命を管理する必要はなく、生のポインタでも問題ありません。

#include <vector>

// 身体の部品を表すクラス群
class Part { /* ... */ };
class Head : public Part { /* ... */ };
class Arm : public Part { /* ... */ };
class Leg : public Part { /* ... */ };

// 身体クラス
class Body {
private:
    // 身体の部品を埋め込み
    Head head;
    Arm rightArm, leftArm;
    Leg rightLeg, leftLeg;

    // 身体の部品 (へのポインタ) のリスト
    std::vector<Part*> parts = { &head, &rightArm, &leftArm, &rightLeg, &leftLeg };
};

std::unique_ptr を格納する

コピーコンストラクタが存在したため所有権が曖昧になる可能性があった std::auto_ptr とは異なり、std::unique_ptr コピーコンストラクタが禁止され、ムーブコンストラクタが提供されているので、安心してコレクションに格納できます。

#include <vector>
#include <memory>
#include <stdio.h>

class Foo {
public:
    Foo(int n) : no(n) {}
    void print() { printf("Foo(%d)\n", no); }

private:
    int no;
};

int main()
{
    std::vector<std::unique_ptr<Foo>> v;
    Foo* f = new Foo(1);
    v.push_back(std::unique_ptr<Foo>(f));
    v.push_back(std::unique_ptr<Foo>(new Foo(2)));
    v.emplace_back(new Foo(3));
    //auto p = std::unique_ptr<Foo>(new Foo(4));
    //v.push_back(p); // これは std::unique_ptr のコピーになるので、コンパイルエラー

    for (const auto& f : v) { // なお、for (auto f : v) とすると f は std::unique_ptr のコピーになるので、コンパイルエラー
        f->print(); // ちなみに、f は const std::unique_ptr<Foo>& であり、Foo に関しては const ではない
    }
}

とはいえ、一度 std::unique_ptr にしてしまったら、後で必ず delete されるので、自分で delete したり、delete できないものを格納してはいけません。

#include <memory>

class Foo { /* ... */ };

int main()
{
    Foo f;
    auto p = std::unique_ptr<Foo>(&f); // コンパイルエラーにならないが、自動変数 f (へのポインタ) が delete されてしまう
}

std::shared_ptr を格納する

同じオブジェクトを指す std::unique_ptr は同時に複数のコレクションに格納することはできませんが、std::shared_ptr を使えば大丈夫です。

#include <vector>
#include <memory>

class Foo { /* ... */ };

int main()
{
    std::vector<std::shared_ptr<Foo>> v1;
    std::vector<std::shared_ptr<Foo>> v2;

    auto p = std::make_shared<Foo>( /* ... */ );
    v1.push_back(p);
    v2.push_back(p);
}

さらに、std::shared_ptr にはカスタムデリータを指定できるので、何もしないカスタムデリータを指定すれば delete できないものを格納することもできます。

#include <vector>
#include <memory>

class Foo { /* ... */ };

int main()
{
    Foo f;
    std::vector<std::shared_ptr<Foo>> v;
    //v.push_back(std::shared_ptr<Foo>(&f)); // コンパイルエラーにならないけど、やっちゃ駄目
    v.push_back(std::shared_ptr<Foo>(&f, [](Foo*){})); // これなら大丈夫 (厳密には Foo f; が v よりも後ろにあるとやばい気がするけど、実用上は問題にならないのでは…)
}

この際、カスタムデリータは個々の std::shared_ptr インスタンスごとに指定できるので、一つのコレクションの中に delete されるものとされないものを混在させることができます。

おまけ (std::initializer_list について)

これは当然オブジェクトにも使えます。

#include <initializer_list>
#include <stdio.h>

class Foo {
public:
    Foo(int n) : no(n) {}
    Foo(const Foo& f) : no(f.no + 1) {}
    void print() { printf("Foo(%d)\n", no); }

private:
    int no;
};

int main()
{
    Foo f1(100);
    Foo f2(200);
    Foo f3(300);

    for (auto f : { f1, f2, f3 }) { f.print(); } // コピーコンストラクタが 2 回ずつ呼ばれる
    for (auto f& : { f1, f2, f3 }) { f.print(); } // コピーコンストラクタが 1 回ずつ呼ばれる
    for (auto f* : { &f1, &f2, &f3 }) { f->print(); } // コピーコンストラクタは呼ばれない
}

共通の基底クラスを持つ異なるクラスのインスタンスを取り扱うには、明示的に std::initializer_list を指定してやる必要があるようです。残念。

#include <initializer_list>

// 身体の部品を表すクラス群
class Part { /* ... */ };
class Head : public Part { /* ... */ };
class Arm : public Part { /* ... */ };
class Leg : public Part { /* ... */ };

int main()
{
    Head head;
    Arm arm;
    Leg leg;

    for (auto& part : std::initializer_list<Part>({ head, arm, leg })) { /* ... */ } // スライシングされる
    for (auto* part : std::initializer_list<Part*>({ &head, &arm, &leg })) { /* ... */ } // 大丈夫
}
78
75
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
78
75

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?