В std::map начиная с C++17 есть отличный метод try_emplace. Он особенно хорош, когда конструирование mapped_type очень дешевое. Например, когда в качестве ключа у нас int, а в качестве значения -- структура с несколькими int-ами внутри. Тогда получается эффективно: попробовали вставить, если ключа в map еще нет, то из параметров сконструировали mapped_type и добавили в map новое значение. А если же в map ключ уже есть, то передача в try_emplace нескольких int-ов как параметров для конструктора mapped_type -- это копейки, на которые во многих случаях можно просто не обращать внимания.
Но вот когда у нас в качестве mapped_type какой-то "тяжелый" объект, вот тогда ситуация грустнее. Например, mapped_type -- это std::unique_ptr с указателем на класс с кучей собственных контейнеров внутри.
Если мы напишем что-то вроде:
|
auto [it, inserted] = my_map.try_emplace(key, std::make_unique<heavy_object>(...)); |
то это явная пессимизация -- ведь нам придется создавать heavy_object при каждом обращении к try_emplace, даже если ключ в map уже есть.
Я вижу два стандартных пути выхода из этой ситуации.
Во-первых, мы можем пойти классическим способом, через find с последующим emplace, если find завершился неудачно:
|
auto it = my_map.find(key); if(it == my_map.end()) { it = my_map.emplace(key, std::make_unique<heavy_object>(...)).first; } // Теперь it указывает на объект внутри std::map. |
Но здесь плохо то, что для вставки объекта поиск по std::map нужно будет делать дважды.
Во-вторых, в try_emplace можно передать пустой unique_ptr, а сам объект создать уже после вставки. Т.е. что-то вроде:
|
auto [it, inserted] = my_map.try_emplace(key, std::unique_ptr<heavy_object>{}); if(inserted) { // Была вставка. Теперь у нас в my_map лежит нулевой указатель, нужно // это исправить. it->second = std::make_unique<heavy_object>(...); } |
Но здесь плохо то, что нам нужно позаботиться об exception safety, ведь вызов make_unique может бросать исключение. И самое худшее, что можно сделать, это написать что-то вроде:
|
auto [it, inserted] = my_map.try_emplace(key, std::unique_ptr<heavy_object>{}); if(inserted) { // Была вставка. Теперь у нас в my_map лежит нулевой указатель, нужно // это исправить. try { it->second = std::make_unique<heavy_object>(...); } catch(...) { // Удаляем только что вставленный пустой указатель. my_map.erase(it); throw; } } |
Гораздо лучше было бы иметь что-то вроде scope(failure) из D. Что-то вроде:
|
auto [it, inserted] = my_map.try_emplace(key, std::unique_ptr<heavy_object>{}); if(inserted) { // Была вставка. Теперь у нас в my_map лежит нулевой указатель, нужно // это исправить. // Защищаемся от исключений. auto guard = at_failure( // Эта лямбда будет вызвана если выход из скоупа произойдет // из-за исключения. [&it, &my_map]() { my_map.erase(it); }); it->second = std::make_unique<heavy_object>(...); } |
Если бы дело касалось SObjectizer-а, то там бы я написал так с использованием уже имеющегося инструмента:
|
auto [it, inserted] = my_map.try_emplace(key, std::unique_ptr<heavy_object>{}); if(inserted) { // Была вставка. Теперь у нас в my_map лежит нулевой указатель, нужно // это исправить. // Защищаемся от исключений. do_with_rollback_on_exception( // Что хотим сделать. [&it, ...]() { it->second = std::make_unique<heavy_object>(...); }, // Эта лямбда будет вызвана если первая лямбда бросит исключение. [&it, &my_map]() { my_map.erase(it); }); } |
Но все эти приседания были бы не нужны, если бы был вариант try_emplace, который бы принимал не аргументы для конструктора mapped_type, а фабрику, которая может породить экземпляр mapped_type для вставки:
|
auto [it, inserted] = my_map.try_emplace_with_factory(key, // Эта лямбда будет вызвана, если объекта в map нет. [...]() { return std::make_unique<heavy_object>(...); }); |
К сожалению, такого варианта try_emplace_with_factory в std::map нет.
PS. Вышесказанное относится и к std::unordered_map.
Upd. В обсужении на LinkedIn посоветовали обходной маневр вида:
|
#include <map> #include <string> class simple_data { std::string _data; public: simple_data(const char * s) : _data{ s } {} }; class data_holder { std::string _data; public: template<typename... Args> data_holder(Args && ...args) : _data{ std::forward<Args>(args)... } {} }; template<typename F> struct deferred { F _f; template<typename T> operator T() { return _f(); } }; int main() { std::map<int, simple_data> m1; m1.try_emplace(0, deferred{ []{ return simple_data{ "Hello, world" }; } }); std::map<int, data_holder> m2; // m2.try_emplace(0, deferred{ []{ return std::string{ "Hello, world" }; } }); m2.try_emplace(0, "Hello, world"); } |
Но не для всех случаев он будет работать. В частности для data_holder-а не сработает, т.к. у data_holder-а есть шаблонный конструктор (цынк).
