如何安全地处理容器类中的方法/异常?
How do I handle thows/exceptions in a container class safely?
我目前正在研究一个使用分配器来管理资源的容器类。我将尝试给出一个简短的版本,我目前做什么来调整容器的大小。(实际的不是一维的,但方案是相同的,因为分配的数据是连续的。)
所有我不清楚的都标记为[[[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
块,并依赖于局部对象的自动销毁来清理资源。
-
如果内存分配失败,您将永远不会构造任何新对象。(他们会去哪里?)然而,重新抛出通常是有意义的,因为在
bad_alloc
之后继续的唯一方法就是重试。 -
最安全的方法是只在移动构造函数为
noexcept
时移动构造函数,否则复制构造函数。如果你的编译器不支持::std::is_nothrow_move_constructible<T>
,你可以要求你的类型的实现者只实现至少是事务安全的move构造函数,或者总是复制构造函数。 -
。代码可以向您抛出任何东西-从
::std::exception
,unsigned long long
甚至void*
派生的东西。这正是通用捕获的目的。 -
差不多。调用clear应该是不必要的,但就我所见,你正在执行一个正确的回滚,这样你的对象是在一个一致的状态。
其他笔记:
-
你应该总是按值抛出,按引用捕获(你是按值捕获
std::bad_alloc
)。 -
如果分配器类型是类的模板参数,请注意分配器提供的类型(例如
size_type
)可能与您期望的不太一样。(导致诸如比较它们是否具有不抛出保证之类的问题。) -
您正在依赖
clear()
是不抛出的。考虑这样一种情况:您正确地创建了新内存并构造了所有对象。您现在尝试clear
您的旧数据-如果这抛出,您正在泄漏new_mem
和所有对象。如果您移动它们的构造,这将给您留下内存和对象泄漏和使您的数据结构处于不可用状态,即使clear()
是事务安全的!
- 处理多个异常集合的C++方法
- 引发异常的方法的命名约定 (C++)?
- 有没有更好的方法来处理异常? try-catch块真的很丑
- C++ 捕获异常后进行清理的标准方法是什么?
- 方法c++内部出现异常
- 有没有一种方法可以让OpenCLC++绑定为所有错误抛出异常
- 关于异常,覆盖标准异常方法
- 处理许多自定义异常的最佳方法是什么
- 抽象包装带有异常的 C 错误处理的最佳方法
- 提出异常并处理C 的某些异常类型的正确方法是什么?
- 在类中抛出异常的最佳方法是什么
- 构造函数中的异常:init() 方法、指针、大型 try/catch 或
- 引发异常后无法退出方法
- 如何在C++中显式调用异常抛出方法
- 推荐一种在未输入获取行分隔符时引发异常的方法?
- 无法捕获模板规范化方法引发的异常
- 创建自己的异常(2种方法)C
- C 向量异常处理:哪一种是抛出out_of_range()的更好方法以及原因
- 如何安全地处理容器类中的方法/异常?
- 我在哪里可以找到有关c++ /STL方法异常保证的信息