もう迷わない!QMap::equal_range()とfind()、contains()の使い分け【Qtプログラミング】

2025-05-27

QMapはキーと値のペアを格納する連想コンテナですが、std::mapとは異なり、同じキーを複数持つことはできません。つまり、QMapは常にキーが一意であることを保証します。

このため、QMap::equal_range(const Key &key)を呼び出すと、戻り値は常に以下のいずれかになります。

  1. キーが存在しない場合
    - 最初のイテレータ(first)と2番目のイテレータ(second)はどちらも、指定されたキーが挿入されるべき位置を指します。この場合、first == second となります。

  2. キーが存在する場合
    - firstsecondはどちらも、指定されたキーを持つ唯一の要素を指します。この場合も、firstsecondは同じ要素を指すことになりますが、firstは要素自体を、secondはその要素の次の位置を指すため、厳密には first != second となります。ただし、範囲としては単一の要素を指します。

戻り値の型

equal_range()は、std::pair<QMap::iterator, QMap::iterator>(またはconst版ではstd::pair<QMap::const_iterator, QMap::const_iterator>)を返します。このstd::pairfirstメンバーは指定されたキー以上の最初の要素へのイテレータを、secondメンバーは指定されたキーより大きい最初の要素へのイテレータを指します。

QMapの特性上、キーが一意であるため、equal_range()が返す範囲は、最大でも1つの要素しか含まれません。

使用例

#include <QCoreApplication>
#include <QMap>
#include <QDebug>

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

    QMap<QString, int> myMap;
    myMap.insert("apple", 10);
    myMap.insert("banana", 20);
    myMap.insert("orange", 30);

    // "banana" の範囲を取得
    auto range = myMap.equal_range("banana");
    if (range.first != range.second) {
        qDebug() << "Key 'banana' found:";
        for (auto it = range.first; it != range.second; ++it) {
            qDebug() << "  Key:" << it.key() << ", Value:" << it.value();
        }
    } else {
        qDebug() << "Key 'banana' not found.";
    }

    // "grape" の範囲を取得 (存在しないキー)
    auto rangeNotFound = myMap.equal_range("grape");
    if (rangeNotFound.first != rangeNotFound.second) {
        qDebug() << "Key 'grape' found (unexpected):";
        for (auto it = rangeNotFound.first; it != rangeNotFound.second; ++it) {
            qDebug() << "  Key:" << it.key() << ", Value:" << it.value();
        }
    } else {
        qDebug() << "Key 'grape' not found (expected).";
    }

    return a.exec();
}

出力例

Key 'banana' found:
  Key: "banana" , Value: 20
Key 'grape' not found (expected).

QMapと非常によく似たクラスにQMultiMapがありますが、こちらは同じキーを複数持つことができます



QMultiMapとの混同による誤解

これは最も一般的な誤解です。

よくある誤解
equal_range()は、指定したキーと同じキーを持つすべての要素を返すだろう」

なぜ誤解か
QMapはキーの一意性を保証します。つまり、同じキーを持つ要素は1つしか存在しません。したがって、QMap::equal_range()は、そのキーが存在すればその単一の要素を指す範囲を返し、存在しなければ空の範囲を返します。複数の要素を期待している場合、それはQMultiMapの動作です。

トラブルシューティング

  • キーの設計を見直す
    もしQMapを使い続けたいが、異なる「値」を区別したいのであれば、キー自体をより詳細に設計できないかを検討します(例: QStringをキーにする場合、"item_1", "item_2"のようにする)。
  • 複数のキーを持つ必要があるか検討する
    もし本当に同じキーで複数の値を管理する必要があるなら、QMultiMapの使用を検討してください。QMultiMapであれば、equal_range()は期待通りの複数の要素を含む範囲を返します。
  • QMapの挙動を再確認する
    QMapはキーと値のペアを格納し、各キーは一度しか出現しません。新しい値が同じキーで挿入されると、古い値が上書きされます。

イテレータの無効化

QMapに要素を追加したり削除したりすると、既存のイテレータが無効になる可能性があります。これはequal_range()に限らず、イテレータを使用するQtコンテナ全般に言えることです。

問題
equal_range()で取得したイテレータのペアを保持しておき、その後マップの内容を変更すると、保持していたイテレータが無効になり、アクセス違反や予期せぬ動作を引き起こす可能性があります。

トラブルシューティング

  • ループ内でマップを変更しない
    イテレータを使ってマップをイテレートしている最中に、そのマップの要素を追加したり削除したりすることは避けるべきです(一部の関数は安全ですが、一般的には推奨されません)。もし変更する必要がある場合は、変更後にイテレータを再取得するか、std::mapのようにイテレータを返す削除関数を使用するなど、注意深く行う必要があります。
  • イテレータの寿命を短くする
    equal_range()で取得したイテレータは、その直後の処理でのみ使用し、マップが変更される可能性のある操作(挿入、削除など)の後は再取得するようにします。

