Реализация std::vector::push_back безопасности сильных исключений
Я внедряю свой собственный вектор, основанный на проекте Сан-Диего после 2018 года ( N4791), и у меня есть несколько вопросов относительно реализации строгой безопасности исключений.
Вот некоторый код:
template <typename T, typename Allocator>
void Vector<T, Allocator>::push_back(const T& value)
{
if (buffer_capacity == 0)
{
this->Allocate(this->GetSufficientCapacity(1));
}
if (buffer_size < buffer_capacity)
{
this->Construct(value);
return;
}
auto new_buffer = CreateNewBuffer(this->GetSufficientCapacity(
buffer_size + 1), allocator);
this->MoveAll(new_buffer);
try
{
new_buffer.Construct(value);
}
catch (...)
{
this->Rollback(new_buffer, std::end(new_buffer));
throw;
}
this->Commit(std::move(new_buffer));
}
template <typename T, typename Allocator>
void Vector<T, Allocator>::Allocate(size_type new_capacity)
{
elements = std::allocator_traits<Allocator>::allocate(allocator,
new_capacity);
buffer_capacity = new_capacity;
}
template <typename T, typename Allocator> template <typename... Args>
void Vector<T, Allocator>::Construct(Args&&... args)
{
// TODO: std::to_address
std::allocator_traits<Allocator>::construct(allocator,
elements + buffer_size, std::forward<Args>(args)...);
++buffer_size;
}
template <typename T, typename Allocator>
Vector<T, Allocator> Vector<T, Allocator>::CreateNewBuffer(
size_type new_capacity, const Allocator& new_allocator)
{
Vector new_buffer{new_allocator};
new_buffer.Allocate(new_capacity);
return new_buffer;
}
template <typename T, typename Allocator>
void Vector<T, Allocator>::Move(iterator first, iterator last, Vector& buffer)
{
if (std::is_nothrow_move_constructible_v<T> ||
!std::is_copy_constructible_v<T>)
{
std::move(first, last, std::back_inserter(buffer));
}
else
{
std::copy(first, last, std::back_inserter(buffer));
}
}
template <typename T, typename Allocator
void Vector<T, Allocator>::MoveAll(Vector& buffer)
{
Move(std::begin(*this), std::end(*this), buffer);
}
template <typename T, typename Allocator>
void Vector<T, Allocator>::Rollback(Vector& other, iterator last) noexcept
{
if (!std::is_nothrow_move_constructible_v<T> &&
std::is_copy_constructible_v<T>)
{
return;
}
std::move(std::begin(other), last, std::begin(*this));
}
template <typename T, typename Allocator>
void Vector<T, Allocator>::Commit(Vector&& other) noexcept
{
this->Deallocate();
elements = other.elements;
buffer_capacity = other.buffer_capacity;
buffer_size = other.buffer_size;
allocator = other.allocator;
other.elements = nullptr;
other.buffer_capacity = 0;
other.buffer_size = 0;
}
Я вижу 2 проблемы с этим кодом. Я пытался следовать std::move_if_noexcept
логика, но что делать, если элемент не движется конструктивно, но allocator_traits::construct
Выдает исключение, скажем, в некотором журнальном коде внутри собственного распределителя? Тогда мой MoveAll
колл скину и выдаст только базовую гарантию. Это дефект в стандарте? Должны ли быть более строгие формулировки Allocator::construct
?
И еще один в Rollback
, Это действительно дает сильную гарантию, только если перемещенные элементы не могут быть назначены на ход. Иначе опять же только базовая гарантия. Это так и должно быть?
1 ответ
На основе диапазона std::move/copy
функции не способны обеспечить надежную исключительную гарантию. В случае исключения вам нужен итератор для последнего элемента, который был успешно скопирован / перемещен, чтобы вы могли отменить все правильно. Вы должны сделать копирование / перемещение вручную (или написать специальную функцию для этого).
Что касается подробностей вашего вопроса, стандарт на самом деле не касается того, что должно произойти, если construct
выдает исключение, которое не выдается из конструктора строящегося объекта. Цель стандарта (по причинам, которые я объясню ниже), вероятно, заключается в том, что это обстоятельство никогда не должно происходить. Но мне еще предстоит найти какое-либо утверждение в стандарте по этому поводу. Итак, давайте на минутку предположим, что это должно быть возможно.
Для того, чтобы контейнеры, осведомленные о распределителе, могли предложить гарантию сильного исключения, construct
по крайней мере, не должны бросать после создания объекта. В конце концов, вы не знаете, какое исключение было сгенерировано, иначе вы бы не смогли определить, был ли объект успешно построен или нет. Это сделает реализацию стандартного требуемого поведения невозможной. Итак, давайте предположим, что пользователь не сделал то, что делает реализацию невозможной.
Учитывая это обстоятельство, вы можете написать свой код, предполагая, что любое исключение construct
означает, что объект не был построен. Если construct
выдает исключение, несмотря на то, что ему даны аргументы, noexcept
конструктор, тогда вы предполагаете, что конструктор никогда не вызывался. И вы пишете свой код соответственно.
В случае копирования вам нужно только удалить все уже скопированные элементы (в обратном порядке, конечно). Ход дела немного сложнее, но все же вполне выполнимо. Вы должны переместить-назначить каждый успешно перемещенный объект обратно в исходное положение.
Эта проблема? vector<T>::*_back
не требует, чтобы T
быть MoveAssignable. Это только требует, чтобы T
быть Move Insertable: то есть вы можете использовать распределитель для создания их в неинициализированной памяти. Но вы не перемещаете это в неинициализированную память; вам нужно переместить его туда, куда переехал T
уже существует. Таким образом, чтобы сохранить это требование, вам нужно будет уничтожить все T
s, которые были успешно перемещены, а затем MoveInsert их обратно на место.
Но так как MoveInsertion требует использования construct
, который, как было установлено ранее, может бросить... ой. Действительно, именно поэтому vector
Функции перераспределения не перемещаются, если тип не является перемещаемым или не копируемым (а если это последний случай, вы не получаете гарантию сильного исключения).
Так что мне кажется довольно ясным, что любой распределитель construct
стандарт ожидает, что метод будет выбрасывать только если выбрасывает выбранный конструктор. Нет другого способа реализовать требуемое поведение в vector
, Но, учитывая, что нет четкого изложения этого требования, я бы сказал, что это недостаток в стандарте. И это не новый дефект, так как я посмотрел стандарт C++17, а не рабочий документ.
По-видимому, с 2014 года этот вопрос стал предметом LWG, и его решение было... хлопотным.