閉じる

【C++】std::make_uniqueの使い方|newとの違いやメリットを徹底解説

C++においてメモリ管理は非常に重要なテーマですが、手動でのnewやdeleteの操作はメモリリークや二重解放といったバグの原因になりやすいため、現代的なC++(モダンC++)では「スマートポインタ」の利用が推奨されています。

その中でも、所有権を独占的に管理するstd::unique_ptrを安全かつ簡潔に生成するための関数がstd::make_uniqueです。

本記事では、C++14で導入されたこの便利な関数の使い方から、new演算子と比較した際の圧倒的なメリット、そして実戦で役立つ注意点までを詳しく解説します。

C++におけるメモリ管理の重要性とmake_uniqueの役割

C++の歴史において、動的なメモリ確保は常に開発者を悩ませる要因の一つでした。

従来のnew演算子によるメモリ確保では、対応するdeleteを書き忘れるだけでメモリリークが発生してしまいます。

生ポインタからスマートポインタへの移行

かつてのC++では、ポインタを直接扱うことが一般的でしたが、コードが複雑になるにつれて「誰がこのメモリを解放する責任を持っているのか」が不明確になる問題がありました。

これを解決するために登場したのがRAII(Resource Acquisition Is Initialization)という考え方です。

リソースの確保をオブジェクトの初期化時に行い、破棄をデストラクタで行うこの手法を体現したのがスマートポインタです。

std::make_uniqueの登場背景

C++11でstd::unique_ptrが登場しましたが、当時はまだstd::make_uniqueが存在しませんでした。

そのため、エンジニアはstd::unique_ptr<Type>(new Type())のように記述していましたが、これには例外安全性における脆弱性が含まれていました。

C++14でstd::make_uniqueが追加されたことで、より安全に、そしてより短い記述でインスタンスを生成できるようになりました。

std::make_uniqueとは何か

std::make_uniqueは、std::unique_ptrのインスタンスを生成するためのヘルパー関数(ファクトリ関数)です。

基本的な定義

この関数は、指定された型のオブジェクトをヒープ領域に構築し、そのオブジェクトを管理するstd::unique_ptrを返します。

内部的には可変引数テンプレートを用いて、ターゲットとなる型のコンストラクタに引数を転送(パーフェクトフォワーディング)しています。

必要なヘッダ

std::make_uniqueを使用するには、<memory>ヘッダをインクルードする必要があります。

また、C++14以降の標準規格が必要となるため、コンパイルオプションで-std=c++14以上を指定してください。

C++
#include <memory> // std::make_uniqueを利用するために必要
#include <iostream>
#include <string>

class Player {
public:
    Player(const std::string& name) : name_(name) {
        std::cout << name_ << " が生成されました。" << std::endl;
    }
    ~Player() {
        std::cout << name_ << " が破棄されました。" << std::endl;
    }
    void Greet() const {
        std::cout << "こんにちは、" << name_ << " です。" << std::endl;
    }
private:
    std::string name_;
};

int main() {
    // std::make_uniqueによるインスタンス生成
    auto player = std::make_unique<Player>("勇者");
    
    player->Greet();

    // スコープを抜けると自動的にdeleteが呼ばれる
    return 0;
}
実行結果
勇者 が生成されました。
こんにちは、勇者 です。
勇者 が破棄されました。

std::make_uniqueの基本的な使い方

ここでは、具体的なコード例を通じて、様々なシーンでの使い方を解説します。

1. 単一オブジェクトの生成

最も一般的な使い方は、クラスや構造体のインスタンスを生成することです。

std::make_unique<型名>(引数)の形式で記述します。

C++
// 整数型の生成
auto myInt = std::make_unique<int>(42);

// 自作クラスの生成(コンストラクタに複数の引数を渡す)
struct Vec3 {
    float x, y, z;
    Vec3(float x, float y, float z) : x(x), y(y), z(z) {}
};

auto position = std::make_unique<Vec3>(10.0f, 20.0f, 30.0f);

2. 配列の生成

std::make_uniqueは配列の生成もサポートしています。

その場合、テンプレート引数にT[]を指定し、関数の引数に配列のサイズを渡します。

C++
// 10個のint型配列を確保
auto numbers = std::make_unique<int[]>(10);

for (int i = 0; i < 10; ++i) {
    numbers[i] = i * 10;
}

std::cout << "5番目の要素: " << numbers[4] << std::endl;
実行結果
5番目の要素: 40

3. 関数の引数として渡す

std::unique_ptrはコピーが禁止されていますが、ムーブ(所有権の移転)は可能です。

関数の引数に渡す際は、std::moveを使用します。

C++
void ProcessPlayer(std::unique_ptr<Player> p) {
    p->Greet();
} // ここでpは破棄される

int main() {
    auto p1 = std::make_unique<Player>("戦士");
    
    // 所有権を移動させる
    ProcessPlayer(std::move(p1));
    
    if (!p1) {
        std::cout << "p1は空になりました。" << std::endl;
    }
    return 0;
}

std::make_uniqueを使うべき4つの大きなメリット

なぜnewを直接使うのではなく、make_uniqueを使うべきなのでしょうか。

それには明確な理由が4つあります。

