QListの「rvalue_ref」概念を解き明かす:Qtコンテナの効率的な使い方

2025-06-06

QList::rvalue_ref という表記は、Qtの公式ドキュメントや一般的に公開されているAPIとしては見かけません。しかし、文脈から判断すると、C++11で導入された「右辺値参照 (rvalue reference)」とQListの内部実装またはQListを使用する際の最適化に関連する概念について尋ねていらっしゃると思われます。

Qtのコンテナクラス、特にQListは、C++11以降のムーブセマンティクス(move semantics)と右辺値参照を活用して、要素の追加、削除、ソートなどの操作をより効率的に行えるように最適化されています。

右辺値参照 (rvalue reference) とは何か?

まず、右辺値参照そのものについて簡単に説明します。

C++11で導入された「右辺値参照」は、&& で表現される型修飾子です。これは主に一時オブジェクト(右辺値)にバインドされ、そのリソースを「盗む」(ムーブする)ことを可能にします。これにより、オブジェクトのコピーにかかるコストを削減し、パフォーマンスを向上させることができます。

  • 右辺値 (rvalue)
    アドレスを持たず、式の評価後に破棄される一時的なオブジェクト。例: 10, a + b, 関数から返される一時オブジェクト
  • 左辺値 (lvalue)
    アドレスを持ち、式の評価後も存続するオブジェクト。通常は変数など。例: int x;, x

QListと右辺値参照

QListが右辺値参照をどのように活用しているかというと、主に以下の点で効率化が図られています。

    • 以前は、QListに要素を追加する際、その要素がコピーされてQListの内部ストレージに格納されていました。
    • 右辺値参照とムーブコンストラクタ(move constructor)/ムーブ代入演算子(move assignment operator)が利用可能な型(例えば、QStringやカスタムクラス)の場合、一時オブジェクトを追加する際にコピーではなくムーブが可能になります。これにより、データのコピーにかかる時間とメモリが節約されます。
    • 例えば、myList.append(QString("Hello") + QString("World")); のようなコードでは、QString("Hello") + QString("World") の結果として生成される一時的なQStringオブジェクトは右辺値であり、QListはこれをムーブして格納することができます。
  1. 一時的なQListオブジェクトの受け渡し

    • 関数がQListを返す場合、以前はQList全体のコピーが発生していましたが、ムーブセマンティクスにより、QListの内部データ(配列など)の所有権を効率的に転送できるようになります。
    • 同様に、関数がQListを右辺値参照で受け取る場合(void func(QList<T>&& list))、渡された一時的なQListの内部リソースを「盗む」ことで、不要なコピーを回避できます。

QList::rvalue_refという表記の可能性

もし「QList::rvalue_ref」という表記が実際に存在するとすれば、それは以下のような文脈で内部的に、あるいは非常に専門的な議論の中で使われる可能性が考えられます。

  • 誤解
    QListが右辺値参照をサポートしているという事実を、直接的なメンバー名のように誤解して表現している。
  • 特定のバージョンや非公開API
    非常に古いバージョンや、通常のユーザーには公開されていない内部的な開発バージョンで使われていた可能性。
  • 概念的な説明
    QListが右辺値参照をどのように利用しているか、その動作原理を説明するための抽象的な表現。
  • 内部的な型エイリアスやトレイト
    QListのテンプレート引数Tが右辺値参照として扱われる場合の型を表す、内部的なtypedefusing宣言、あるいは型特性(type trait)として。

QtのQListにおける「rvalue_ref」という言葉は、直接的なAPIとしては存在しませんが、QListがC++11以降の右辺値参照とムーブセマンティクスを内部的に活用することで、パフォーマンスの最適化を実現しているという文脈で理解するのが適切です。これにより、一時オブジェクトのコピーを削減し、リソースの効率的な転送を可能にしています。



QList::rvalue_refという直接のAPIは存在しないため、それ自体に関するエラーはありません。しかし、C++11で導入された右辺値参照とムーブセマンティクスをQListと組み合わせて使用する際に、開発者が陥りがちな問題や、期待通りのパフォーマンスが得られない場合のトラブルシューティングのポイントはいくつか存在します。

ここでは、QListが内部的にムーブセマンティクスを利用する際、あるいはユーザーが明示的にムーブ操作を行う際に発生しうる問題を説明します。

