如何安全地处理容器类中的thows / exceptions?

时间:2013-05-16 00:51:47

标签: c++ exception-handling try-catch containers throw

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

我不清楚的一切都标有[[[x]]]。

示例代码

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

注意:

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

调整大小的来源(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()并重新抛出此处,或者是否仍然是当前对象的析构函数,如果我没有抓到这里?

[[[2]]]

使用const_cast()或std :: move()在这里转换为rvalue-reference怎么样? 这会打破异常安全吗?

如果我移动构造,让我们说10个元素中的9个,并且元素10在移动构造上抛出一些东西,我会丢失10个对象中的9个!?

[[[3]]]

我读到应该避免使用catch (...)。不过,我不知道是否还有其他可能性。 有没有办法避免使用通用捕获而不知道构造函数是否会向我扔什么?

[[[4]]]

我相信这里的正确步骤是:

  • 通过调用范围[new_memory,new_memory + counter]上的析构函数来回滚已完成的构造
  • 取消分配new_mem
  • 致电明确()
  • 重新掷

这是对的吗?

2 个答案:

答案 0 :(得分:1)

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

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

  3. 没有。代码可以向您抛出任何内容 - 来自::std::exceptionunsigned long long甚至void*的内容。这正是通用捕获的目的。

  4. 差不多。但是,调用clear应该是不必要的 - 据我所知,您正在执行正确的回滚,以使您的对象处于一致状态。

  5. 其他说明:

    • 您应始终throw by value and catch by reference(按价值获取std::bad_alloc)。

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

    • 您依靠clear()来表达自己的想法。考虑一下您正确创建新内存并构建所有对象的情况。您现在尝试clear旧数据 - 如果抛出,则表示您正在泄漏new_mem和所有对象。如果移动构造它们,这将为您留下内存和对象泄漏使您的数据结构处于不可用状态,即使clear()是事务安全的!

答案 1 :(得分:1)

您确实希望避免使用所有try / catch内容并使用RAII来确保正确的资源清理。 E.g:

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()_A.destroy()[_begin,_end)中的所有元素调用_A.deallocate(_begin,size())”。为简单起见,我假设deallocate并不真正关心某些分配器的size()参数。如果这很重要,那么您可能希望example具有“容量”概念以及_capacity_end_of_storage成员。将 size 容量分开将使清理更容易编写并且更加健壮。

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

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