我目前正在研究一个使用分配器来管理资源的容器类。我将尝试给出我目前正在调整容器大小的简短版本。 (真实的不是一维的,但由于分配的数据是连续的,因此方案是相同的。)
我不清楚的一切都标有[[[x]]]。
template<typename T>
class example
// ...
注意:
调整大小的来源(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;
}
}
我是否应该调用clear()并重新抛出此处,或者是否仍然是当前对象的析构函数,如果我没有抓到这里?
使用const_cast()或std :: move()在这里转换为rvalue-reference怎么样? 这会打破异常安全吗?
如果我移动构造,让我们说10个元素中的9个,并且元素10在移动构造上抛出一些东西,我会丢失10个对象中的9个!?
我读到应该避免使用catch (...)
。不过,我不知道是否还有其他可能性。
有没有办法避免使用通用捕获而不知道构造函数是否会向我扔什么?
我相信这里的正确步骤是:
这是对的吗?
答案 0 :(得分:1)
如果您的内存分配失败,您将永远不会构造任何新对象。 (他们会去哪里?)但是,重新抛出通常是有意义的,因为在bad_alloc
之后继续尝试的唯一方法就是重试。
这里最安全的方法是仅在移动构造函数为noexcept
时才移动构造,否则仅复制构造。如果您的编译器不支持::std::is_nothrow_move_constructible<T>
,您可以要求类型的实现者只实现至少是事务安全的移动构造函数,或者始终复制构造。
没有。代码可以向您抛出任何内容 - 来自::std::exception
,unsigned long long
甚至void*
的内容。这正是通用捕获的目的。
差不多。但是,调用clear应该是不必要的 - 据我所知,您正在执行正确的回滚,以使您的对象处于一致状态。
其他说明:
您应始终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
块,并依赖于自动销毁本地对象来清理资源。