メモリリーク対策も!QList::removeAt() とポインタの安全な扱い方

2025-06-06

QList::removeAt() は、Qt のコンテナクラスである QList のメンバ関数の一つで、指定されたインデックス位置にある要素をリストから削除するために使用されます。

使い方 (使用例)

QList<QString> myList;
myList << "Apple" << "Banana" << "Cherry" << "Date";

// インデックス 1 (2番目の要素) の "Banana" を削除
myList.removeAt(1);

// 結果: myList は {"Apple", "Cherry", "Date"} となる

詳細

  1. 引数:

    • int i: 削除したい要素のインデックスを指定します。インデックスは 0 から始まります。
  2. 戻り値:

    • void: 戻り値はありません。
  3. 動作:

    • 指定されたインデックス i にある要素をリストから削除します。
    • 要素が削除されると、その位置より後ろにあった要素はすべて、削除された要素のあった位置に移動し、リストのサイズが 1 減少します。
    • インデックス i が無効な場合 (例えば、負の数やリストのサイズ以上の値の場合)、プログラムはクラッシュする可能性があります(デバッグビルドではアサートが発生することが多いです)。そのため、removeAt() を呼び出す前に、インデックスが有効な範囲内にあることを確認することが重要です。通常は QList::size()QList::isEmpty() などでチェックします。
  4. 計算量 (パフォーマンス):

    • QList は内部的に配列のような構造を持っているため、removeAt() の計算量は通常 O(n) となります。ここで n は削除位置より後ろにある要素の数です。これは、要素を削除すると、その後のすべての要素が「ずれる」必要があるためです。
    • リストの先頭に近い要素を削除すると、多くの要素をずらす必要があるため、パフォーマンスへの影響が大きくなります。リストの末尾に近い要素を削除する場合は、ずらす要素が少ないため、比較的速いです。

QList には、removeAt() 以外にも要素を削除する方法がいくつかあります。

  • QList::clear(): リスト内のすべての要素を削除します。
  • QList::takeAt(int i): 指定されたインデックスの要素を削除し、削除された要素のコピーを返します。元の要素はリストから削除されます。
  • QList::removeAll(const T &value): 指定された値と一致するすべての要素を削除します。
  • QList::removeOne(const T &value): 指定された値と最初に一致した要素を削除します。


インデックスの範囲外アクセス (Out-of-Bounds Access)

エラーの症状:

  • デバッグビルドでは、アサート (Q_ASSERT) が発生して停止する。
  • プログラムがクラッシュする (Segmentation fault, Access violationなど)。

原因: removeAt(i) を呼び出す際に、i がリストの有効なインデックス範囲 (0 から size() - 1) を超えている場合に発生します。例えば、空のリストで removeAt(0) を呼び出したり、リストのサイズが5なのに removeAt(5) を呼び出したりするケースです。

トラブルシューティング: removeAt() を呼び出す前に、必ずインデックスが有効な範囲内にあるかを確認します。

QList<QString> myList;
// ... myListに要素を追加 ...

int indexToRemove = 5; // 削除したいインデックス

if (indexToRemove >= 0 && indexToRemove < myList.size()) {
    myList.removeAt(indexToRemove);
} else {
    qWarning() << "指定されたインデックスは無効です:" << indexToRemove;
}

特に、ユーザーからの入力や外部データに基づいてインデックスを決定する場合は、このチェックが不可欠です。

ループ内での要素削除によるインデックスのずれ

エラーの症状:

  • ループの途中でクラッシュする。
  • 予期しない要素が削除されたり、削除されない要素があったりする。
  • リストの要素を順番に処理しながら削除しているのに、すべての要素が削除されない。

原因: QList::removeAt() は要素を削除すると、その後の要素のインデックスをずらします。例えば、リストに{"A", "B", "C", "D"}があり、インデックス1の"B"を削除すると、"C"がインデックス1になり、"D"がインデックス2になります。

この状態で、通常のforループでインデックスを増やしていくと、要素のずれによって一部の要素がスキップされたり、存在しないインデックスにアクセスしようとしてクラッシュしたりします。

// よくある間違いの例
QList<QString> myList = {"A", "B", "C", "D"};
for (int i = 0; i < myList.size(); ++i) { // size()はループ中に変わる
    if (myList.at(i) == "B" || myList.at(i) == "D") {
        myList.removeAt(i);
        // ここでインデックスがずれるため、次のループで"C"がスキップされる可能性がある
    }
}
// 期待通りに"A", "C"とならず、"A", "D"が残るなどの結果になる場合がある