キーの比較問題

QMapはキーの比較にoperator<を使用します。カスタム型をキーとして使用する場合、operator<を適切にオーバーロードしていないと、equal_range()が正しく動作しない可能性があります。

問題

  • 論理エラー
    operator<が正しく実装されていない(例えば、a < bb < aが同時に真になるなど)場合、QMapの内部構造が壊れ、検索が正しく行われない可能性があります。equal_range()は、要素が存在しないと判断したり、誤った要素を指したりする可能性があります。
  • コンパイルエラー
    operator<が定義されていない場合。

トラブルシューティング

  • qHash()の必要性
    QMapqHash()を直接使用しませんが、キーの同一性チェックにはoperator==も重要です。Qtのコンテナは一般的にoperator<operator==が正しく実装されていることを前提とします。
  • カスタムキー型のoperator<を実装する
    QMapのキーとして使用するすべてのカスタム型に対して、厳密弱順序(Strict Weak Ordering)を満たすoperator<を実装する必要があります。これは、以下の特性を持つ必要があります。
    • 非反射的
      !(x < x)
    • 非対称的
      x < yならば!(y < x)
    • 推移的
      x < yかつy < zならばx < z
    • 同値の推移的
      !(x < y)かつ!(y < x)ならば、!(x < z)かつ!(z < x)のとき!(y < z)かつ!(z < y)

equal_range()std::pair<Iterator, Iterator>を返します。このペアのfirst含まれるイテレータを、second含まれないイテレータを指します。

問題
firstsecondがどちらも同じ要素を指している場合(つまり、first == second)、「範囲に要素が含まれていない」と判断するべきなのに、「要素が見つかった」と誤解してしまうことがあります。特に、キーが存在しない場合、firstsecondはどちらもキーが挿入されるべき位置を指し、first == secondとなります。

  • ループ条件を正しく記述する
    for (auto it = range.first; it != range.second; ++it) のように、it != range.secondを終了条件として使うのが正しいです。
  • 常にfirst != secondで範囲の有効性をチェックする
    auto range = myMap.equal_range("key_to_find");
    if (range.first != range.second) {
        // キーが見つかった場合(QMapでは単一要素)
        qDebug() << "Key found:" << range.first.key() << range.first.value();
    } else {
        // キーが見つからなかった場合
        qDebug() << "Key not found.";
    }
    


基本的な使用例:キーの存在チェックと値の取得

最も基本的な使い方です。特定のキーがマップに存在するかどうかを確認し、存在する場合はその値を取得します。

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

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

    QMap<QString, int> studentScores;
    studentScores.insert("Alice", 95);
    studentScores.insert("Bob", 80);
    studentScores.insert("Charlie", 70);

    // 例 1: 存在するキーを検索
    QString searchName1 = "Bob";
    auto range1 = studentScores.equal_range(searchName1);

    if (range1.first != range1.second) {
        // range1.first は "Bob" の要素を指し、range1.second はその次の要素(または end())を指す
        // QMapではキーが一意なので、ループは一度しか実行されないか、単に range1.first を直接使っても良い
        qDebug() << searchName1 << " のスコアが見つかりました:";
        for (auto it = range1.first; it != range1.second; ++it) {
            qDebug() << "  名前: " << it.key() << ", スコア: " << it.value();
        }
    } else {
        qDebug() << searchName1 << " は見つかりませんでした。";
    }

    qDebug() << "---";

    // 例 2: 存在しないキーを検索
    QString searchName2 = "David";
    auto range2 = studentScores.equal_range(searchName2);

    if (range2.first != range2.second) {
        // このブロックは実行されない
        qDebug() << searchName2 << " のスコアが見つかりました (予期しない):";
        for (auto it = range2.first; it != range2.second; ++it) {
            qDebug() << "  名前: " << it.key() << ", スコア: " << it.value();
        }
    } else {
        // range2.first と range2.second は同じイテレータを指す (キーが挿入されるべき位置)
        qDebug() << searchName2 << " は見つかりませんでした。";
    }

    return a.exec();
}

出力例

"Bob" のスコアが見つかりました:
  名前:  "Bob" , スコア:  80
---
"David" は見つかりませんでした。

QMap::find()equal_range() の比較

QMap::find() はキーが存在すればその要素へのイテレータを返し、存在しなければ QMap::end() を返します。QMap の特性上、equal_range()find() と非常に似た働きをします。