ムーブセマンティクスが期待通りに機能しない(コピーが発生してしまう)

問題の症状
要素をQListに追加したり、QList間でデータを移動したりする際に、パフォーマンスの改善が見られない、またはデバッガでコピーコンストラクタが頻繁に呼び出されていることが確認できる。

原因

  • 最適化レベル
    コンパイラの最適化レベルが低い場合、一部のムーブ最適化が効かないことがあります(稀ですが)。
  • 右辺値参照として扱われない
    std::move()を使わずに、左辺値を右辺値参照として渡そうとしている。
  • const右辺値参照
    右辺値参照として渡されたオブジェクトがconst修飾されている場合、ムーブ操作は許可されず、コピーが発生します。ムーブ操作はリソースの変更(奪取)を伴うため、constではできません。
  • ムーブコンストラクタ/ムーブ代入演算子が定義されていない
    QListに格納するカスタムクラスTに、ムーブコンストラクタやムーブ代入演算子が明示的に定義されていない場合、コンパイラはデフォルトのコピーコンストラクタ/コピー代入演算子を生成するか、ユーザーが定義したコピー関連の関数が呼び出されます。

トラブルシューティング

  • constの確認
    QListに渡す一時オブジェクトや、ムーブの対象となるオブジェクトがconstではないことを確認してください。
  • std::move()の適切な使用
    左辺値をムーブしたい場合は、明示的にstd::move()を使用してください。
    MyObject obj;
    myList.append(std::move(obj)); // obj はムーブされた後、有効だが不定な状態になる
    
    注意点: std::move()を使用すると、元のオブジェクトはムーブされた後、その状態は「有効だが不定 (valid but unspecified)」になります。ムーブ後にそのオブジェクトを使用する場合は、再初期化するか、その状態に依存しないように注意が必要です。
  • カスタムクラスの確認
    QListに格納するクラスTが、適切なムーブコンストラクタとムーブ代入演算子を持っているか確認してください。
    • Rule of Five (or Zero)
      クラスにデストラクタ、コピーコンストラクタ、コピー代入演算子、ムーブコンストラクタ、ムーブ代入演算子のいずれか一つでもユーザー定義されている場合、残りの特殊メンバ関数も適切に定義するか、= default= deleteで明示的に指定する必要があります。もし定義しない場合、コンパイラはデフォルトのコピー操作を生成する可能性があります。

    • class MyResourceHolder {
      public:
          // ... コピーコンストラクタ、コピー代入演算子 など
      
          // ムーブコンストラクタ
          MyResourceHolder(MyResourceHolder&& other) noexcept {
              // other のリソースを this にムーブし、other は空にする
          }
      
          // ムーブ代入演算子
          MyResourceHolder& operator=(MyResourceHolder&& other) noexcept {
              if (this != &other) {
                  // 既存のリソースを解放し、other のリソースをムーブ
              }
              return *this;
          }
      };
      

ムーブされた後のオブジェクトの状態に関する誤解

問題の症状
std::move()でムーブしたはずのオブジェクトをその後も使用しようとした結果、クラッシュや予期せぬ動作が発生する。

原因
std::move()は、オブジェクトの所有権を転送するようコンパイラに指示するものであり、元のオブジェクトを「消滅」させるわけではありません。ムーブされた後のオブジェクトは通常、リソース(メモリ、ファイルハンドルなど)を持たない「空」の状態になりますが、その状態はクラスによって定義され、「有効だが不定」とされます。つまり、特定の操作(例えば、メンバー関数呼び出し)がエラーになる可能性があります。

トラブルシューティング

  • クラスの設計
    カスタムクラスのムーブコンストラクタやムーブ代入演算子を設計する際、ムーブされた元のオブジェクトが安全に破棄できるような状態になるように注意深く実装してください。
  • ムーブ後のオブジェクトの扱い
    std::move()した後、元のオブジェクトは、再初期化するか、その状態に依存しない(例えば、デストラクタが安全に呼び出されることのみを期待する)ようにしてください。
    MyObject obj;
    myList.append(std::move(obj));
    // obj はムーブされた後、ここでの使用は避けるべき、または再初期化が必要
    obj = MyObject(); // 再初期化
    

