Qt スマートポインタ活用 - 所有権から考える選び方
(掲載 2026年6月xx日)
Qt には QPointer、QSharedPointer、QWeakPointer、QScopedPointer があります。
この記事では、それぞれの仕様を並べるのではなく、QObject の親子関係、所有権、
非同期処理、API 境界という観点から、どの型を選ぶべきかを整理します。
なぜ Qt でスマートポインターを使い分けるのか
C++ では、動的に確保したオブジェクトをいつ、誰が、どのように破棄するかを明確にする必要があります。
この責任が曖昧なままだと、メモリリーク、二重解放、寿命切れ参照が起きます。
Qt には QObject の親子関係という強力な寿命管理の仕組みがあります。
親 QObject が破棄されると、その子 QObject も自動的に破棄されます。
GUI 部品や QObject ベースのオブジェクトでは、まずこの仕組みを使えるかを考えるのが基本です。
ただし、親子関係だけですべてを表現できるわけではありません。
親子関係に乗らないデータ、複数の場所から共有されるオブジェクト、
非同期処理の中で削除済みかどうかを確認したい QObject には、別の表現が必要です。
そのために Qt には QPointer、QSharedPointer、QWeakPointer、QScopedPointer が用意されています。
重要なのは、スマートポインターを単なる自動 delete の道具として見るのではなく、
所有権と寿命の意図を型で表す道具として見ることです。
まず判断すること
Qt のスマートポインターを選ぶ前に、まず次の順番で考えると整理しやすくなります。
- 対象は QObject か、それ以外の C++ オブジェクトか。
- QObject なら、親子関係で寿命を管理できるか。
- 複数の場所が同じオブジェクトの寿命を共有する必要があるか。
- 所有せず、削除済みかどうかだけを安全に確認したいのか。
- ひとつのスコープ内だけで単独所有すればよいのか。
この順番で考えると、最初から QSharedPointer を使うべきかどうかで迷いにくくなります。
QObject の親子関係で十分なら、それが最も自然です。
所有しない監視でよいなら QPointer、共有所有が必要なら QSharedPointer、
共有所有を延ばしたくない参照なら QWeakPointer、スコープ内の単独所有なら QScopedPointer を検討します。
QPointer: QObject を所有せずに監視する
QPointer は QObject を所有しません。
その代わり、参照先の QObject が破棄されると自動的に nullptr になります。
そのため、非同期処理やイベント処理の後で、対象がまだ存在するかを確認したい場面に向いています。
#include <QApplication>
#include <QDebug>
#include <QDialog>
#include <QPointer>
#include <QTimer>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QPointer<QDialog> dialog = new QDialog;
dialog->setWindowTitle("Tracked dialog");
dialog->show();
int tick = 0;
QTimer timer;
QObject::connect(&timer, &QTimer::timeout, [&]() {
++tick;
if (dialog) {
qDebug() << "tick" << tick << ": dialog is alive";
} else {
qDebug() << "tick" << tick << ": dialog is null";
}
if (tick == 2 && dialog) {
qDebug() << "delete dialog";
delete dialog.data();
}
if (tick == 4) {
timer.stop();
app.quit();
}
});
timer.start(100);
return app.exec();
}
このサンプルでは、タイマーで定期的に QPointer を確認してから dialog にアクセスしています。
QPointer は dialog を所有していないため、dialog の寿命を延ばしません。
しかし、削除済みであれば nullptr になるため、寿命切れの QObject にアクセスする危険を避けられます。
つまり QPointer は、「持っている」ための型ではなく、「まだ存在するかを確認する」ための型です。
QSharedPointer: 共有所有を API で表す
QSharedPointer は、複数の保持者が同じオブジェクトの寿命を共有するための型です。
最後の QSharedPointer が破棄または clear されると、対象オブジェクトも破棄されます。
生ポインターを API に渡すと、呼び出し先がそのオブジェクトをどのくらい保持するのか、
呼び出し元がいつまで生かしておく必要があるのかが分かりにくくなります。
一方、QSharedPointer を渡す API は、呼び出し先も寿命を共有することを型で表します。
#include <QCoreApplication>
#include <QDebug>
#include <QSharedPointer>
#include <QString>
class SharedSettings
{
public:
explicit SharedSettings(QString environment)
: m_environment(environment)
{
}
~SharedSettings()
{
qDebug() << "SharedSettings destroyed";
}
QString environment() const
{
return m_environment;
}
private:
QString m_environment;
};
static QSharedPointer<SharedSettings> createSettings()
{
return QSharedPointer<SharedSettings>::create("staging");
}
class SettingsConsumer
{
public:
void setSettings(const QSharedPointer<SharedSettings> &settings)
{
m_settings = settings;
}
void printEnvironment() const
{
qDebug() << "consumer environment:" << m_settings->environment();
}
private:
QSharedPointer<SharedSettings> m_settings;
};
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
SettingsConsumer consumer;
{
QSharedPointer<SharedSettings> settings = createSettings();
consumer.setSettings(settings);
qDebug() << "main environment:" << settings->environment();
}
consumer.printEnvironment();
return 0;
}
この例では、SettingsConsumer が QSharedPointer をメンバーとして保持します。
呼び出し元が持っていた QSharedPointer を手放しても、SettingsConsumer が保持している間は SharedSettings が破棄されません。
このように、QSharedPointer は「この API は対象の寿命を共有する」という設計上の意味を表します。
ただし、共有所有は便利な一方で、解放タイミングを読みづらくする面もあります。
誰かが保持している限り破棄されないため、不要な共有所有を広げると寿命の境界が曖昧になります。
QWeakPointer: 共有所有を延ばさずに参照する
QWeakPointer は、QSharedPointer で管理されているオブジェクトを弱く参照するための型です。
QWeakPointer 自体は所有権を持たないため、対象オブジェクトの寿命を延ばしません。
特に重要なのは、QSharedPointer 同士の循環参照を避ける用途です。
A が B を QSharedPointer で保持し、B も A を QSharedPointer で保持すると、
互いに参照カウントを増やし合うため、外側の参照を手放しても解放されません。
#include <QCoreApplication>
#include <QDebug>
#include <QSharedPointer>
#include <QWeakPointer>
class Node
{
public:
explicit Node(const char *name)
: m_name(name)
{
++s_aliveCount;
qDebug() << "Node created:" << m_name;
}
~Node()
{
--s_aliveCount;
qDebug() << "Node destroyed:" << m_name;
}
static int aliveCount()
{
return s_aliveCount;
}
QSharedPointer<Node> next;
QWeakPointer<Node> previous;
private:
const char *m_name;
static int s_aliveCount;
};
int Node::s_aliveCount = 0;
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
{
QSharedPointer<Node> first = QSharedPointer<Node>::create("first");
QSharedPointer<Node> second = QSharedPointer<Node>::create("second");
first->next = second;
second->previous = first;
qDebug() << "second has previous:" << !second->previous.toStrongRef().isNull();
qDebug() << "alive in scope:" << Node::aliveCount();
}
qDebug() << "alive after scope:" << Node::aliveCount();
qDebug() << "outer owners are gone, and the nodes were destroyed";
return 0;
}
この例では、next は所有関係を表すため QSharedPointer にし、
previous は戻り参照にすぎないため QWeakPointer にしています。
previous が寿命を延ばさないため、循環参照を作らずに双方向の関係を表せます。
QWeakPointer を使うときは、必要なタイミングで toStrongRef() を使って QSharedPointer に変換し、
有効な場合だけアクセスします。
QScopedPointer: スコープ内で単独所有する
QScopedPointer は、ひとつのスコープ内で単独所有するための型です。
スコープを抜けると自動的に対象を破棄します。
所有者を移動したり共有したりする必要がない場合に適しています。
#include <QCoreApplication>
#include <QDebug>
#include <QScopedPointer>
#include <memory>
class QtScoped
{
public:
QtScoped()
{
qDebug() << "QtScoped created";
}
~QtScoped()
{
qDebug() << "QtScoped destroyed";
}
};
class StdScoped
{
public:
StdScoped()
{
qDebug() << "StdScoped created";
}
~StdScoped()
{
qDebug() << "StdScoped destroyed";
}
};
static void runQScopedPointerExample()
{
qDebug() << "enter QScopedPointer scope";
QScopedPointer<QtScoped> qtScoped(new QtScoped);
qDebug() << "leave QScopedPointer scope";
}
static void runStdUniquePtrExample()
{
qDebug() << "enter std::unique_ptr scope";
std::unique_ptr<StdScoped> stdScoped = std::make_unique<StdScoped>();
qDebug() << "leave std::unique_ptr scope";
}
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
runQScopedPointerExample();
qDebug() << "QScopedPointer function finished";
runStdUniquePtrExample();
qDebug() << "std::unique_ptr function finished";
return 0;
}
このような関数内の一時的な所有では、手動で delete を書く必要がなくなり、
例外や早期 return があってもスコープを抜けると確実に破棄されます。
現代 C++ では std::unique_ptr も同じような役割を持ちます。
Qt 固有の削除方法や既存の Qt コードとの一貫性を重視する場合は QScopedPointer、
標準 C++ との親和性や所有権の移動を重視する場合は std::unique_ptr が選択肢になります。
避けるべき使い方
Qt のスマートポインターで特に注意が必要なのは、QObject の親子関係と QSharedPointer を同じオブジェクトに重ねることです。
親 QObject が子を削除し、さらに QSharedPointer も同じオブジェクトを削除しようとすると、二重解放になります。
#include <QCoreApplication>
#include <QDebug>
#include <QPointer>
#include <QSharedPointer>
#include <QObject>
class Child : public QObject
{
public:
explicit Child(QObject *parent = nullptr)
: QObject(parent)
{
qDebug() << "Child created";
}
~Child()
{
qDebug() << "Child destroyed";
}
};
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
QObject *parent = new QObject;
QSharedPointer<Child> child(new Child(parent));
QPointer<Child> watched = child.data();
qDebug() << "before deleting parent, watched is null:" << watched.isNull();
delete parent;
qDebug() << "after deleting parent, watched is null:" << watched.isNull();
qDebug() << "clearing QSharedPointer after parent already destroyed the child";
child.clear();
return 0;
}
この例では、child は parent の子としてすでに所有されています。
そこに QSharedPointer を作ると、parent と QSharedPointer の両方が child を破棄する責任を持つ形になってしまいます。
これは避けるべき使い方です。
また、QSharedPointer を安易に API 全体へ広げるのも注意が必要です。
共有所有は便利ですが、誰が最後に手放すのかが見えにくくなります。
所有しない参照でよい場面では、生ポインター、参照、QPointer、QWeakPointer などを使い分けるべきです。
まとめ
Qt のスマートポインターは、単に delete を自動化するための道具ではありません。
どの型を使うかによって、所有するのか、監視するだけなのか、共有所有を補助するのか、
スコープ内で単独所有するのかをコード上に表せます。
| 状況 |
選択肢 |
| QObject を親子関係で管理できる |
QObject の親子関係 |
| QObject を所有せず安全に監視したい |
QPointer |
| 非 QObject を共有所有したい |
QSharedPointer |
| 共有所有を延ばさず参照したい |
QWeakPointer |
| スコープ内で単独所有したい |
QScopedPointer / std::unique_ptr |
重要なのは、スマートポインターを「便利そうだから使う」のではなく、
所有権と寿命の関係を明確にするために選ぶことです。
QObject の親子関係で十分な場面ではそれを使い、所有しない参照には QPointer、
共有所有には QSharedPointer、循環参照を避ける補助には QWeakPointer、
スコープ内の単独所有には QScopedPointer というように、目的に応じて型を選びます。