トラブルシューティング:

  • QList::removeIf (C++11以降) または std::remove_ifQList::erase: 複数の要素を条件で削除する場合、より簡潔で効率的な方法です。これは、実際に要素を削除する前に、削除する要素をリストの後ろに移動させる(remove-erase idiom)ことで行われます。

    #include <algorithm> // for std::remove_if
    
    QList<QString> myList = {"A", "B", "C", "D"};
    
    // C++11以降のラムダ式を使用
    auto it = std::remove_if(myList.begin(), myList.end(),
                             [](const QString& s){ return s == "B" || s == "D"; });
    
    myList.erase(it, myList.end());
    // 結果: {"A", "C"}
    

    Qt 6からは QList::removeIf が追加されており、より直接的に利用できます。

    // Qt 6 以降
    QList<QString> myList = {"A", "B", "C", "D"};
    myList.removeIf([](const QString& s){ return s == "B" || s == "D"; });
    // 結果: {"A", "C"}
    
  • イテレータを使用する: Qtのイテレータは要素を削除した場合の正しい次のイテレータを返してくれるため、より安全でC++らしい方法です。

    QList<QString> myList = {"A", "B", "C", "D"};
    for (QList<QString>::iterator it = myList.begin(); it != myList.end(); ) {
        if (*it == "B" || *it == "D") {
            it = myList.erase(it); // erase()は次の要素のイテレータを返す
        } else {
            ++it; // 要素を削除しなかった場合は次の要素へ進む
        }
    }
    // 結果: {"A", "C"}
    

    QList::erase() は要素を削除し、その次の要素へのイテレータを返します。この戻り値を使うことで、ループの継続条件を正しく保てます。

  • while ループとインデックス調整: 要素を削除した場合にのみインデックスを増やさないようにします。

    QList<QString> myList = {"A", "B", "C", "D"};
    int i = 0;
    while (i < myList.size()) { // size()はループ中に変わる
        if (myList.at(i) == "B" || myList.at(i) == "D") {
            myList.removeAt(i);
            // 要素を削除した場合、現在のiの位置に新しい要素が来るので、iを増やさない
        } else {
            i++; // 要素を削除しなかった場合は次の要素へ進む
        }
    }
    // 結果: {"A", "C"}
    
  • 後ろからループする: 要素を削除しても、それ以前のインデックスには影響がないため、リストの後ろから前に向かってループします。

    QList<QString> myList = {"A", "B", "C", "D"};
    for (int i = myList.size() - 1; i >= 0; --i) {
        if (myList.at(i) == "B" || myList.at(i) == "D") {
            myList.removeAt(i);
        }
    }
    // 結果: {"A", "C"}
    

ポインタを保持する QList のメモリリーク

エラーの症状:

  • 意図しないメモリ使用量の増加。
  • プログラム終了時や、リストがクリアされたときに、メモリリークが発生する(メモリが解放されない)。

原因: QList<MyObject*> のように、リストがオブジェクトへのポインタを保持している場合、removeAt() はリストからポインタを削除するだけで、そのポインタが指す実体(オブジェクト)を解放しません。オブジェクト自体はヒープ上に残り、メモリリークが発生します。

トラブルシューティング: ポインタを保持するリストから要素を削除する場合、そのポインタが指すオブジェクトも適切に解放する必要があります。

  • スマートポインタを利用する: C++11以降で利用可能なstd::unique_ptrstd::shared_ptrを使うことで、メモリ管理を自動化し、メモリリークのリスクを大幅に減らすことができます。Qt 5.x以降であれば、QSharedPointerなども選択肢になります。

    // std::unique_ptr を使用
    QList<std::unique_ptr<MyObject>> myList;
    myList.append(std::make_unique<MyObject>());
    myList.append(std::make_unique<MyObject>());
    
    myList.removeAt(0); // unique_ptrが自動的にオブジェクトを解放する
    // リストがスコープを抜ける際も、残りのunique_ptrが自動で解放してくれる
    

    これが現代のC++における推奨されるプラクティスです。

  • qDeleteAll()QList::clear(): リスト内のすべての要素を解放し、リストを空にする場合に使います。

    QList<MyObject*> myList;
    // ...
    
    qDeleteAll(myList); // リスト内のすべてのMyObject*が指すオブジェクトをdeleteする
    myList.clear();     // リストからすべてのポインタを削除し、リストを空にする
    
  • QList::takeAt()delete を組み合わせる: takeAt() は要素をリストから削除し、その要素を返すため、これを使って安全に削除と解放ができます。

    QList<MyObject*> myList;
    // ...
    
    int indexToRemove = 0;
    if (indexToRemove >= 0 && indexToRemove < myList.size()) {
        MyObject* objToDelete = myList.takeAt(indexToRemove); // リストから取り出し、ポインタを取得
        delete objToDelete; // オブジェクトを解放
    }
    
  • delete で明示的に解放する:

    QList<MyObject*> myList;
    // ... myListにMyObject*を追加 (new MyObject()などで作成) ...
    
    int indexToRemove = 0;
    if (indexToRemove >= 0 && indexToRemove < myList.size()) {
        delete myList.at(indexToRemove); // オブジェクトを解放
        myList.removeAt(indexToRemove);  // ポインタをリストから削除
    }
    

    複数の要素を削除する場合は、ループ内で同様に処理します。