不必要な一時オブジェクトの生成(パフォーマンス問題)

問題の症状
QListに要素を追加する際に、期待していたよりも多くのメモリ使用量やCPU時間が発生している。

原因

  • コンストラクタのオーバーロード不足
    QListappend()などの関数が、右辺値参照を受け取るオーバーロードを持っていない、または適切に選択されていない。
  • 一時オブジェクトの不必要なコピー
    QListにオブジェクトを追加する際、一時オブジェクトがコピーされた後、さらにQListの内部でムーブされる、という二重のオーバーヘッドが発生している可能性があります。

トラブルシューティング

  • Qtのバージョン
    使用しているQtのバージョンが古い場合、ムーブセマンティクスへの対応が不完全である可能性があります。最新のQtバージョンでは、より広範にムーブセマンティクスが活用されています。
  • 直接のムーブ
    可能であれば、一時オブジェクトを直接生成し、それをstd::move()QListに渡すようにします。
    // 悪い例 (MyClass の一時オブジェクトが作られ、コピーされ、さらに QList 内部でムーブされる可能性)
    // MyClass temp_obj;
    // myList.append(temp_obj);
    
    // 良い例 (直接ムーブ)
    myList.append(MyClass{/*...コンストラクタ引数...*/}); // RVO/NRVO やムーブが期待できる
    

例外安全性とムーブセマンティクス

問題の症状
ムーブ操作中に例外が発生した場合、プログラムが不安定になったり、リソースリークが発生したりする。

原因
ムーブコンストラクタやムーブ代入演算子がnoexcept指定されていない場合、コンパイラは、例外安全性を確保するためにムーブではなくコピーを選択することがあります(特に標準コンテナ内で)。QListも内部的にこれに類似した考慮を行う可能性があります。

  • noexcept指定
    自作のクラスのムーブコンストラクタとムーブ代入演算子は、例外を投げないことが保証される場合、noexceptを指定してください。これにより、コンパイラはより積極的にムーブ最適化を適用できるようになります。
    MyResourceHolder(MyResourceHolder&& other) noexcept { /* ... */ }
    MyResourceHolder& operator=(MyResourceHolder&& other) noexcept { /* ... */ }
    


As I've explained in previous responses, QList::rvalue_ref is not a standard, publicly documented API or a directly exposed member function of QList. Therefore, there isn't any code example that explicitly uses QList::rvalue_ref as if it were a function or type within QList.

Instead, QList (and other Qt containers) internally leverage C++11 rvalue references and move semantics for efficiency when you pass rvalue objects to its functions. The "rvalue_ref" concept in your query is likely referring to this underlying mechanism.

So, the examples below will demonstrate how QList benefits from rvalue references/move semantics when you interact with it, rather than showing a non-existent QList::rvalue_ref function. I'll focus on how to write code that allows QList to perform move operations, thereby avoiding unnecessary copies.

QList::rvalue_refという直接のコードは存在しませんが、QListが内部的にC++11以降の右辺値参照とムーブセマンティクスをどのように利用し、それによってどのようにコードを最適化できるかを示す例を挙げます。

主要なポイントは、QListに要素を追加する際に、コピーではなくムーブ操作を誘発することです。これは、QListが受け取る要素の型にムーブコンストラクタやムーブ代入演算子が定義されており、かつその要素が右辺値として渡される場合に発生します。

以下の例では、リソース(ここではintの配列)を持つシンプルなカスタムクラスMyDataを定義し、そのコピーとムーブの挙動を追跡できるようにしています。

基本的なMyDataクラスの定義 (コピーとムーブのログ出力付き)

#include <QList>
#include <QDebug>
#include <QString>
#include <memory> // std::unique_ptr を使用

// カスタムクラスの例
class MyData {
public:
    // コンストラクタ
    explicit MyData(int size = 1) : m_size(size), m_data(new int[size]) {
        qDebug() << "MyData Constructor: size =" << m_size << this;
        for (int i = 0; i < m_size; ++i) {
            m_data[i] = i;
        }
    }

    // デストラクタ
    ~MyData() {
        qDebug() << "MyData Destructor:" << this;
        // m_data は std::unique_ptr が管理するので、手動で delete は不要
    }

