2

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

我不清楚的所有内容都标记为 [[[ 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() 转换为右值引用怎么样?这个打破异常安全吗?

如果我移动构造,假设 10 个元素中有 9 个,并且元素 10 在移动构造中抛出一些东西,我将失去 10 个对象中的 9 个!?

[[[3]]]

我读到catch (...)应该避免。不过,我不知道是否还有其他可能。有没有办法在不知道构造函数是否或向我抛出什么的情况下避免使用通用捕获?

[[[4]]]

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

  • 通过调用范围 [new_memory, new_memory+counter) 上的析构函数来回滚已完成的构造
  • 释放 new_mem
  • 调用清除()
  • 重新投掷

这个对吗?

4

2 回答 2

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

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

  3. 不,代码可以向你抛出任何东西——从::std::exception、 aunsigned long long甚至是void*. 这正是通用捕获的目的。

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

其他注意事项:

  • 您应该始终按值抛出并按引用捕获(您正在捕获std::bad_alloc按值)。

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

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

于 2013-05-16T06:45:29.057 回答
1

你真的想避免所有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()需要和”_A.destroy()中的所有元素。为简单起见,我假设它并不真正关心某些分配器的参数。如果这很重要,那么您可能想要一个“容量”的概念和一个或成员。将大小容量分开将使清理更容易编写和更健壮。[_begin,_end)_A.deallocate(_begin,size())deallocatesize()example_capacity_end_of_storage

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

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

于 2013-05-16T07:07:22.610 回答