エラーの症状:

  • イテレータを使用中に removeAt() を呼び出すと、イテレータが無効になり、クラッシュや不正な動作を引き起こす。
  • QList をコピーして、元のリストを変更したときに、コピーしたはずのリストも変更されてしまう(またはその逆)。

原因: QListという仕組みを採用しています。これは、リストがコピーされたときに、実際にはデータがコピーされず、内部ポインタが共有されるだけというものです。リストが変更される際に初めてデータの深いコピー(deep copy)が行われます("Copy-on-Write")。

removeAt() はリストを変更する操作なので、もしそのリストが共有されている場合、removeAt() の呼び出し時にデータの深いコピーが発生します。また、要素が削除されると、その後の要素の物理的なメモリ位置が変更されるため、そのリストに対する既存のイテレータ(特に要素へのポインタを持つイテレータ)は無効になる可能性があります。

トラブルシューティング:

  • イテレータの使用の注意: removeAt() のような変更操作を行う際には、ループ内のイテレータが無効にならないように注意します。前述の「ループ内での要素削除」の解決策(特にerase()の戻り値を使う方法)が有効です。
  • 明示的なコピー: リストの独立したコピーが必要な場合は、QList のコンストラクタや代入演算子によるコピーの後、すぐにそのコピーを修正する操作を行うか、QList::deepCopy() (Qt 6.x以降) などを使って明示的にコピーを行います。

QList::removeAt() を使用する際の主な注意点は以下の通りです。

  1. インデックス範囲の確認: 常に有効なインデックスを指定すること。
  2. ループ内での要素削除: インデックスのずれに注意し、後ろからのループ、whileループとインデックス調整、またはイテレータのerase()の戻り値を利用する。
  3. ポインタの解放: リストがポインタを保持している場合は、removeAt() でポインタを削除するだけでなく、オブジェクト自体もdeleteで解放するか、スマートポインタを利用する。


QList::removeAt() は、指定されたインデックス位置の要素をリストから削除する関数です。基本的な使い方から、よくある落とし穴とその対処法まで、具体的なコードを見ていきましょう。

基本的な使い方

最もシンプルな例です。リストの特定のインデックスの要素を削除します。

#include <QCoreApplication>
#include <QList>
#include <QDebug> // デバッグ出力用

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

    QList<QString> fruits;
    fruits << "Apple" << "Banana" << "Cherry" << "Date" << "Elderberry";

    qDebug() << "オリジナルリスト:" << fruits;
    // 出力例: オリジナルリスト: ("Apple", "Banana", "Cherry", "Date", "Elderberry")

    // インデックス2(3番目の要素)の "Cherry" を削除
    // インデックスは0から始まるため、0, 1, 2...
    fruits.removeAt(2);

    qDebug() << "removeAt(2)後:" << fruits;
    // 出力例: removeAt(2)後: ("Apple", "Banana", "Date", "Elderberry")

    // インデックス0(最初の要素)の "Apple" を削除
    fruits.removeAt(0);

    qDebug() << "removeAt(0)後:" << fruits;
    // 出力例: removeAt(0)後: ("Banana", "Date", "Elderberry")

    // リストの最後の要素を削除(サイズ-1のインデックスを指定)
    // 現在のリストサイズは3なので、インデックス2を削除
    if (!fruits.isEmpty()) { // 空でないことを確認
        fruits.removeAt(fruits.size() - 1);
    }
    qDebug() << "最後の要素削除後:" << fruits;
    // 出力例: 最後の要素削除後: ("Banana", "Date")

    return a.exec();
}

解説: removeAt() を呼び出すと、指定したインデックスの要素が削除され、それより後の要素が前に詰められます。リストのサイズは1減少します。

インデックスの範囲外アクセス (Out-of-Bounds Access) の回避

誤ったインデックスを指定するとクラッシュの原因になります。size() 関数を使って範囲チェックを行うことが重要です。