    // コピーコンストラクタ (左辺値参照を受け取る)
    MyData(const MyData& other) : m_size(other.m_size), m_data(new int[other.m_size]) {
        qDebug() << "MyData Copy Constructor: from" << &other << "to" << this;
        std::copy(other.m_data.get(), other.m_data.get() + other.m_size, m_data.get());
    }

    // コピー代入演算子 (左辺値参照を受け取る)
    MyData& operator=(const MyData& other) {
        qDebug() << "MyData Copy Assignment: from" << &other << "to" << this;
        if (this != &other) {
            m_size = other.m_size;
            m_data.reset(new int[m_size]); // 古いリソースを解放し、新しいリソースを割り当て
            std::copy(other.m_data.get(), other.m_data.get() + other.m_size, m_data.get());
        }
        return *this;
    }

    // ムーブコンストラクタ (右辺値参照を受け取る && noexcept)
    MyData(MyData&& other) noexcept
        : m_size(other.m_size), m_data(std::move(other.m_data)) { // other.m_data の所有権を奪う
        qDebug() << "MyData Move Constructor: from" << &other << "to" << this;
        other.m_size = 0; // ムーブ元のオブジェクトは空の状態にする
    }

    // ムーブ代入演算子 (右辺値参照を受け取る && noexcept)
    MyData& operator=(MyData&& other) noexcept {
        qDebug() << "MyData Move Assignment: from" << &other << "to" << this;
        if (this != &other) {
            m_size = other.m_size;
            m_data = std::move(other.m_data); // other.m_data の所有権を奪う
            other.m_size = 0; // ムーブ元のオブジェクトは空の状態にする
        }
        return *this;
    }

    int size() const { return m_size; }
    int value(int index) const {
        if (index >= 0 && index < m_size) return m_data[index];
        return -1; // エラー
    }

private:
    int m_size;
    std::unique_ptr<int[]> m_data; // リソース管理に unique_ptr を使用
};

QListへの要素追加とムーブセマンティクスの活用例

QList::append()QList::insert()などのメソッドは、内部的に渡されたオブジェクトが右辺値であればムーブを試みます。

#include <QCoreApplication>

int main(int argc, char *argv[]) {
    QCoreApplication a(argc, argv);

    qDebug() << "--- 1. QListに左辺値(変数)を追加する場合 ---";
    QList<MyData> list1;
    MyData data1(5); // MyData コンストラクタ呼び出し
    list1.append(data1); // MyData コピーコンストラクタが呼び出される (data1 は左辺値)
    qDebug() << "list1 size:" << list1.size();
    // ここで data1 はまだ有効で、変更可能。リストにはそのコピーが格納されている。
    data1 = MyData(10); // data1 を再代入。リスト内の MyData とは異なる。
    qDebug() << "After re-assigning data1, list1[0] size:" << list1[0].size();


    qDebug() << "\n--- 2. QListに一時オブジェクト(右辺値)を追加する場合 ---";
    QList<MyData> list2;
    // MyData(3) は一時オブジェクト(右辺値)。
    // QList::append() は MyData のムーブコンストラクタを呼び出す。
    list2.append(MyData(3)); // MyData コンストラクタ -> MyData ムーブコンストラクタ
    qDebug() << "list2 size:" << list2.size();
    // ここで一時オブジェクトはすでにムーブされて破棄される。


    qDebug() << "\n--- 3. std::move() を使って左辺値を右辺値として渡す場合 ---";
    QList<MyData> list3;
    MyData data3(7); // MyData コンストラクタ
    qDebug() << "Before std::move, data3 size:" << data3.size();

    // std::move() は data3 を右辺値参照にキャストする。
    // その結果、QList::append() は MyData のムーブコンストラクタを呼び出す。
    list3.append(std::move(data3)); // MyData ムーブコンストラクタ

    qDebug() << "list3 size:" << list3.size();
    // data3 はムーブされた後、有効だが不定な状態になる。
    // 例えば、data3.size() を呼び出すと 0 を返す(MyData::m_size を 0 に設定しているため)。
    qDebug() << "After std::move, data3 size (should be 0):" << data3.size();


    qDebug() << "\n--- 4. QListを返す関数とムーブセマンティクス ---";
    // QList そのものがムーブされる場合(RVO/NRVOやQListのムーブコンストラクタ)
    auto createQList = []() {
        QList<MyData> temp_list;
        temp_list.append(MyData(2)); // ムーブコンストラクタ
        temp_list.append(MyData(4)); // ムーブコンストラクタ
        return temp_list; // 戻り値最適化 (RVO/NRVO) が働けばムーブはスキップされる
    };

    QList<MyData> list4 = createQList(); // QList のムーブコンストラクタが呼び出される可能性がある
    qDebug() << "list4 size:" << list4.size();

    qDebug() << "\n--- 5. QString などQtの組み込み型の場合 ---";
    QList<QString> stringList;
    QString s1 = "Hello";
    stringList.append(s1); // QString のコピーコンストラクタ

    stringList.append(QString("World") + "!"); // QString のムーブコンストラクタ
                                             // (QString("World") + "!" は一時オブジェクト)

    qDebug() << "stringList:" << stringList;

    return a.exec();
}