#include <QCoreApplication>
#include <QMap>
#include <QDebug>

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

    QMap<int, QString> products;
    products.insert(101, "Laptop");
    products.insert(205, "Mouse");
    products.insert(310, "Keyboard");

    int productId = 205;

    // find() を使った方法
    QMap<int, QString>::iterator it_find = products.find(productId);
    if (it_find != products.end()) {
        qDebug() << "find() で見つかりました: ID =" << it_find.key() << ", 名前 =" << it_find.value();
    } else {
        qDebug() << "find() で見つかりませんでした: ID =" << productId;
    }

    // equal_range() を使った方法
    auto range_equal_range = products.equal_range(productId);
    if (range_equal_range.first != range_equal_range.second) {
        qDebug() << "equal_range() で見つかりました: ID =" << range_equal_range.first.key() << ", 名前 =" << range_equal_range.first.value();
    } else {
        qDebug() << "equal_range() で見つかりませんでした: ID =" << productId;
    }

    qDebug() << "---";

    productId = 999; // 存在しないID

    // find() を使った方法
    it_find = products.find(productId);
    if (it_find != products.end()) {
        qDebug() << "find() で見つかりました (予期しない): ID =" << it_find.key() << ", 名前 =" << it_find.value();
    } else {
        qDebug() << "find() で見つかりませんでした: ID =" << productId;
    }

    // equal_range() を使った方法
    range_equal_range = products.equal_range(productId);
    if (range_equal_range.first != range_equal_range.second) {
        qDebug() << "equal_range() で見つかりました (予期しない): ID =" << range_equal_range.first.key() << ", 名前 =" << range_equal_range.first.value();
    } else {
        qDebug() << "equal_range() で見つかりませんでした: ID =" << productId;
    }

    return a.exec();
}

出力例

find() で見つかりました: ID = 205 , 名前 = "Mouse"
equal_range() で見つかりました: ID = 205 , 名前 = "Mouse"
---
find() で見つかりませんでした: ID = 999
equal_range() で見つかりませんでした: ID = 999

この例からわかるように、QMapの場合、find()equal_range()の機能的な違いはほとんどありません。find()の方がシンプルに記述できるため、単一の要素を検索する場合はfind()を使うことが多いでしょう。equal_range()は、QMultiMapで複数の要素を扱う場合にその真価を発揮します。

constQMap オブジェクトに対して equal_range() を呼び出す場合、返されるイテレータは const_iterator になります。これにより、マップの内容を変更せずに読み取り専用アクセスが保証されます。

#include <QCoreApplication>
#include <QMap>
#include <QDebug>

// QMap を const 参照で受け取る関数
void printMapEntry(const QMap<QString, double>& dataMap, const QString& key)
{
    // const QMap から equal_range() を呼び出すと const_iterator が返される
    std::pair<QMap<QString, double>::const_iterator, QMap<QString, double>::const_iterator> range = dataMap.equal_range(key);

    if (range.first != range.second) {
        // const_iterator なので、キーや値を変更することはできない
        qDebug() << "Key '" << key << "' found. Value: " << range.first.value();
    } else {
        qDebug() << "Key '" << key << "' not found.";
    }
}

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

    QMap<QString, double> prices;
    prices.insert("milk", 1.25);
    prices.insert("bread", 2.50);
    prices.insert("eggs", 3.00);

    printMapEntry(prices, "bread");
    printMapEntry(prices, "sugar"); // 存在しないキー

    return a.exec();
}
Key ' milk ' found. Value:  1.25
Key ' bread ' found. Value:  2.5
Key ' eggs ' found. Value:  3
Key ' sugar ' not found.


QMap::find() を使用する (最も一般的)

QMap::find() は、指定されたキーを持つ要素へのイテレータを返します。キーが見つからない場合は、QMap::end() を返します。これは、equal_range() と同様に要素の存在チェックとアクセスに使え、より簡潔です。

equal_range() との類似点

  • どちらもキーが見つからない場合の処理が必要。
  • どちらもイテレータを返す。

equal_range() との違い

  • equal_range() はイテレータのペアを返す。
  • find() は単一のイテレータを返す。

コード例

#include <QCoreApplication>
#include <QMap>
#include <QDebug>

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

    QMap<QString, int> scores;
    scores.insert("Alice", 95);
    scores.insert("Bob", 80);

    QString searchKey = "Alice";
    QMap<QString, int>::iterator it = scores.find(searchKey); // find() を使用

    if (it != scores.end()) {
        qDebug() << searchKey << "のスコアが見つかりました: " << it.value();
    } else {
        qDebug() << searchKey << "は見つかりませんでした。";
    }

    searchKey = "David";
    it = scores.find(searchKey);
    if (it != scores.end()) {
        qDebug() << searchKey << "のスコアが見つかりました: " << it.value();
    } else {
        qDebug() << searchKey << "は見つかりませんでした。";
    }

    return a.exec();
}

