閉じる

【C++】vectorの使い方:所有権の移動と注意点

C++におけるメモリ管理は、プログラムの安全性とパフォーマンスに直結する極めて重要なテーマです。

かつては生のポインタ(Raw Pointer)を使用して動的なメモリ確保を行っていましたが、現代のC++では「所有権」の概念を明確にするスマートポインタの利用が標準となっています。

その中でも、一つのオブジェクトを一人が所有するstd::unique_ptrstd::vectorで管理する手法は、メモリリークを防ぎつつ柔軟なデータ構造を構築するための強力な武器となります。

この記事では、std::vector<std::unique_ptr<T>>の基本から、所有権の移動、そして実務で陥りやすい注意点までを詳しく解説します。

vector<unique_ptr>の基本概念

まず、なぜstd::vectorの中にstd::unique_ptrを入れる必要があるのか、そのメリットと基本的な構造を理解しましょう。

生のポインタとの違い

従来のstd::vector<T*>では、vectorが破棄されたとしても、中身のポインタが指し示すメモリは自動で解放されません。

そのため、手動でdeleteを呼び出す必要があり、これがメモリリークの大きな原因となっていました。

一方、std::vector<std::unique_ptr<T>>を使用すると、vector自体の寿命が尽きたときや、要素が削除されたときに、管理されているオブジェクトのメモリも自動的に解放されます

これがRAII(Resource Acquisition Is Initialization)と呼ばれるC++の強力な設計パターンです。

unique_ptrの特徴と制限

std::unique_ptrは「唯一の所有権」を持つポインタです。

そのため、以下の特性があります。

特徴内容
コピー禁止他のポインタにコピーすることはできません。
移動可能std::moveを使用して所有権を譲渡できます。
自動解放スコープを抜けると自動的にdeleteされます。
オーバーヘッド生のポインタとほぼ同等のパフォーマンス(サイズも同じ)です。

この「コピー禁止」という制約が、std::vectorで扱う際の最大のポイントとなります。

要素の追加と所有権の移動

std::vectorunique_ptrを追加する場合、通常のオブジェクトのようにコピーして追加することはできません。

必ず所有権を移動(ムーブ)させる必要があります。

push_backとstd::moveの利用

インスタンス化したunique_ptrpush_backで追加する例を見てみましょう。

C++
#include <iostream>
#include <vector>
#include <memory>
#include <string>

class MyClass {
public:
    std::string name;
    MyClass(std::string n) : name(n) {
        std::cout << name << " が生成されました\n";
    }
    ~MyClass() {
        std::cout << name << " が破棄されました\n";
    }
};

int main() {
    // vectorの宣言
    std::vector<std::unique_ptr<MyClass>> vec;

    // 1. std::make_uniqueで直接追加(推奨)
    vec.push_back(std::make_unique<MyClass>("Item 1"));

    // 2. 一度変数に受けてから追加(std::moveが必要)
    auto ptr = std::make_unique<MyClass>("Item 2");
    
    // vec.push_back(ptr); // コンパイルエラー!コピーはできない
    vec.push_back(std::move(ptr)); // 所有権をmoveして追加

    if (!ptr) {
        std::cout << "ptr は move されたため空になりました\n";
    }

    return 0;
}
実行結果
Item 1 が生成されました
Item 2 が生成されました
ptr は move されたため空になりました
Item 1 が破棄されました
Item 2 が破棄されました

emplace_backによる直接生成

emplace_backを使用すると、unique_ptrのコンストラクタ引数を直接渡すことができ、コードをより簡潔に記述できます。

C++
// emplace_backを利用した追加
// 内部で std::unique_ptr<MyClass>(new MyClass("Item 3")) が呼ばれるイメージ
vec.emplace_back(std::make_unique<MyClass>("Item 3"));

ただし、unique_ptrの場合は結局std::make_uniqueの結果を渡すことが多いため、push_backemplace_backで劇的な差が出るわけではありません。

要素へのアクセスと操作

vector内の要素にアクセスする際も、所有権を意識する必要があります。

基本的には「参照」または「生のポインタ」として扱います。

参照によるアクセス

ループで要素を回す場合は、const auto&auto&を使用します。

値渡し(コピー)はできないため、参照を使わないとコンパイルエラーになります。

C++
// 参照を使ってアクセス
for (const auto& item : vec) {
    std::cout << "名前: " << item->name << std::endl;
}

生のポインタの取得(get関数)

関数の引数などで「所有権は渡さないが、中身を利用したい」という場合は、get()メソッドを使用して生のポインタを渡すのが一般的です。

C++
void printName(MyClass* obj) {
    if (obj) {
        std::cout << "名前表示: " << obj->name << std::endl;
    }
}

// 呼び出し側
printName(vec[0].get());

このように、unique_ptr「所有」と「利用」を明確に分離できる点が優れています。

要素の削除とメモリ解放

vectorから要素を削除すると、その要素が保持していたオブジェクトも即座に解放されます。

特定の要素を削除する

C++
// 最初の要素を削除
vec.erase(vec.begin());
// この時点で「Item 1 が破棄されました」と出力される