実行結果のログ (例)

--- 1. QListに左辺値(変数)を追加する場合 ---
MyData Constructor: size = 5 0x... // data1 のコンストラクタ
MyData Copy Constructor: from 0x... to 0x... // list1.append(data1) でコピーが発生
list1 size: 1
MyData Constructor: size = 10 0x... // data1 の再代入で新しいオブジェクトが作られる
MyData Destructor: 0x... // 古い data1 が破棄される
MyData Copy Assignment: from 0x... to 0x... // data1 への代入でコピー代入
After re-assigning data1, list1[0] size: 5 // リスト内のオブジェクトは変更されていない

--- 2. QListに一時オブジェクト(右辺値)を追加する場合 ---
MyData Constructor: size = 3 0x... // MyData(3) のコンストラクタ
MyData Move Constructor: from 0x... to 0x... // list2.append(MyData(3)) でムーブが発生
MyData Destructor: 0x... // 一時オブジェクトがムーブされた後、破棄される
list2 size: 1

--- 3. std::move() を使って左辺値を右辺値として渡す場合 ---
MyData Constructor: size = 7 0x... // data3 のコンストラクタ
Before std::move, data3 size: 7
MyData Move Constructor: from 0x... to 0x... // list3.append(std::move(data3)) でムーブが発生
list3 size: 1
After std::move, data3 size (should be 0): 0 // ムーブされた data3 は空の状態

--- 4. QListを返す関数とムーブセマンティクス ---
MyData Constructor: size = 2 0x...
MyData Move Constructor: from 0x... to 0x...
MyData Constructor: size = 4 0x...
MyData Move Constructor: from 0x... to 0x...
// ここで temp_list が createQList から返される際、QList のムーブコンストラクタが
// 呼び出される可能性がある (またはRVO/NRVOでスキップされる)
QList Move Constructor: from 0x... to 0x... // QList 自体のムーブ(Qtの内部実装に依存)
MyData Destructor: 0x... // temp_list 内の MyData オブジェクトの破棄
MyData Destructor: 0x... // temp_list 内の MyData オブジェクトの破棄
list4 size: 2

--- 5. QString などQtの組み込み型の場合 ---
stringList: ("Hello", "World!")

この例から分かるように、QListに対して明示的にQList::rvalue_refという関数を呼び出すことはありません。しかし、

  1. QList::append(一時オブジェクト): MyData(3)のように一時オブジェクトを直接渡すと、QListは自動的にそのオブジェクトのムーブコンストラクタ(またはムーブ代入演算子)を呼び出して、コピーを避けます。
  2. QList::append(std::move(左辺値)): std::move(data3)のように左辺値をstd::move()でキャストして渡すと、そのオブジェクトは右辺値として扱われ、同様にムーブコンストラクタが呼び出されます。


Therefore, when you ask about "alternative methods for programming related to 'QList::rvalue_ref'", you're essentially asking about alternative ways to manage data in QList (or similar containers) when you want to optimize for performance, specifically by avoiding unnecessary copies, or when you're dealing with resource-owning objects.

「QList::rvalue_ref」という直接のAPIが存在しないため、その「代替手法」という表現は、実際には「QListが内部的にムーブセマンティクスを利用する際の、より効率的なデータ管理方法」や「コピーを避けるための他のアプローチ」を指すことになります。