1. 例外安全性(Exception Safety)

これが最も技術的に重要な理由です。

C++では、関数の引数評価の順序が厳密に決まっていない場合があります。

2. コードの簡潔化と可読性の向上

型名を2回書く必要がなくなるため、コードがスッキリします。

特に複雑なテンプレート型を扱う際にその恩恵は顕著です。

C++
// newを使う場合:型名を2回書く必要がある
std::unique_ptr<VeryLongClassNameWithTemplate<int>> ptr(new VeryLongClassNameWithTemplate<int>());

// make_uniqueを使う場合:autoと組み合わせて1回で済む
auto ptr = std::make_unique<VeryLongClassNameWithTemplate<int>>();

3. 「new」をコードから追放できる

モダンC++の設計指針として「ソースコード内に生のリソース管理(new/delete)を直接書かない」というものがあります。

すべてをmake_uniquemake_sharedで統一することで、「newがある場所はどこかバグがあるかもしれない」と警戒すべき箇所が明確になります。

4. パフォーマンスの最適化(ごく僅か)

厳密にはmake_sharedほど劇的な差はありませんが、コンパイラにとって最適化しやすい構造になります。

コードの局所性が高まり、命令の並び替えによるリスクが減るため、安全かつ高速なバイナリ生成に寄与します。

new演算子を直接使う場合との決定的な違い

両者の違いを表にまとめました。

特徴std::make_uniquenew を使った生成
例外安全性非常に高い(安全)状況によりリークの危険あり
記述量少ない(autoと相性が良い)多い(型名を繰り返す)
配列サポート対応(C++14以降)対応
カスタムデリータ非対応対応
推奨度推奨(モダンC++)非推奨

カスタムデリータが必要な場合

唯一、std::make_uniqueが使えないケースがあります。

それは、メモリ解放時に独自の処理(カスタムデリータ)を行いたい場合です。

例えば、C言語のライブラリが提供する特定の解放関数を呼び出す必要がある場合などは、std::unique_ptrのコンストラクタを直接使う必要があります。

C++
// C言語風の構造体と解放関数
struct NativeResource { /* ... */ };
void FreeResource(NativeResource* r) { /* ... */ }

// このような場合は make_unique は使えない
std::unique_ptr<NativeResource, decltype(&FreeResource)> ptr(new NativeResource(), FreeResource);

std::make_uniqueを使用する際の注意点と制限

非常に便利なstd::make_uniqueですが、いくつか知っておくべき制限事項があります。

1. プライベートコンストラクタへのアクセス

std::make_uniqueは外部の関数であるため、クラスのprivateまたはprotectedなコンストラクタを呼び出すことができません

Factoryパターンなどでコンストラクタを隠蔽している場合、そのクラス内部からであってもstd::make_uniqueは使えません。

2. 中括弧初期化(initializer_list)の挙動

std::make_uniqueは丸括弧()を使って引数を転送するため、std::initializer_listを受け取るコンストラクタを意図した通りに呼び出せないことがあります。

C++
// 意図: 要素{1, 2, 3}を持つvector
// 実際: vectorのコンストラクタ(size_t, T)が呼ばれる可能性がある
auto v = std::make_unique<std::vector<int>>(3, 10); // 10, 10, 10 という内容になる

もし{1, 2, 3}という初期値を与えたい場合は、明示的にstd::initializer_listを渡すか、newを使う必要があります。

3. C++11以前の環境

前述の通り、std::make_uniqueはC++14以降の機能です。

C++11しか使えない古いプロジェクトでは、自作のmake_uniqueを定義するか、大人しくunique_ptr<T>(new T())と書くしかありません。

std::shared_ptrとの使い分け

最後に、もう一つの主要なスマートポインタであるstd::shared_ptrとの使い分けについても触れておきます。

std::make_uniqueを選ぶべき時

そのオブジェクトの所有者が自分一人である場合。

パフォーマンスを最優先する場合(unique_ptrは生ポインタとほぼ同じオーバーヘッド)。

基本的にはこちらをデフォルトにするべきです。

std::make_sharedを選ぶべき時

複数のオブジェクトから参照され、誰が最後に消すかわからない場合。

イベントハンドラやキャッシュ機構など、ライフサイクルが複雑な場合。

原則として、「まずはunique_ptr(make_unique)で設計し、どうしても共有が必要になった時だけshared_ptrに変更する」のが良い設計とされています。

まとめ

std::make_uniqueは、モダンなC++プログラミングにおいて欠かすことのできない必須ツールです。

newを直接記述する手法に比べて、例外安全性に優れ、タイピング量を減らし、意図の明確なコードを書くことができます。

カスタムデリータが必要な場合や、プライベートコンストラクタを呼ぶ場合などの特殊なケースを除き、ヒープ上にオブジェクトを作成する際は常にstd::make_uniqueを使用することを強く推奨します。

この記事で紹介した使い方をマスターすることで、メモリリークのリスクを最小限に抑え、堅牢でメンテナンス性の高いC++プログラムを構築できるはずです。

ぜひ今日から、あなたのコードの中にあるnewstd::make_uniqueに置き換えてみてください。

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

URLをコピーしました!