Does std::vector::emplace() really offer the strong exception guarantee in the face of a throwing move constructor/assignment operator?
Asked Answered
F

1

8

According to cppreference.com, std::vector::emplace() offers the strong exception guarantee unconditionally:

If an exception is thrown (e.g. by the constructor), the container is left unmodified, as if this function was never called (strong exception guarantee).

However, this doesn't seem to be the case in practice with GCC 7.1.1. The following program:

#include <iostream>
#include <vector>

struct ugly
{
  int i;

  ugly(int i) : i{i} { }

  ugly(const ugly& other) = default;

  ugly& operator=(ugly&& other) {
    if (other.i == 3) {
      throw other.i;
    }
    i = other.i;
    return *this;
  }

  ugly& operator=(const ugly& other) = default;
};

int main() {
  std::vector<ugly> vec;
  vec.reserve(6);
  vec.emplace_back(0);
  vec.emplace_back(1);
  vec.emplace_back(2);
  vec.emplace_back(4);
  vec.emplace_back(5);

  try {
    vec.emplace(vec.begin() + 3, 3);
  } catch (int i) {
  }

  for (const auto& u : vec) {
    std::cout << u.i << "\n";
  }

  return 0;
}

prints

0
1
2
4
4
5

In fact, I have a hard time seeing how emplace() could possibly provide the strong guarantee if copying/moving is allowed to throw. To emplace in the middle, we have to move a bunch of elements out of the way first, then construct the new element in its place. If any of that throws, we'd have to move all the other elements back where they were, but those moves can throw too!

So who's wrong, cppreference or gcc?

Firry answered 16/7, 2017 at 4:53 Comment(2)
Downvote for misquoting cppreference. The guarantee you quote is for emplace_back and (sorry, correction) is followed by additional restrictions.Officialdom
@ArneVogel The page was updated since I wrote this questionFirry
A
9

According to the C++14 standard the strong exception guarantee only holds if the type you insert itself has a strong exception guarantee.

Here:

23.3.6.5 vector modifiers [ vector.modifiers ]

iterator insert(const_iterator position, const T& x);
iterator insert(const_iterator position, T&& x);
iterator insert(const_iterator position, size_type n, const T& x);
template <class InputIterator>
iterator insert(const_iterator position, InputIterator first, InputIterator last);
iterator insert(const_iterator position, initializer_list<T>);
template <class... Args> void emplace_back(Args&&... args);
template <class... Args> iterator emplace(const_iterator position, Args&&... args);
void push_back(const T& x);
void push_back(T&& x);

1 Remarks: Causes reallocation if the new size is greater than the old capacity. If no reallocation happens, all the iterators and references before the insertion point remain valid. If an exception is thrown other than by the copy constructor, move constructor, assignment operator, or move assignment operator of T or by any InputIterator operation there are no effects. If an exception is thrown while inserting a single element at the end and T is CopyInsertable or is_nothrow_move_constructible::value is true, there are no effects. Otherwise, if an exception is thrown by the move constructor of a non-CopyInsertable T, the effects are unspecified.

So it looks like cppreference.com is wrong.

Anorexia answered 16/7, 2017 at 5:9 Comment(2)
@TavianBarnes "other than by the ... move assignment operator of T". Your example throws an exception in its move assignment operator.Anorexia
Right, so that sentence doesn't apply. But neither do any of the others, so nothing puts any constraints on the behaviour of emplace() in this case I guess. Thanks!Firry

© 2022 - 2024 — McMap. All rights reserved.