QtやC++のプログラミングにおいて、QListのようなコンテナで要素のコピーコストを削減したり、リソースの所有権を効率的に移動させたりするための代替的(あるいは補完的)な手法を以下に説明します。

ムーブセマンティクス(C++11以降)の積極的な活用

これは「QList::rvalue_ref」の概念の根幹をなすものであり、代替というよりは「本命」のアプローチです。

  • 関連するQt機能
    QVector, QList, QString, QByteArrayなど、Qtの主要なコンテナや値クラスは、C++11以降の環境ではムーブセマンティクスをサポートしています。
  • 欠点
    ムーブ対象のクラスが適切にムーブセマンティクスを実装している必要があります。ムーブ後に元のオブジェクトが「有効だが不定な状態」になることを理解しておく必要があります。
  • 利点
    最もC++的で効率的な方法です。コードが簡潔になり、パフォーマンスが向上します。
  • 詳細
    カスタムクラスにムーブコンストラクタとムーブ代入演算子を適切に実装し、QListに一時オブジェクトを渡す際や、std::move()を使って左辺値を右辺値として渡す際に、コンパイラが自動的にムーブ操作を選択するようにします。これにより、オブジェクトの深いコピーを避け、リソースのポインタやハンドルなどの所有権だけを高速に転送します。
// 以前の例の MyData クラスはムーブセマンティクスを実装済みと仮定
QList<MyData> myDataList;

// 一時オブジェクトを直接追加 (ムーブが起きる)
myDataList.append(MyData(100));

// std::move() を使って既存のオブジェクトをムーブ (ムーブが起きる)
MyData existingData(50);
myDataList.append(std::move(existingData)); // existingData はムーブされた後、不定な状態

ポインタ(スマートポインタ)の使用

リソースを所有するオブジェクトのコピーコストが高い場合、コンテナに直接オブジェクトを格納する代わりに、そのオブジェクトへのポインタを格納する方法があります。特にスマートポインタは、メモリ管理の自動化と安全性を提供します。

  • 欠点
    • 間接参照のオーバーヘッド(ポインタのデリファレンスが必要)。
    • メモリの局所性が低下し、キャッシュ効率が悪くなる可能性。
    • QList<std::unique_ptr<MyData>>のようにすると、std::unique_ptr自体はムーブ可能ですが、QListの要素はMyDataオブジェクトそのものではなくstd::unique_ptrオブジェクトになります。
    • QSharedPointerを使う場合、参照カウントのオーバーヘッドがあります。
  • 利点
    • オブジェクトの深いコピーを完全に回避できます(ポインタ自体はコピーされる)。
    • 大きなオブジェクトを頻繁にコンテナに追加/削除する場合のパフォーマンスが大幅に向上します。
    • std::unique_ptrQSharedPointerを使えばメモリ管理が自動化され、生ポインタの危険性を減らせます。
  • 詳細
    QList<MyData*>QList<std::unique_ptr<MyData>>QList<QSharedPointer<MyData>> などを利用します。
    • 生ポインタ (MyData*): 最も単純ですが、メモリ管理(new/deleteの責任)が完全にプログラマに委ねられるため、リソースリークやUse-After-Freeなどのバグの温床になりやすいです。
    • std::unique_ptr: 所有権が唯一であることを保証するスマートポインタ。ムーブ可能ですがコピー不可です。QList<std::unique_ptr<MyData>>に格納すると、要素の追加・削除時にムーブ操作が適用され、コピーは発生しません。コンテナが破棄されると、格納されたオブジェクトも自動的に解放されます。
    • QSharedPointer / std::shared_ptr: 参照カウント方式のスマートポインタ。複数のポインタが同じリソースを共有できます。コピー可能ですが、共有のオーバーヘッドがあります。
#include <QList>
#include <QDebug>
#include <memory> // std::unique_ptr, std::shared_ptr
#include <QSharedPointer> // Qt のスマートポインタ

// MyData クラスは以前と同じく、コピー・ムーブログ付きと仮定

// 1. std::unique_ptr を使用
QList<std::unique_ptr<MyData>> uniquePtrList;
uniquePtrList.append(std::make_unique<MyData>(5)); // オブジェクトはヒープに作成され、unique_ptr で管理
uniquePtrList.append(std::make_unique<MyData>(8));