#include <QCoreApplication>
#include <QList>
#include <QDebug>

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

    QList<int> numbers;
    numbers << 10 << 20 << 30;

    qDebug() << "オリジナルリスト:" << numbers;
    // 出力例: オリジナルリスト: (10, 20, 30)

    int validIndex = 1; // 有効なインデックス
    int invalidIndexTooHigh = 3; // 無効なインデックス (サイズを超えている)
    int invalidIndexNegative = -1; // 無効なインデックス (負の数)

    // 有効なインデックスでの削除
    if (validIndex >= 0 && validIndex < numbers.size()) {
        numbers.removeAt(validIndex);
        qDebug() << "有効なインデックス" << validIndex << "削除後:" << numbers;
        // 出力例: 有効なインデックス 1 削除後: (10, 30)
    } else {
        qDebug() << "インデックス" << validIndex << "は範囲外です。";
    }

    // 無効なインデックス (高すぎる) での削除試行
    if (invalidIndexTooHigh >= 0 && invalidIndexTooHigh < numbers.size()) {
        numbers.removeAt(invalidIndexTooHigh);
        qDebug() << "無効なインデックス" << invalidIndexTooHigh << "削除後:" << numbers;
    } else {
        qDebug() << "インデックス" << invalidIndexTooHigh << "は範囲外です。削除は行われません。";
        // 出力例: インデックス 3 は範囲外です。削除は行われません。
    }

    // 無効なインデックス (負の数) での削除試行
    if (invalidIndexNegative >= 0 && invalidIndexNegative < numbers.size()) {
        numbers.removeAt(invalidIndexNegative);
        qDebug() << "無効なインデックス" << invalidIndexNegative << "削除後:" << numbers;
    } else {
        qDebug() << "インデックス" << invalidIndexNegative << "は範囲外です。削除は行われません。";
        // 出力例: インデックス -1 は範囲外です。削除は行われません。
    }

    // 空のリストで削除を試みる
    QList<int> emptyList;
    qDebug() << "空のリスト:" << emptyList;
    if (0 >= 0 && 0 < emptyList.size()) { // emptyList.size() は 0 なので条件はfalse
        emptyList.removeAt(0);
        qDebug() << "空のリストから削除後:" << emptyList;
    } else {
        qDebug() << "空のリストからは要素を削除できません。";
        // 出力例: 空のリストからは要素を削除できません。
    }

    return a.exec();
}

解説: if (index >= 0 && index < list.size()) の条件式は、removeAt() を安全に呼び出すための基本的なチェックです。これを怠ると、特にデバッグビルドではアサートで停止し、リリースビルドでは不定な動作やクラッシュに繋がります。

ループ内での要素削除(インデックスのずれへの対処)

これが removeAt() を使う上での最も一般的な落とし穴です。リストを前から順番に処理しながら要素を削除すると、削除によってインデックスがずれて、期待通りの結果にならないことがあります。

誤った例(インデックスがずれる)
#include <QCoreApplication>
#include <QList>
#include <QDebug>

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

    QList<QString> items = {"apple", "banana", "orange", "grape", "banana", "kiwi"};
    qDebug() << "オリジナルリスト:" << items;

    // "banana" を削除しようとする(間違った方法)
    // 0: apple, 1: banana, 2: orange, 3: grape, 4: banana, 5: kiwi
    for (int i = 0; i < items.size(); ++i) { // ループ中にitems.size()が変わる
        if (items.at(i) == "banana") {
            items.removeAt(i);
            // 最初の"banana"(インデックス1)を削除すると、"orange"がインデックス1に、
            // "grape"がインデックス2に、"banana"がインデックス3にずれる。
            // しかし、iは増え続けるので、次のループではi=2となり、"grape"をチェックしてしまう。
            // 結果として、2番目の"banana"がスキップされる。
        }
    }
    qDebug() << "前方からループで削除後 (誤):" << items;
    // 出力例: 前方からループで削除後 (誤): ("apple", "orange", "grape", "banana", "kiwi")
    // 2番目の"banana"が残ってしまっている
    // QtのQListは要素数が変わるとQDebugの表示も変わるため、"banana"が残っていることが確認できる。

    return a.exec();
}
後ろからループする(推奨される解決策1)

要素を削除しても、それ以前のインデックスには影響がないため、リストの後ろから前に向かってループします。

#include <QCoreApplication>
#include <QList>
#include <QDebug>

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

    QList<QString> items = {"apple", "banana", "orange", "grape", "banana", "kiwi"};
    qDebug() << "オリジナルリスト:" << items;

    // "banana" を削除する(正しい方法:後ろからループ)
    for (int i = items.size() - 1; i >= 0; --i) {
        if (items.at(i) == "banana") {
            items.removeAt(i);
        }
    }
    qDebug() << "後方からループで削除後 (正):" << items;
    // 出力例: 後方からループで削除後 (正): ("apple", "orange", "grape", "kiwi")
    // すべての"banana"が削除された

    return a.exec();
}
while ループとインデックス調整(推奨される解決策2)

