Qt 暗黙の共有入門 - コピーを恐れず値として扱うための仕組み
(掲載 2026年6月8日)
Qt の多くの値型クラスは、暗黙の共有を使っています。
暗黙の共有とは、コピーした時点では内部データを共有し、実際に変更が必要になったときだけデータを複製する仕組みです。
この仕組みにより、QString、QByteArray、QList、QImage などを値として自然に扱いながら、不要なコピーを避けられます。
この記事では、Qt 公式ドキュメントの
暗黙の共有
をもとに、仕組み、使いどころ、注意点、独自クラスでの実装方法を整理します。
はじめに
C++ では、オブジェクトを値で渡すとコピーが発生する、という感覚があります。
小さな値なら問題ありませんが、大きな文字列、画像、配列、コンテナでは、コピーのたびにメモリ確保やデータ複製が起きると負荷が大きくなります。
Qt はこの問題に対して、値型の使いやすさを保ったまま、内部でデータ共有を行う設計を採っています。
これが暗黙の共有です。
利用者は通常、共有されているかどうかを意識せず、普通の値のように代入し、関数に渡し、戻り値として返せます。
重要なのは、暗黙の共有が「コピーしない」仕組みではなく、「必要になるまで深いコピーを遅らせる」仕組みだという点です。
読み取りだけなら共有を保ち、変更が必要になったときにだけ切り離します。
値として扱える Qt クラス
暗黙の共有は、Qt の多くの値型クラスで使われています。
代表的なものには、次のようなクラスがあります。
QString: Unicode 文字列
QByteArray: バイト列
QStringList: 文字列リスト
QList、QMap、QHash、QSet: コンテナ
QImage、QPixmap、QIcon: 画像やアイコン
QPen、QBrush、QFont、QPalette: 描画や表示に関わる値
QJsonDocument、QJsonObject、QJsonArray: JSON データ
QRegularExpression: 正規表現
これらのクラスは、C++ の通常の値型と同じように扱えます。
たとえば、関数の戻り値として QString を返したり、QImage を別の変数に代入したりしても、ただちに全データが複製されるとは限りません。
QString makeTitle()
{
QString title = "Implicit Sharing";
return title;
}
void printTitle(QString title)
{
qDebug() << title;
}
このようなコードは、Qt では自然な書き方です。
const QString & を使うべき場面もありますが、Qt の値型は値渡しや戻り値でも扱いやすいように設計されています。
シャローコピーとディープコピー
暗黙的に共有されるオブジェクトは、概念的には「小さなハンドル」と「共有データブロック」に分かれています。
オブジェクト本体は共有データブロックへのポインタを持ち、共有データブロック側には参照カウントと実データがあります。
代入やコピーコンストラクタでは、通常はデータ全体を複製しません。
共有データブロックへのポインタをコピーし、参照カウントを増やします。
これがシャローコピーです。
QString a = "Qt";
QString b = a; // ここでは内部データを共有する
この時点で、a と b は同じ文字列データを参照できます。
ただし、プログラムから見ると a と b は別々の QString オブジェクトです。
一方を変更しても、もう一方まで変更されたようには見えません。
実データを複製するコピーはディープコピーです。
暗黙の共有では、ディープコピーは主に「共有されているデータを変更しようとしたとき」に発生します。
コピーオンライトの仕組み
コピーオンライトは、書き込み時にコピーするという考え方です。
共有データブロックの参照カウントが 1 なら、そのオブジェクトだけがデータを参照しているため、そのまま変更できます。
参照カウントが 2 以上なら、他のオブジェクトも同じデータを参照しています。
この場合、変更前にデータを複製し、自分だけのデータに切り離します。
QString a = "Qt";
QString b = a; // a と b は内部データを共有できる
b.append(" 6"); // b の変更前に必要なら切り離しが起きる
qDebug() << a; // "Qt"
qDebug() << b; // "Qt 6"
この例では、b.append() の前までは a と b が内部データを共有できます。
しかし b を変更すると、b は a から切り離されます。
そのため、a は元の "Qt" のままです。
Qt のクラス内部では、変更を行うメンバ関数が必要に応じて detach を行います。
利用者は通常、明示的に detach() を呼ぶ必要はありません。
ただし、後述するように、イテレータや非 const アクセスでは detach のタイミングを意識した方がよい場面があります。
基本サンプルで確認する
まず、QString の単純なコピーと変更を見ます。
#include <QCoreApplication>
#include <QDebug>
#include <QString>
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
QString original = "implicit";
QString copied = original;
copied.append(" sharing");
qDebug() << "original:" << original;
qDebug() << "copied:" << copied;
return 0;
}
実行結果は、original が "implicit"、copied が "implicit sharing" になります。
コピー後に copied を変更しても、original は変わりません。
これは値型として当然の振る舞いです。
暗黙の共有は、この値型としての振る舞いを保ちながら、変更前まではデータ共有できるようにします。
次に、QByteArray の読み取りと書き込みを分けて見ます。
QByteArray bytes = "abc";
QByteArray shared = bytes;
const char *readOnly = shared.constData(); // 読み取り用
qDebug() << readOnly;
shared[0] = 'A'; // 変更時に必要なら切り離し
qDebug() << bytes; // "abc"
qDebug() << shared; // "Abc"
読み取りだけなら共有を保てます。
一方、要素を書き換えると、共有されているデータから切り離されます。
画像データでも考え方は同じです。
QImage をコピーしただけでは、すぐにピクセルデータ全体を複製するとは限りません。
コピー先の画像に対してピクセル変更や描画を行うと、必要に応じて切り離されます。
QImage image(100, 100, QImage::Format_ARGB32);
image.fill(Qt::white);
QImage edited = image;
edited.setPixelColor(0, 0, Qt::red);
edited のピクセルを変更しても、image の同じピクセルが赤くなるわけではありません。
プログラムからは、2 つの QImage は独立した値として見えます。
暗黙の共有が効く場面
暗黙の共有が特に効くのは、読み取り中心の値を受け渡しする場面です。
- 関数に
QString や QByteArray を渡す
- 設定値や検索結果を戻り値として返す
QImage や QPixmap を一時的にコピーして保持する
- コンテナを別の処理に渡して読み取る
- API を値型中心にして、所有権の問題を単純にする
たとえば、設定情報を QJsonObject として返す API は、呼び出し側にとって扱いやすい設計です。
QJsonObject makeSettings()
{
QJsonObject settings;
settings["theme"] = "dark";
settings["autosave"] = true;
return settings;
}
戻り値で値を返すと、所有権を誰が持つかを考えずに済みます。
暗黙の共有により、Qt の値型ではこのような API を比較的自然に書けます。
注意が必要な場面
暗黙の共有は便利ですが、万能ではありません。
書き込みが発生すると、そこでディープコピーが必要になる場合があります。
大きなデータを共有したあとに何度も変更する処理では、想定よりコストが高くなることがあります。
QImage base = loadLargeImage();
QImage work = base;
for (int y = 0; y < work.height(); ++y) {
for (int x = 0; x < work.width(); ++x) {
work.setPixelColor(x, y, transform(work.pixelColor(x, y)));
}
}
この例では、work を変更し始めた時点で、必要なら base から切り離されます。
画像全体の複製が発生する可能性があるため、大きな画像ではコストを意識する必要があります。
また、非 const のメンバ関数や非 const のデータアクセスは、変更の準備として detach を引き起こすことがあります。
読み取りだけが目的なら、できるだけ const な API を使います。
void inspect(const QByteArray &bytes)
{
const char *p = bytes.constData(); // 読み取り目的を明確にする
qDebug() << p;
}
「変更しない」ことを型で示すと、読み手にも Qt のクラスにも意図が伝わりやすくなります。
イテレータと暗黙の共有の落とし穴
Qt 公式ドキュメントでも注意されている代表的な落とし穴が、暗黙的に共有されるコンテナとイテレータの組み合わせです。
特に STL スタイルイテレータを使っている間にコンテナをコピーしたり、コピーされたコンテナを変更したりすると、detach によってイテレータが期待どおりに使えなくなる場合があります。
基本方針は単純です。
イテレータで走査している間は、そのコンテナや共有関係にあるコピーを不用意に変更しないようにします。
変更が必要な場合は、走査前にコピーを明示的に作る、インデックスベースで処理する、変更対象を一時リストに集めてから反映する、といった設計にします。
QList<int> values = {1, 2, 3, 4};
QList<int> copy = values;
for (QList<int>::const_iterator it = values.cbegin(); it != values.cend(); ++it) {
qDebug() << *it;
}
copy.append(5); // 走査対象とは別のタイミングで変更する
読み取りだけなら const イテレータを使います。
コンテナを書き換える処理では、どのコンテナを走査し、どのコンテナを変更しているのかを明確にします。
スレッドと暗黙の共有
暗黙的に共有される Qt クラスでは、参照カウントの操作はスレッドをまたいだコピーに対応できるように設計されています。
そのため、あるスレッドで作った QString や QByteArray を、別のスレッドへ値として渡すことは Qt でよく行われます。
ただし、これは「同じオブジェクトを複数スレッドから自由に変更してよい」という意味ではありません。
同じインスタンスや同じ共有データに対して複数スレッドから読み書きする場合は、通常の C++ と同じく同期が必要です。
// 良い考え方: スレッドへ値として渡し、受け取った側で自分の値として扱う
QString message = "work item";
emit workRequested(message);
スレッド間では「値を渡す」と考えると整理しやすくなります。
共有されているかどうかは内部実装の最適化であり、複数スレッドから同じ可変状態を同時に触ってよいという保証ではありません。
独自の暗黙的共有クラスを作る
独自の値型クラスで暗黙の共有を使いたい場合は、QSharedData と QSharedDataPointer を使います。
QSharedData を継承したデータクラスに実データを置き、公開クラスは QSharedDataPointer をメンバとして持ちます。
#include <QSharedData>
#include <QSharedDataPointer>
#include <QString>
class BookData : public QSharedData
{
public:
QString title;
QString author;
};
class Book
{
public:
Book()
: d(new BookData)
{
}
QString title() const
{
return d->title;
}
void setTitle(const QString &title)
{
d->title = title; // QSharedDataPointer が必要に応じて detach する
}
QString author() const
{
return d->author;
}
void setAuthor(const QString &author)
{
d->author = author;
}
private:
QSharedDataPointer<BookData> d;
};
QSharedDataPointer は、非 const 経由でデータにアクセスするときに必要に応じて detach します。
そのため、上の setTitle() のような変更メンバ関数では、共有されているデータを直接壊すのではなく、自分用のデータに切り離してから変更できます。
この Book は、QString などと同じように値としてコピーできます。
Book a;
a.setTitle("Qt");
Book b = a;
b.setTitle("Qt 6");
qDebug() << a.title(); // "Qt"
qDebug() << b.title(); // "Qt 6"
独自クラスで暗黙の共有を使うべきかどうかは、そのクラスを値型として頻繁にコピーするか、大きなデータを持つか、読み取り中心で共有する場面が多いかで判断します。
小さな単純データなら、普通の値型として実装した方が分かりやすい場合もあります。
暗黙の共有とスマートポインタの違い
暗黙の共有は、QSharedPointer のようなスマートポインタとは目的が異なります。
名前に「共有」が含まれるため混同しやすいですが、設計上の意味は別です。
| 仕組み |
主な目的 |
利用者から見た意味 |
| 暗黙の共有 |
値型のコピー最適化 |
普通の値として扱う |
QSharedPointer |
オブジェクトの共有所有 |
寿命を複数の保持者で共有する |
QWeakPointer |
共有所有を延ばさない参照 |
必要なときだけ生存確認して参照する |
暗黙の共有では、利用者は QString a と QString b を別々の値として扱います。
内部でデータが共有されていても、それは最適化のための実装詳細です。
一方、QSharedPointer は所有権を共有するための型です。
「誰がいつまでオブジェクトを生かすか」を API 上で表します。
暗黙の共有は値型の話、スマートポインタは寿命管理の話、と分けて考えると混乱しにくくなります。
よくある誤解
暗黙の共有については、いくつか誤解しやすい点があります。
コピーされないわけではない
暗黙の共有は、コピーを完全になくす仕組みではありません。
変更が必要になれば、ディープコピーが発生することがあります。
特に大きな画像や大きなコンテナを共有後に変更する場合は、コピーのコストを意識します。
常に速いわけではない
読み取り中心の受け渡しでは効果的ですが、コピーしてすぐに大きく変更するような処理では、最初から別の設計にした方がよい場合があります。
性能を判断する場合は、共有の有無だけでなく、変更頻度、データサイズ、メモリ確保の回数を見ます。
参照渡しが不要になるわけではない
Qt の値型は値渡ししやすい設計ですが、すべてを値渡しにすればよいという意味ではありません。
大きなオブジェクトを読み取り専用で受け取る API では、const T & が自然な場合があります。
一方、戻り値では値で返す方が所有権の説明が単純になります。
共有されていることを前提にしたコードを書かない
暗黙の共有は実装上の最適化です。
利用者は、QString や QImage を独立した値として扱うべきです。
「この時点では内部データが同じはず」といった前提でコードを書くと、detach のタイミングや Qt の実装変更に依存した読みにくいコードになります。
まとめ
暗黙の共有は、Qt の値型を使いやすくしている重要な仕組みです。
コピー時には内部データを共有し、変更時に必要なら切り離すことで、値型としての自然な振る舞いとコピーコストの削減を両立します。
- 代入や値渡しでは、通常は内部データを共有できる
- 変更時には、必要に応じてコピーオンライトで切り離される
- 読み取り中心の API では、Qt の値型を自然に扱える
- 大きなデータを変更する場面では、detach のコストを意識する
- コンテナのイテレータ使用中は、コピーや変更に注意する
- 独自値型では、
QSharedData と QSharedDataPointer を使って暗黙の共有を実装できる
- 暗黙の共有は値型の最適化であり、スマートポインタによる所有権管理とは別の話である
Qt の値型を設計どおりに使うと、API はシンプルになります。
必要以上にポインタや参照に寄せるのではなく、値として扱えるものは値として扱い、所有権が問題になるオブジェクトだけを明示的に管理する。
これが、暗黙の共有を理解したうえでの実践的な使い方です。