// unique_ptr はコピーできないため、ムーブで所有権を移す
std::unique_ptr<MyData> ptr1 = std::make_unique<MyData>(3);
uniquePtrList.append(std::move(ptr1)); // ptr1 はヌルポインタになる

// 2. QSharedPointer を使用
QList<QSharedPointer<MyData>> sharedPtrList;
sharedPtrList.append(QSharedPointer<MyData>(new MyData(7)));
sharedPtrList.append(QSharedPointer<MyData>::create(12)); // C++11 の make_shared に相当

QSharedPointer<MyData> ptr2 = QSharedPointer<MyData>::create(6);
sharedPtrList.append(ptr2); // QSharedPointer はコピー可能(参照カウントが増える)

値セマンティクスを持つ軽量なラッパークラス

もし格納したいオブジェクトが比較的軽量で、ポインタの間接参照オーバーヘッドを避けたいが、一部の複雑なリソースを共有したい場合、値セマンティクスを持ちつつ内部で共有を行うラッパークラス(Implicit Sharing / Copy-on-Write)を設計することが考えられます。QtのQString, QByteArray, QImageなどがこのパターンです。

  • 欠点
    • 独自のImplicit Sharingクラスを実装するのは複雑。
    • 書き込み時にコピーが発生する可能性があるため、そのタイミングを考慮する必要がある。
    • スレッドセーフにするには追加の考慮が必要(QtのCOWクラスは通常スレッドセーフ)。
  • 利点
    • コンテナに「値」として格納できるため、ポインタを扱う手間がない。
    • 読み取りアクセスは非常に高速。
    • 多くのオブジェクトが共有されている場合、メモリ使用量が大幅に削減される。
    • データ変更時のみコピーが発生するため、変更されない限りコピーコストは発生しない。
  • 詳細
    クラス内部で共有ポインタ(QSharedDataPointerstd::shared_ptr)を使って実際のデータを管理し、コピーコンストラクタやコピー代入演算子ではポインタのコピーと参照カウントの増加だけを行います。実際のデータのコピーは、書き込み操作が行われる最初の時点で(Copy-on-Write)発生します。

これは自分で実装するには高度なテクニックですが、Qtの主要な値クラスがこの挙動を示すため、その恩恵を享受できます。

// 例えば、QString はImplicit Sharing を持つ
QList<QString> myStringList;
QString str1 = "Hello";
myStringList.append(str1); // str1 のデータはコピーされず、参照カウントが増える

QString str2 = "World";
myStringList.append(str2);

// str1 のデータが変更されると、初めてコピーが発生する
str1.append(" C++"); // ここで内部的にコピーが発生する可能性あり

QVectorの使用(連続メモリの利点)

QListはリンクリストの特性を持つため、要素の追加/削除(特に中央での)は効率的ですが、要素へのランダムアクセスや連続メモリへの最適化(キャッシュ効率など)ではQVectorに劣ります。もし頻繁な追加/削除よりもランダムアクセスやイテレーションのパフォーマンスが重要であれば、QVectorが優れた代替選択肢となります。

  • 欠点
    • 中央での要素の挿入/削除は高コスト(後続の要素がすべてシフトされるため)。
    • 事前に適切なサイズをreserve()しておかないと、頻繁な再割り当てがパフォーマンスを低下させる可能性がある。
  • 利点
    • ランダムアクセス(operator[])が非常に高速。
    • キャッシュ効率が良い。
    • ムーブセマンティクスと組み合わせることで、サイズ変更時のオーバーヘッドが軽減される。
  • 詳細
    QVectorは内部的に連続したメモリブロックを使用します。要素の追加によって再割り当て(reallocation)が発生すると、すべての要素が新しいメモリ領域にムーブまたはコピーされます。ムーブセマンティクスが有効な型であれば、この再割り当てはコピーではなくムーブによって行われるため、効率的です。
QVector<MyData> myDataVector;
myDataVector.reserve(10); // 再割り当てを避けるため事前にメモリを確保

myDataVector.append(MyData(5)); // ムーブが起きる
myDataVector.append(MyData(8)); // ムーブが起きる

MyData temp(3);
myDataVector.append(std::move(temp)); // ムーブが起きる