要素を削除した場合にのみインデックスを増やさないようにします。

#include <QCoreApplication>
#include <QList>
#include <QDebug>

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

    QList<QString> items = {"apple", "banana", "orange", "grape", "banana", "kiwi"};
    qDebug() << "オリジナルリスト:" << items;

    // "banana" を削除する(正しい方法:whileループとインデックス調整)
    int i = 0;
    while (i < items.size()) {
        if (items.at(i) == "banana") {
            items.removeAt(i);
            // 要素を削除したので、現在のiの位置に次の要素が来ている。
            // よって、iを増やさずに、再度同じiの位置をチェックする。
        } else {
            i++; // 要素を削除しなかった場合は、次の要素へ進む
        }
    }
    qDebug() << "whileループで削除後 (正):" << items;
    // 出力例: whileループで削除後 (正): ("apple", "orange", "grape", "kiwi")

    return a.exec();
}
イテレータを使用する (QList::erase())(推奨される解決策3)

イテレータは要素を削除した場合の正しい次のイテレータを返してくれるため、より安全でC++らしい方法です。

#include <QCoreApplication>
#include <QList>
#include <QDebug>

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

    QList<QString> items = {"apple", "banana", "orange", "grape", "banana", "kiwi"};
    qDebug() << "オリジナルリスト:" << items;

    // "banana" を削除する(正しい方法:イテレータとerase())
    // QList::iterator を使用
    for (QList<QString>::iterator it = items.begin(); it != items.end(); ) {
        if (*it == "banana") {
            // erase()は要素を削除し、削除された要素の次の要素のイテレータを返す
            it = items.erase(it);
        } else {
            ++it; // 要素を削除しなかった場合は、次の要素へ進む
        }
    }
    qDebug() << "イテレータとerase()で削除後 (正):" << items;
    // 出力例: イテレータとerase()で削除後 (正): ("apple", "orange", "grape", "kiwi")

    return a.exec();
}

解説: items.erase(it) は、it が指す要素を削除し、削除された要素の次の要素を指す新しいイテレータを返します。この戻り値を it に再代入することで、ループの継続条件が正しく保たれ、要素のスキップを防ぎます。

QList::removeIf (Qt 6以降) または std::remove_if と QList::erase
#include <QCoreApplication>
#include <QList>
#include <QDebug>
#include <algorithm> // std::remove_if に必要

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

    QList<QString> items1 = {"apple", "banana", "orange", "grape", "banana", "kiwi"};
    qDebug() << "オリジナルリスト1:" << items1;

    // std::remove_if と erase を使用
    // std::remove_if は、削除対象の要素をリストの末尾に移動させ、
    // 削除対象の最初の要素へのイテレータを返す。
    auto newEnd = std::remove_if(items1.begin(), items1.end(),
                                 [](const QString& s){ return s == "banana"; });
    items1.erase(newEnd, items1.end()); // 移動された削除対象要素を実際に削除
    qDebug() << "std::remove_if + erase で削除後:" << items1;
    // 出力例: std::remove_if + erase で削除後: ("apple", "orange", "grape", "kiwi")

    // Qt 6 以降では QList::removeIf が利用可能
    QList<QString> items2 = {"apple", "banana", "orange", "grape", "banana", "kiwi"};
    qDebug() << "オリジナルリスト2:" << items2;
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
    items2.removeIf([](const QString& s){ return s == "banana"; });
    qDebug() << "QList::removeIf で削除後:" << items2;
    // 出力例: QList::removeIf で削除後: ("apple", "orange", "grape", "kiwi")
#else
    qDebug() << "Qt 6 より前のバージョンでは QList::removeIf は利用できません。";
#endif

    return a.exec();
}

解説: これは "remove-erase idiom" と呼ばれるパターンです。std::remove_if は要素を削除するわけではなく、条件を満たす要素をリストの終端に移動させ、それらの要素の開始位置を指すイテレータを返します。その後、erase() を呼び出して、そのイテレータからリストの終端までの要素を実際に削除します。 Qt 6 からは、この処理をより簡潔に実行できる QList::removeIf が追加されました。

ポインタを保持する QList のメモリ管理

QList がポインタ(特にヒープ上に確保されたオブジェクトへのポインタ)を保持している場合、removeAt() はポインタを削除するだけで、指しているオブジェクトは解放しません。メモリリークを防ぐために、オブジェクトの解放も行う必要があります。

#include <QCoreApplication>
#include <QList>
#include <QDebug>

