Подтвердить что ты не робот

Безопасность исключений на С++ при добавлении в несколько контейнеров std

У меня есть код, который добавляет к std::vector и std::map после создания объекта.

v.push_back(object);     // std::vector
m[object->id] = object;  // std::map

Я хочу, чтобы у этого была сильная гарантия исключения. Обычно для выполнения таких операций, как эти атомы, я бы использовал метод свопинга для каждого контейнера и вызывал все функции, которые могли бы бросать временные копии контейнера:

vector temp_v(v);
map temp_map(m);

temp_v.push_back(object);
temp_m[object->id] = object;

// The swap operations are no-throw
swap(temp_v, v)
swap(temp_m, m)

Однако создание временных копий всего вектора и карты кажется очень дорогостоящим. Есть ли способ реализовать надежную гарантию исключения для этой функции без дорогостоящих копий?

4b9b3361

Ответ 1

Общий случай

Технически требуется только одна копия:

  • Скопировать вектор
  • Обновить копию
  • Обновить карту
  • Поменяйте копию и исходный вектор

Другим вариантом является catch-roll-back-and-rethrow:

  v.push_back(object);
  try
  {
    m.insert(object->id, object); // Assuming it cannot be present yet
  }
  catch(..)
  {
    v.pop_back();
    throw;
  }

Или наоборот. Я выбрал этот порядок, потому что vector::pop_back() гарантированно не сработает.

UPDATE. Если присутствует объект- > id, см. Grizzly answer для решения.


Конкретный пример для указателей

Однако, поскольку вы используете object->, вы можете хранить указатели. Копировать-конструктор указателя нельзя выбросить, и мы можем использовать этот факт для упрощения кода:

v.reserve(v.size() + 1);
m[object->id] = object; // can throw, but not during the assignment
v.push_back(object); // will not throw: capacity is available, copy constructor does not throw

И если вы действительно обеспокоены частым изменением размера:

if (v.capacity() == v.size()) v.resize(v.size()*2); // or any other growth strategy
m[object->id] = object;
v.push_back(object);

Ответ 2

Я думаю, что это ситуация, когда использование try-catch - правильный способ обращения с ней. Если доступ к map отменяет отмену операции на vector и ретроне:

v.push_back(object);  
try 
{
  m[object->id] = object;
} 
catch(...)
{
  v.pop_back();
  throw;
}

Однако это все равно не даст вам надежной гарантии, поскольку operator[] on maps является проблематичной инструкцией по безопасности исключений (если элемент не находится в map, объект по умолчанию сконфигурирован, который останется в карту, если operator= бросает (очень маловероятно в этом случае, поскольку вы, похоже, сохраняете указатели, но все же).

Поэтому я бы переписал его как

try 
{
  auto iter = m.find(object->id);//used auto for shorter code,
  if(iter == m.end())
    m.insert(std::make_pair(object->id, object);
 else
   iter->second = object;
}

Ответ 3

Вы можете использовать объект scopeguard, который откатывает операции при уничтожении, если только это не будет сказано. Этот подход описан в Generic: измените способ написания кода с исключительным кодом - навсегда.

например. что-то вроде:

container1.push_back(a);
Guard g(bind(&ContainerType::pop_back, &container1));
container2.push_back(a);
// ...
g.dismiss();

Ответ 4

Я полагаю, вы могли бы свернуть свой собственный объект типа RAII:

template<typename T>
class reversible_vector_pusher
{
  private:
    std::vector<T> * const ptrV;
    bool committed = false;
  public:
    reversible_vector_pusher(std::vector<T> & v, const T & obj) : ptrV(&v)
        { v.push_back(obj); }
    void commit()
        { committed = true; }
    ~reversible_vector_pusher()
    {
        if(! committed)
             ptrV->pop_back();
    }
};



reversible_vector_pusher<...> rvp(v, object); // replace ... with object type
m[object->id] = object;
rvp.commit();

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