出力例

Alice のスコアが見つかりました:  95
David は見つかりませんでした。

QMap::contains() と QMap::value() を組み合わせる

特定のキーが存在するかどうかだけを確認し、存在すればその値を取得する場合、contains()value() を組み合わせるのが非常に読みやすく、一般的です。

equal_range() との比較

  • value() は直接値を返すため、イテレータを介する必要がありません。
  • contains() はブール値を返すため、キーの存在チェックが直感的です。

注意点
operator[] を使うこともできますが、キーが存在しない場合にデフォルトコンストラクタで新しい要素を挿入してしまうため、意図しない変更を避けるためにも、通常はcontains()value() の組み合わせが推奨されます。

コード例

#include <QCoreApplication>
#include <QMap>
#include <QDebug>

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

    QMap<QString, double> productPrices;
    productPrices.insert("Apple", 1.50);
    productPrices.insert("Banana", 0.75);

    QString searchProduct = "Apple";
    if (productPrices.contains(searchProduct)) { // キーの存在を確認
        double price = productPrices.value(searchProduct); // 値を取得
        qDebug() << searchProduct << "の価格: " << price;
    } else {
        qDebug() << searchProduct << "は見つかりませんでした。";
    }

    searchProduct = "Orange";
    if (productPrices.contains(searchProduct)) {
        double price = productPrices.value(searchProduct);
        qDebug() << searchProduct << "の価格: " << price;
    } else {
        qDebug() << searchProduct << "は見つかりませんでした。";
    }

    // value() のデフォルト値オーバーロードも便利
    double orangePrice = productPrices.value("Orange", -1.0); // 見つからない場合は -1.0 を返す
    qDebug() << "Orange の価格 (見つからない場合は -1.0): " << orangePrice;

    return a.exec();
}

出力例

Apple の価格:  1.5
Orange は見つかりませんでした。
Orange の価格 (見つからない場合は -1.0):  -1

QMap::lowerBound() と QMap::upperBound() を個別に利用する

equal_range() は実質的に lowerBound()upperBound() を呼び出してペアを返します。キーが存在すれば lowerBound() はその要素を、upperBound() はその次の要素を指します。キーが存在しない場合は、どちらもキーが挿入されるべき位置を指します。

QMap の場合、equal_range() と同じ結果を得られますが、コードが冗長になるため、単一の要素検索ではあまり使われません。しかし、より複雑な範囲ベースの操作や、QMultiMap への移行を念頭に置いている場合は、これらの関数を個別に理解しておくことが役立ちます。

コード例

#include <QCoreApplication>
#include <QMap>
#include <QDebug>

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

    QMap<int, QString> itemMap;
    itemMap.insert(10, "Pen");
    itemMap.insert(20, "Notebook");
    itemMap.insert(30, "Eraser");

    int searchId = 20;

    // lowerBound() と upperBound() を使用
    QMap<int, QString>::iterator lower = itemMap.lowerBound(searchId);
    QMap<int, QString>::iterator upper = itemMap.upperBound(searchId);

    if (lower != upper) {
        qDebug() << "ID" << searchId << " のアイテムが見つかりました:";
        for (auto it = lower; it != upper; ++it) {
            qDebug() << "  ID: " << it.key() << ", アイテム: " << it.value();
        }
    } else {
        qDebug() << "ID" << searchId << " は見つかりませんでした。";
    }

    searchId = 25; // 存在しないID
    lower = itemMap.lowerBound(searchId);
    upper = itemMap.upperBound(searchId);

    if (lower != upper) {
        qDebug() << "ID" << searchId << " のアイテムが見つかりました (予期しない):";
    } else {
        qDebug() << "ID" << searchId << " は見つかりませんでした。";
    }

    return a.exec();
}
ID 20  のアイテムが見つかりました:
  ID:  20 , アイテム:  "Notebook"
ID 25  は見つかりませんでした。
  • 汎用的なコード (QMapとQMultiMapの両方に対応する可能性)
    • equal_range()lowerBound() / upperBound() を使用すると、将来的に QMultiMap に変更する際にコードの変更が少なくて済みます。
  • 単一の要素の存在チェックと取得
    • 最も推奨
      QMap::contains()QMap::value() の組み合わせ。読みやすく、意図が明確。
    • 次点
      QMap::find()。イテレータが必要な場合や、QMultiMap と同様のイテレータ操作をしたい場合に便利。
    • operator[] は新しい要素の挿入を防ぐために、キーの存在確認と組み合わせる場合にのみ使用を検討。