// 例として、MyObject クラスを定義
class MyObject {
public:
    QString name;
    MyObject(const QString& n) : name(n) {
        qDebug() << "MyObject" << name << "が作成されました。";
    }
    ~MyObject() {
        qDebug() << "MyObject" << name << "が削除されました。";
    }
};

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

    QList<MyObject*> objectList;

    objectList.append(new MyObject("One"));
    objectList.append(new MyObject("Two"));
    objectList.append(new MyObject("Three"));
    objectList.append(new MyObject("Four"));

    qDebug() << "\n-- リストから要素を削除し、オブジェクトを解放 --";
    // インデックス 1 (MyObject("Two")) を削除
    int indexToRemove = 1;
    if (indexToRemove >= 0 && indexToRemove < objectList.size()) {
        MyObject* objToDelete = objectList.at(indexToRemove); // 削除対象のポインタを取得
        objectList.removeAt(indexToRemove); // リストからポインタを削除
        delete objToDelete; // ポインタが指すオブジェクトを解放 (重要!)
        qDebug() << "インデックス" << indexToRemove << "の要素を削除しました。";
    }

    qDebug() << "\n-- 残りのリスト要素とオブジェクトをクリーンアップ --";
    // 残りのすべてのオブジェクトを解放し、リストをクリアする
    // qDeleteAll() はリスト内のすべてのポインタに対して delete を呼び出す便利な関数
    qDeleteAll(objectList); // 全てのMyObjectをdeleteする
    objectList.clear();     // リストからポインタを削除する

    qDebug() << "プログラム終了。";

    return a.exec();
}

実行結果の例:

MyObject "One" が作成されました。
MyObject "Two" が作成されました。
MyObject "Three" が作成されました。
MyObject "Four" が作成されました。

-- リストから要素を削除し、オブジェクトを解放 --
MyObject "Two" が削除されました。
インデックス 1 の要素を削除しました。

-- 残りのリスト要素とオブジェクトをクリーンアップ --
MyObject "One" が削除されました。
MyObject "Three" が削除されました。
MyObject "Four" が削除されました。
プログラム終了。

解説: objectList.removeAt(indexToRemove); だけではオブジェクト自体は解放されません。delete objToDelete; のように明示的に delete を呼び出すか、qDeleteAll() を使用して、ヒープ上のオブジェクトのメモリを解放する必要があります。 現代のC++では、このような手動のメモリ管理を避けるために std::unique_ptrstd::shared_ptr のようなスマートポインタの使用が強く推奨されます。



QList::removeFirst() / QList::removeLast()

リストの最初または最後の要素を削除する場合に便利です。removeAt(0)removeAt(list.size() - 1) と同じ機能ですが、より意図が明確で、末端での削除は QList の内部実装の最適化により非常に高速(定数時間 O(1))です。

機能:

  • removeLast(): リストの最後の要素を削除します。
  • removeFirst(): リストの最初の要素を削除します。

使用例:

#include <QCoreApplication>
#include <QList>
#include <QDebug>

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

    QList<QString> names;
    names << "Alice" << "Bob" << "Charlie" << "David";

    qDebug() << "オリジナルリスト:" << names;
    // 出力例: ("Alice", "Bob", "Charlie", "David")

    names.removeFirst(); // "Alice" を削除
    qDebug() << "removeFirst()後:" << names;
    // 出力例: ("Bob", "Charlie", "David")

    names.removeLast();  // "David" を削除
    qDebug() << "removeLast()後:" << names;
    // 出力例: ("Bob", "Charlie")

    // リストが空でないことを確認してから削除することが重要
    while (!names.isEmpty()) {
        names.removeFirst();
        qDebug() << "繰り返しremoveFirst()後:" << names;
    }
    // 出力例:
    // 繰り返しremoveFirst()後: ("Charlie")
    // 繰り返しremoveFirst()後: ()

    return a.exec();
}

QList::removeOne(const T &value)

指定された値と最初に一致した要素をリストから削除します。

機能:

  • 削除が成功した場合(要素が見つかった場合)は true を、見つからなかった場合は false を返します。
  • リスト内でvalueと等しい最初の要素を削除します。

使用例:

#include <QCoreApplication>
#include <QList>
#include <QDebug>

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

    QList<int> numbers;
    numbers << 10 << 20 << 30 << 20 << 40;

    qDebug() << "オリジナルリスト:" << numbers;
    // 出力例: (10, 20, 30, 20, 40)

    bool removed = numbers.removeOne(20); // 最初の20を削除
    qDebug() << "removeOne(20)後 (成功:" << removed << "):" << numbers;
    // 出力例: removeOne(20)後 (成功: true ): (10, 30, 20, 40)

    removed = numbers.removeOne(50); // 存在しない50を削除
    qDebug() << "removeOne(50)後 (成功:" << removed << "):" << numbers;
    // 出力例: removeOne(50)後 (成功: false ): (10, 30, 20, 40)

    return a.exec();
}

