Skip to content

Latest commit

 

History

History
89 lines (67 loc) · 4.19 KB

map_subscript.md

File metadata and controls

89 lines (67 loc) · 4.19 KB

operator [] ассоциативных контейнеров

Удивительное дело, но в этой заметке не будет ничего, связанного с неопределенным поведением. По крайней мере напрямую.

В стандартной библиотеке C++ много неоднозначных решений. Одно из таких: объединить операцию вставки в ассоциативный контейнер и получения элемента.

operator [] для ассоциативных контейнеров, пытается вызвать конструктор по умолчанию для элемента, если не находит переданный ключ.

С одной стороны это удобно:

std::map<Word, int> counts;
for (Word c : text) {
    ++counts[word]; // ровно один поиск по ключу 
}

В иных языках придется постараться, чтобы записать то же самое и не допустить повторного поиска.

map.put(key, map.containsKey(key) ? map.get(key) + 1 : 1); // поиск трижды!
map.put(key, map.getOrDefault(key, 0) + 1); // поиск дважды!

Оно, конечно, может быть, отоптимизируется JIT-компилятором... Но мы в C++ любим гарантии.

С другой стороны, этот вызов конструктора, если элемент не найден, может выйти боком:

struct S {
    int x;
    explicit S (int x) : x {x} {}
};

std::map<int, S> m { { 1, S{2} }}; // Ok
m[0] = S(5); // огромная трудночитаемая ошибка компиляции
auto s = m[1]; // опять огромная трудночитаемая ошибка компиляции

//-------------------
struct Huge {
    Huge() { 
        data.reserve(4096);
    }

    std::vector<int> data;
};

std::map<int, Huge> m;
Huge h; 
... /* заполняем h */
m[0] = std::move(h); // бесполезный вызов default-конструктора, лишняя аллокация, 
                     // а потом перемещение

Чтобы выпутаться из этой неприятности, у ассоциативных контейнеров к C++17 (20) наплодили целую гору методов insert_or_assign, try_emplace и insert с непонятным для непосвященных возвращаемым значением pair<iterator, bool>.

Всем этим добром, конечно же, пользоваться тяжело и неудобно. Про них пишут длинные статьи в блоги о том, как эффективно пользоваться поиском по контейнерам...

С operator[], конечно же, проще, «понятнее» и короче. Но и ловушка для невнимательных. А если еще и с мерзопакостными особенностями других объектов скрестить...

std::map<std::string, std::string> options {
    {"max_value" : "1000"};
}
....
const auto ParseInt = [](std::string s) {
    std::istringstream iss(s);
    int val;
    iss >> val;
    return val;
};

const int value = ParseInt(options["min_value"]); //!перепутали !нет такого поля
// value == 0. Все "ok". Счастливой отладки
// operator[] вернул пустую строку
// >> сфейлился и записал ноль в результат

Избежать неприятностей с operator[] для ассоциативных контейнеров можно, навесив const. И тогда вам этот оператор доступен не будет. И придется использовать либо .at, бросающий исключения. Либо всеми любимый:

if (auto it = m.find(key); it != m.end()) {
    // делай что хочешь с *it, it->second
}

Все просто.