如何安全地处理容器类中的方法/异常?

How do I handle thows/exceptions in a container class safely?

本文关键字:方法 异常 容器类 处理 何安全 安全      更新时间:2023-10-16

我目前正在研究一个使用分配器来管理资源的容器类。我将尝试给出一个简短的版本,我目前做什么来调整容器的大小。(实际的不是一维的,但方案是相同的,因为分配的数据是连续的。)

所有我不清楚的都标记为[[[x]]]。

示例代码

template<typename T>
class example
// ...

指出:

  • size_type === std::allocator::size_type
  • pointer === std::allocator::pointer
  • _A === object of std::allocator
  • _begin是当前容器数据开始的类成员([_begin,_end))
  • size()返回(_end - _begin)
  • clear()对[_begin,_end)和_A.deallocate(_begin,size())中的所有元素调用_A.destroy()
  • 析构函数调用clear()

size(size_t):

void resize (size_type const & new_size)
{
  if (new_size == 0U) 
  { // we resize to zero, so we just remove all data
    clear();
  }
  else if (new_size != size())
  { // we don't go to zero and don't remain the same size
    size_type const old_size = size();
    pointer new_mem(nullptr);
    try
    {
      new_mem = _Allocate(new_size);
    }
    catch (std::bad_alloc e)
    {
      // [[[ 1 ]]]
    }
    size_type counter(0);
    for (size_type i=0; i<new_size; ++i)
    {
      try
      {
        if (i<size()) 
         _A.construct(new_mem + i, const_cast<const_reference>(*(_begin+i)));
         // [[[ 2 ]]]
        else 
          _A.construct(new_mem + i);
        ++counter;
      }
      catch (...) // [[[ 3 ]]]
      {
        // [[[ 4 ]]]
      }
    }
    clear();
    _begin = new_mem;
    _end = _begin + new_size;
  }
}

问题:

[[1]]]

我应该在这里调用clear()和re-throw还是调用当前对象的析构函数,如果我没有在这里捕获?

[[2]]]

在这里使用const_cast()或std::move()转换为右值引用如何?这会破坏异常安全吗?

如果我移动构造,比如说10个元素中的9个,而第10个元素在移动构造中抛出一些东西,我将失去10个对象中的9个!?

[[3]]]

我读到catch (...)应该避免。然而,我不知道是否有其他的可能性。是否有一种方法可以避免使用通用捕获而不知道构造函数是否会向我抛出或抛出什么?

[[4]]]

我认为正确的步骤是:

  • 通过调用[new_memory, new_memory+counter)范围内的析构函数回滚完成的构造
  • 释放new_mem
  • 调用clear ()
  • 抛出收到

正确吗?

您确实希望避免所有try/catch的东西,并使用RAII来确保适当的资源清理。例如:

void resize (size_type const & new_size)
{
    example<T> tmp(_A); // assuming I can construct with an allocator
    // If the allocation throws then the exception can propogate without
    // affecting the original contents of the container.
    tmp._end = tmp._begin = tmp._A.allocate(new_size);

    for (size_type i =  0; i < std::min(size(), new_size); ++i)
    {
        tmp._A.construct(tmp._begin + i, _begin[i]);
        ++tmp._end; // construction successful, increment _end so this
                    // object is destroyed if something throws later
    }
    for (size_type i = size(); i < new_size; ++i)
    {
        tmp._A.construct(tmp._begin + i);
        ++tmp._end; // as above
    }
    // OK, the copy of old objects and construction of new objects succeeded
    // now take ownership of the new memory and give our old contents to the
    // temporary container to be destroyed at the end of the function.
    std::swap(_begin, tmp._begin);
    std::swap(_end, tmp._end);
}

指出:

  • 你说"clear()[_begin,_end)_A.deallocate(_begin,size())中的所有元素调用_A.destroy()"。为了简单起见,我假设deallocate并不真正关心size()参数,这对某些分配器来说是正确的。如果这很重要,那么您可能希望example具有"容量"的概念和_capacity_end_of_storage成员。将大小容量分离将使清理更容易编写,更健壮。

  • 您已经在析构函数(和/或它调用的函数)中编写了正确的清理代码。通过使用临时容器,我可以重用这些代码,而不必复制它们。

  • 通过使用局部对象,我可以避免所有try/catch块,并依赖于局部对象的自动销毁来清理资源。

  1. 如果内存分配失败,您将永远不会构造任何新对象。(他们会去哪里?)然而,重新抛出通常是有意义的,因为在bad_alloc之后继续的唯一方法就是重试。

  2. 最安全的方法是只在移动构造函数为noexcept时移动构造函数,否则复制构造函数。如果你的编译器不支持::std::is_nothrow_move_constructible<T>,你可以要求你的类型的实现者只实现至少是事务安全的move构造函数,或者总是复制构造函数。

  3. 。代码可以向您抛出任何东西-从::std::exception, unsigned long long甚至void*派生的东西。这正是通用捕获的目的。

  4. 差不多。调用clear应该是不必要的,但就我所见,你正在执行一个正确的回滚,这样你的对象是在一个一致的状态。

其他笔记:

  • 你应该总是按值抛出,按引用捕获(你是按值捕获std::bad_alloc)。

  • 如果分配器类型是类的模板参数,请注意分配器提供的类型(例如size_type)可能与您期望的不太一样。(导致诸如比较它们是否具有不抛出保证之类的问题。)

  • 您正在依赖clear()是不抛出的。考虑这样一种情况:您正确地创建了新内存并构造了所有对象。您现在尝试clear您的旧数据-如果这抛出,您正在泄漏new_mem和所有对象。如果您移动它们的构造,这将给您留下内存和对象泄漏使您的数据结构处于不可用状态,即使clear()是事务安全的!