C++におけるメモリ管理は、プログラムの安全性とパフォーマンスに直結する極めて重要な要素です。
かつてのC++ではnewとdeleteを手動で制御していましたが、現代のC++(モダンC++)ではスマートポインタの利用が標準となっています。
その中でも、複数の場所で所有権を共有できるstd::shared_ptrは非常に強力ですが、これを生成する際にはstd::make_sharedを使用することが強く推奨されています。
本記事では、なぜmake_sharedを使うべきなのか、そのメリットから注意点、最新のC++20での拡張機能までを徹底的に解説します。
std::make_sharedとは何か
std::make_sharedは、C++11で導入された関数テンプレートであり、「オブジェクトの構築」と「std::shared_ptrの生成」を同時に行うためのヘルパー関数です。

基本的な構文の比較
まず、従来のnewを使用した生成方法と、make_sharedを使用した生成方法を比較してみましょう。
#include <iostream>
#include <memory>
#include <string>
class Monster {
public:
Monster(std::string name, int hp) : name_(name), hp_(hp) {
std::cout << name_ << "が現れた!" << std::endl;
}
~Monster() {
std::cout << name_ << "は倒れた。" << std::endl;
}
void status() const {
std::cout << "Name: " << name_ << ", HP: " << hp_ << std::endl;
}
private:
std::string name_;
int hp_;
};
int main() {
// 1. 従来のnewを使用する方法 (非推奨)
// コンストラクタの引数をnewに渡し、そのポインタをshared_ptrに渡す
std::shared_ptr<Monster> m1(new Monster("スライム", 10));
// 2. std::make_sharedを使用する方法 (推奨)
// テンプレート引数に型を指定し、関数の引数にコンストラクタの引数を渡す
auto m2 = std::make_shared<Monster>("ドラゴン", 100);
m1->status();
m2->status();
return 0;
}
スライムが現れた!
ドラゴンが現れた!
Name: スライム, HP: 10
Name: ドラゴン, HP: 100
ドラゴンは倒れた。
スライムは倒れた。
上記のコードからわかるように、make_sharedを使用するとautoキーワードと組み合わせて型名の記述を一度だけで済ませることができます。
これにより、コードが簡潔になり、可読性が向上します。
make_sharedを使用する3つの大きなメリット
なぜnewを直接使わず、make_sharedを使うべきなのでしょうか。
それには明確な「パフォーマンス」「安全性」「簡潔性」という3つの理由があります。
1. メモリ割り当ての効率化(パフォーマンス)
std::shared_ptrは内部で2つのデータを保持しています。
一つは対象となるオブジェクト本体、もう一つは参照カウンタなどを管理する「管理ブロック(Control Block)」です。
newを直接使用した場合、オブジェクトの生成で1回、管理ブロックの生成で1回、合計2回のメモリ割り当て(動的確保)が発生します。
メモリ割り当てはコストの高い処理であるため、これはオーバーヘッドとなります。
一方、make_sharedを使用すると、オブジェクトと管理ブロックを単一の連続したメモリ領域にまとめて割り当てます。
これにより、メモリ確保の回数が1回で済み、CPUキャッシュの効率も向上するため、実行速度が改善されます。
2. 例外安全性(Exception Safety)
複雑な関数の引数などでスマートポインタを生成する場合、newを使うとメモリリークの危険性があります。
// このような呼び出しを考える
void process(std::shared_ptr<Widget> ptr, int priority);
// newを使った場合
process(std::shared_ptr<Widget>(new Widget()), get_priority());
C++では、関数の引数の評価順序は厳密に決まっていません。
もし「new Widget()」が実行された直後、shared_ptrのコンストラクタが呼ばれる前にget_priority()が実行され、そこで例外が発生したとします。
この場合、newで確保されたメモリは誰にも管理されず、解放される手段を失いリークします。
make_sharedを使用すれば、オブジェクトの生成とスマートポインタへの格納がアトミック(不可分)に行われるため、このような順序問題によるメモリリークは発生しません。
3. コードの簡潔さと型推論
先述の通り、make_sharedはautoとの相性が抜群です。
| 手法 | 記述量 | 型の重複 |
|---|---|---|
shared_ptr<T> p(new T()) | 多い | あり(Tを2回書く) |
auto p = make_shared<T>() | 少ない | なし(Tは1回だけ) |
特に複雑なテンプレート型を扱う場合、型名を2回書く手間とミスのリスクを大幅に削減できます。
std::make_sharedの詳細な使い方
ここでは、具体的なユースケースに応じたmake_sharedの使い方を見ていきましょう。
引数を持つコンストラクタの呼び出し
make_sharedは可変引数テンプレートを使用しており、渡された引数をそのまま対象クラスのコンストラクタへ転送(Perfect Forwarding)します。
#include <memory>
#include <vector>
class Player {
public:
Player(int id, std::string name) : id_(id), name_(name) {}
private:
int id_;
std::string name_;
};
int main() {
// 複数の引数もそのまま渡せる
auto player = std::make_shared<Player>(1, "Hero");
// std::vectorなどと組み合わせる場合
std::vector<std::shared_ptr<Player>> party;
party.push_back(std::make_shared<Player>(2, "Mage"));
}
C++20からの新機能:配列のサポート
C++17までは、make_sharedで配列を扱うことはできませんでしたが、C++20から配列の生成がサポートされました。
#include <memory>
int main() {
// C++20以降: 要素数5のint配列を確保
auto arr = std::make_shared<int[]>(5);
// インデックスアクセスも可能 (C++17以降のshared_ptr)
arr[0] = 100;
// 初期値を指定して初期化
auto arr_init = std::make_shared<int[]>(3, 10); // {10, 10, 10}
}