std::vectoreraseメソッドやpop_backclearを呼び出すだけで、面倒なdelete処理を意識することなく安全にメモリを管理できます。

多態性(ポリモーフィズム)への活用

vector<unique_ptr>が最も真価を発揮する場面の一つが、派生クラスのオブジェクトを基底クラスのポインタで一括管理する場合です。

継承関係を利用した管理

インターフェースや基底クラスを持つ設計において、異なる派生クラスを一つのvectorで保持できます。

C++
#include <iostream>
#include <vector>
#include <memory>

class Shape {
public:
    virtual void draw() const = 0;
    virtual ~Shape() = default; // 仮想デストラクタを忘れずに!
};

class Circle : public Shape {
public:
    void draw() const override { std::cout << "まるを描く\n"; }
};

class Square : public Shape {
public:
    void draw() const override { std::cout << "しかくを描く\n"; }
};

int main() {
    std::vector<std::unique_ptr<Shape>> shapes;

    shapes.push_back(std::make_unique<Circle>());
    shapes.push_back(std::make_unique<Square>());

    for (const auto& shape : shapes) {
        shape->draw();
    }

    return 0;
}
実行結果
まるを描く
しかくを描く

この方法なら、vectorが破棄される際に各派生クラスのデストラクタが正しく呼び出され、リソースの解放が保証されます。

陥りやすい罠と注意点

非常に便利なvector<unique_ptr>ですが、初心者がハマりやすいポイントがいくつかあります。

vector自体のコピーは禁止

最も多いエラーは、vectorを別のvectorに代入しようとすることです。

C++
std::vector<std::unique_ptr<int>> vec1;
vec1.push_back(std::make_unique<int>(10));

// std::vector<std::unique_ptr<int>> vec2 = vec1; // コンパイルエラー!

vectorをコピーしようとすると、その中身(要素)もコピーしようとします。

しかし、unique_ptrはコピーできないため、vector全体のコピーも不可能になります。

もし別のvectorに中身を移したい場合は、やはりstd::moveを使う必要があります。

C++
std::vector<std::unique_ptr<int>> vec2 = std::move(vec1); // OK: vec1は空になる

アルゴリズム関数の制限

std::sortなどの一部のアルゴリズムは、内部で要素の入れ替え(スワップ)を行うため動作しますが、要素のコピーを前提としたアルゴリズム(例:std::copy)は使用できません。

ソートの例

C++
#include <algorithm>

// 値に基づいてソートする例
std::sort(vec.begin(), vec.end(), [](const auto& a, const auto& b) {
    return a->value < b->value; // unique_ptrが指す先を比較
});

このように、述語(比較関数)を適切に定義すれば、std::sortはムーブセマンティクスを利用して要素を並び替えてくれます。

パフォーマンスとメモリの観点

unique_ptrvectorに格納する場合、メモリの配置についても理解しておくと役立ちます。

項目詳細
メモリの連続性unique_ptr(ポインタ自体)はメモリ上に連続して並びます。
オブジェクトの場所指し示す先のオブジェクトはヒープ領域のあちこちに分散する可能性があります。
キャッシュ効率オブジェクト自体をvector<T>で持つよりは低下しますが、ポインタの走査自体は高速です。

大量の小さなオブジェクトを扱う場合、unique_ptrを介すとキャッシュミスが増える可能性があるため、パフォーマンスが極めて重要な場面ではstd::vector<T>(実体の保持)を検討すべきです。

しかし、ポリモーフィズムが必要な場合や、オブジェクトのサイズが非常に大きく移動コストが高い場合には、vector<unique_ptr>が最適な選択となります。

モダンなC++での実例:C++20以降

C++20以降では、std::rangesの導入により、より直感的に記述できるようになりました。

C++
#include <ranges>
#include <algorithm>

// C++20のrangesを使ったソート
std::ranges::sort(vec, {}, [](const auto& p) { return p->name; });

また、std::erase_if(C++20)を使用すると、特定の条件に合致する要素を安全かつ簡単に削除できます。

C++
// 名前が空の要素を削除
std::erase_if(vec, [](const auto& ptr) {
    return ptr->name.empty();
});

これにより、以前のような「erase-removeイディオム」を使わずに済み、コードの可読性が大幅に向上します。

まとめ

std::vector<std::unique_ptr<T>>は、現代的なC++開発において避けては通れない非常に重要なパターンです。

本記事の重要ポイント:

  • 所有権の自動管理: 手動のdeleteが不要になり、メモリリークを根絶できる。
  • ムーブセマンティクスの必須性: コピーができないため、追加や移動には必ずstd::moveを使用する。
  • 多態性のサポート: 基底クラスのunique_ptrを使うことで、派生クラスを安全に一括管理できる。
  • アクセスのルール: 所有権を奪わないよう、基本的には参照(&)や生のポインタ(get())で利用する。

これらの特性を正しく理解して使いこなすことで、堅牢でメンテナンス性の高いC++プログラムを記述することが可能になります。

まずは小さなプロジェクトから、生のポインタをunique_ptrに置き換えるところから始めてみてください。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!