QList::removeAll(const T &value)

指定された値と一致するすべての要素をリストから削除します。

機能:

  • 削除された要素の数を返します。
  • リスト内でvalueと等しいすべての要素を削除します。

使用例:

#include <QCoreApplication>
#include <QList>
#include <QDebug>

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

    QList<QString> items;
    items << "apple" << "banana" << "orange" << "apple" << "grape" << "banana";

    qDebug() << "オリジナルリスト:" << items;
    // 出力例: ("apple", "banana", "orange", "apple", "grape", "banana")

    int removedCount = items.removeAll("banana"); // 全ての"banana"を削除
    qDebug() << "removeAll(\"banana\")後 (削除数:" << removedCount << "):" << items;
    // 出力例: removeAll("banana")後 (削除数: 2 ): ("apple", "orange", "apple", "grape")

    removedCount = items.removeAll("apple"); // 全ての"apple"を削除
    qDebug() << "removeAll(\"apple\")後 (削除数:" << removedCount << "):" << items;
    // 出力例: removeAll("apple")後 (削除数: 2 ): ("orange", "grape")

    removedCount = items.removeAll("mango"); // 存在しない"mango"を削除
    qDebug() << "removeAll(\"mango\")後 (削除数:" << removedCount << "):" << items;
    // 出力例: removeAll("mango")後 (削除数: 0 ): ("orange", "grape")

    return a.exec();
}

QList::takeAt(int i) / QList::takeFirst() / QList::takeLast()

これらのメソッドは、要素をリストから削除するだけでなく、削除された要素のコピーを返します。削除された要素の値を後で利用したい場合に非常に便利です。

機能:

  • takeLast(): 最後の要素を削除し、その要素のコピーを返します。
  • takeFirst(): 最初の要素を削除し、その要素のコピーを返します。
  • takeAt(int i): 指定されたインデックスの要素を削除し、その要素のコピーを返します。

注意:

  • 空のリストに対してこれらの関数を呼び出すと未定義の動作を引き起こす可能性があるため、isEmpty() でチェックすることが推奨されます。
  • 返されるのはコピーなので、リストがポインタを保持している場合、返されたポインタが指すオブジェクトを別途 delete する必要があります。

使用例:

#include <QCoreApplication>
#include <QList>
#include <QDebug>

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

    QList<QString> tasks;
    tasks << "Task A" << "Task B" << "Task C" << "Task D";

    qDebug() << "オリジナルタスクリスト:" << tasks;
    // 出力例: ("Task A", "Task B", "Task C", "Task D")

    // takeAt() の使用
    if (2 >= 0 && 2 < tasks.size()) { // インデックス範囲チェック
        QString completedTask = tasks.takeAt(2); // "Task C" を削除し、取得
        qDebug() << "完了したタスク (takeAt):" << completedTask;
        qDebug() << "残りタスクリスト:" << tasks;
        // 出力例:
        // 完了したタスク (takeAt): "Task C"
        // 残りタスクリスト: ("Task A", "Task B", "Task D")
    }

    // takeFirst() の使用
    if (!tasks.isEmpty()) {
        QString firstTask = tasks.takeFirst(); // "Task A" を削除し、取得
        qDebug() << "最初に完了したタスク (takeFirst):" << firstTask;
        qDebug() << "残りタスクリスト:" << tasks;
        // 出力例:
        // 最初に完了したタスク (takeFirst): "Task A"
        // 残りタスクリスト: ("Task B", "Task D")
    }

    // takeLast() の使用
    if (!tasks.isEmpty()) {
        QString lastTask = tasks.takeLast(); // "Task D" を削除し、取得
        qDebug() << "最後に完了したタスク (takeLast):" << lastTask;
        qDebug() << "残りタスクリスト:" << tasks;
        // 出力例:
        // 最後に完了したタスク (takeLast): "Task D"
        // 残りタスクリスト: ("Task B")
    }

    return a.exec();
}

QList::erase(iterator pos) / QList::erase(iterator begin, iterator end)

イテレータを使用して要素を削除します。特定の範囲の要素を削除する場合や、イテレータを使ったループで要素を削除する場合に非常に強力です。

機能:

  • erase(iterator begin, iterator end): begin から end (自身は含まない) までの範囲の要素を削除し、end が元々指していた要素(つまり削除範囲の次)を指すイテレータを返します。
  • erase(iterator pos): pos が指す要素を削除し、削除された要素の次の要素を指すイテレータを返します。

使用例:

#include <QCoreApplication>
#include <QList>
#include <QDebug>

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

    QList<double> data = {1.1, 2.2, 3.3, 4.4, 5.5, 6.6};
    qDebug() << "オリジナルデータ:" << data;
    // 出力例: (1.1, 2.2, 3.3, 4.4, 5.5, 6.6)

    // 単一要素の削除 (erase(pos))
    // イテレータでインデックス2 (3.3) を指す
    QList<double>::iterator it = data.begin() + 2; // begin()から2つ進める
    if (it != data.end()) { // 有効なイテレータか確認
        it = data.erase(it); // 3.3 を削除。itは現在 4.4 を指す
        qDebug() << "erase(pos)後:" << data;
        // 出力例: erase(pos)後: (1.1, 2.2, 4.4, 5.5, 6.6)
    }

    // 範囲削除 (erase(begin, end))
    // 2.2 から 5.5 (自身は含まない) までの範囲を削除
    // 現在のリスト: (1.1, 2.2, 4.4, 5.5, 6.6)
    QList<double>::iterator beginErase = data.begin() + 1; // 2.2 を指す
    QList<double>::iterator endErase = data.begin() + 4;   // 6.6 を指す(5.5の次)
    // 削除されるのは 2.2, 4.4, 5.5

    it = data.erase(beginErase, endErase);
    qDebug() << "erase(begin, end)後:" << data;
    // 出力例: erase(begin, end)後: (1.1, 6.6)
    // it は現在 6.6 を指している

    return a.exec();
}

解説: erase() は、removeAt() がインデックスで削除するのに対し、イテレータで削除します。イテレータを使ったループで条件に合う要素を削除する際には、removeAt() よりも推奨されるアプローチです。これは、erase() が次の有効なイテレータを返してくれるため、ループ中のインデックスのずれを心配する必要がないためです。

QList::clear()

リスト内のすべての要素を削除し、リストを空にします。

機能:

  • リストのサイズを0にします。

使用例:

#include <QCoreApplication>
#include <QList>
#include <QDebug>

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

    QList<QString> shoppingList;
    shoppingList << "Milk" << "Bread" << "Eggs";

    qDebug() << "オリジナルショッピングリスト:" << shoppingList;
    // 出力例: ("Milk", "Bread", "Eggs")

    shoppingList.clear(); // 全ての要素を削除

    qDebug() << "clear()後:" << shoppingList;
    // 出力例: clear()後: ()
    qDebug() << "リストは空か:" << shoppingList.isEmpty();
    // 出力例: リストは空か: true

    return a.exec();
}

QList::removeIf() (Qt 6以降)

C++11の std::remove_if に似た機能を提供し、条件に合う要素を簡潔に削除できます。

機能:

  • ラムダ式などの述語 (predicate) を受け取り、その述語が true を返す要素をすべて削除します。

使用例:

#include <QCoreApplication>
#include <QList>
#include <QDebug>

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

    QList<int> numbers = {1, 5, 10, 15, 20, 25, 30};
    qDebug() << "オリジナルリスト:" << numbers;

#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
    // 10より大きい要素を削除
    numbers.removeIf([](int val){ return val > 10; });
    qDebug() << "removeIf(val > 10)後:" << numbers;
    // 出力例: removeIf(val > 10)後: (1, 5, 10)

    // 偶数を削除
    QList<int> moreNumbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    qDebug() << "新しいリスト:" << moreNumbers;
    moreNumbers.removeIf([](int val){ return val % 2 == 0; });
    qDebug() << "removeIf(偶数)後:" << moreNumbers;
    // 出力例: removeIf(偶数)後: (1, 3, 5, 7, 9)
#else
    qDebug() << "Qt 6 より前のバージョンでは QList::removeIf は利用できません。";
    qDebug() << "代替として std::remove_if と erase を使用してください。";
    // Qt 5.x 以前の代替方法:
    // auto it = std::remove_if(numbers.begin(), numbers.end(),
    //                          [](int val){ return val > 10; });
    // numbers.erase(it, numbers.end());
#endif

    return a.exec();
}
  • 条件に基づいて複数の要素を効率的に削除したい場合: removeIf() (Qt 6+) または std::remove_iferase の組み合わせ。
  • イテレータを使ってループ中に削除する場合: erase(iterator pos) が最も安全で効率的です。
  • リスト全体を空にしたい場合: clear()
  • 削除した要素の値も取得したい場合: takeAt(), takeFirst(), takeLast()
  • 最初または最後の要素を削除したい場合: removeFirst(), removeLast()
  • 値で削除したい場合: removeOne() (最初の一つ), removeAll() (全て), removeIf() (条件に合う全て)
  • インデックスで削除したい場合: removeAt() (単一の要素) または erase(iterator begin, iterator end) (範囲)