make_sharedを使ってはいけないケース(デメリット)
非常に便利なmake_sharedですが、万能ではありません。
特定の状況下では、あえて使わないという選択が必要になります。
1. カスタムデリータが必要な場合
std::shared_ptrには、メモリ解放時に独自の処理を行う「カスタムデリータ」を指定できますが、make_sharedではカスタムデリータを指定できません。
// ファイルポインタを自動で閉じるようなケース
FILE* fp = fopen("test.txt", "r");
// make_sharedではデリータを指定できないため、こちらは直接shared_ptrを生成する
std::shared_ptr<FILE> file_ptr(fp, fclose);
2. コンストラクタが非公開(private)の場合
make_sharedは関数内部から対象クラスのコンストラクタを呼び出します。
そのため、Factoryパターンなどでコンストラクタがprivateに設定されているクラスに対しては、外部関数であるmake_sharedからアクセスできず、コンパイルエラーとなります。
3. weak_ptrによるメモリ保持問題
これが最も注意すべきmake_sharedの技術的弱点です。
make_sharedは「オブジェクト」と「管理ブロック」を一つのメモリ領域に確保します。
この領域は、「参照カウンタ(shared_ptr)」と「弱参照カウンタ(weak_ptr)」の両方がゼロになるまで解放されません。

もし対象のオブジェクトが非常に巨大(数MBなど)で、かつ、そのオブジェクトを指すstd::weak_ptrが長期間生存する場合、オブジェクト自体は破棄されているのに、メモリ領域が丸ごと解放されずに残り続けるという現象が発生します。
このような特殊なケースでは、newを使って生成し、オブジェクトのメモリだけでも先に解放できるようにする方がメモリ効率が良くなります。
shared_ptrとの使い分けまとめ
これまでの内容を踏まえ、使い分けを整理します。
| 状況 | 推奨される方法 | 理由 |
|---|---|---|
| 通常のオブジェクト生成 | | 高速、安全、簡潔 |
| C++20以降の配列生成 | | 高速、安全 |
| カスタムデリータが必要 | | make_sharedが非対応 |
| コンストラクタがprivate | | アクセス権限の問題 |
| 巨大オブジェクト + 長期間のweak_ptr | | メモリの早期解放を優先 |
実践的なサンプル:クラス内での活用
最後に、実際の開発でよく見られる、自分自身のポインタを管理するクラスの例を紹介します。
#include <iostream>
#include <memory>
#include <vector>
class Node : public std::enable_shared_from_this<Node> {
public:
Node(int value) : value_(value) {
std::cout << "Node " << value_ << " created." << std::endl;
}
void add_child(std::shared_ptr<Node> child) {
children_.push_back(child);
}
// 自分自身のshared_ptrを安全に取得する
std::shared_ptr<Node> get_ptr() {
return shared_from_this();
}
private:
int value_;
std::vector<std::shared_ptr<Node>> children_;
};
int main() {
// 1. ルートノードを生成
auto root = std::make_shared<Node>(0);
// 2. 子ノードを生成して追加
auto child1 = std::make_shared<Node>(1);
root->add_child(child1);
// 3. 自分自身のポインタを介した操作
auto root_alias = root->get_ptr();
std::cout << "参照数: " << root.use_count() << std::endl; // root と root_alias で 2
return 0;
}
Node 0 created.
Node 1 created.
参照数: 2
この例のように、make_sharedはstd::enable_shared_from_thisを継承したクラスとも組み合わせて頻繁に使用されます。
スマートポインタを利用する設計では、「生ポインタの露出を最小限にする」ことが鉄則であり、その入り口としてmake_sharedは不可欠な存在です。
まとめ
std::make_sharedは、現代のC++開発において第一選択となるべき関数です。
メモリ割り当てを最適化し、例外安全性を確保し、コードを短く保つという3つのメリットは、開発効率とプログラムの品質を大きく向上させます。
C++20からは配列にも対応し、さらに死角がなくなりました。
ただし、カスタムデリータが必要な場合や、weak_ptrによるメモリ保持問題が懸念される巨大なオブジェクトを扱う場合には、従来のshared_ptrコンストラクタを使用するという使い分けが必要です。
「原則としてmake_sharedを使い、できないこと・懸念がある場合のみnewを検討する」というスタンスを持つことで、安全で高速なC++コードを記述することができるようになります。
