当表示资源的对象包含在共享指针中时,应如何处理资源释放期间的错误?
编辑1:
用更具体的术语来说明这个问题:许多 C 风格的接口具有分配资源和释放资源的功能。示例是用于 POSIX 系统上的文件描述符的 open(2) 和 close(2),用于连接到 X 服务器的 XOpenDisplay 和 XCloseDisplay,或用于连接到 SQLite 数据库的 sqlite3_open 和 sqlite3_close。
我喜欢将此类接口封装在 C++ 类中,使用 Pimpl 习惯用法隐藏实现细节,并提供返回共享指针的工厂方法,以确保在没有对资源的引用时释放资源。
但是,在上面给出的所有示例和许多其他示例中,用于释放资源的函数可能会报告错误。如果这个函数被析构函数调用,我不能抛出异常,因为通常析构函数不能抛出。
另一方面,如果我提供了一个公共方法来释放资源,那么我现在有一个具有两种可能状态的类:一种是资源有效,另一种是资源已被释放。这不仅使类的实现复杂化,而且还存在错误使用的可能性。这很糟糕,因为接口应该旨在使使用错误成为不可能。
对于这个问题的任何帮助,我将不胜感激。
问题的原始陈述以及关于可能解决方案的想法如下。
编辑2:
现在有一个赏金这个问题。解决方案必须满足以下要求:
- 当且仅当没有对它的引用时才释放该资源。
- 对资源的引用可能会被显式销毁。如果在释放资源时发生错误,则会引发异常。
- 不能使用已经释放的资源。
- 资源的引用计数和释放是线程安全的。
解决方案应满足以下要求:
- 它使用boost提供的共享指针、C++ Technical Report 1 (TR1)和即将推出的 C++ 标准C++0x。
- 它是通用的。资源类只需要实现资源的释放方式即可。
感谢您的时间和想法。
编辑 3:
感谢所有回答我问题的人。
Alsk 的回答满足了赏金中的所有要求,并被接受。在多线程代码中,此解决方案将需要一个单独的清理线程。
我添加了另一个答案,其中清理期间的任何异常都由实际使用资源的线程引发,而无需单独的清理线程。如果您仍然对这个问题感兴趣(它困扰了我很多),请发表评论。
智能指针是安全管理资源的有用工具。此类资源的示例是内存、磁盘文件、数据库连接或网络连接。
// open a connection to the local HTTP port
boost::shared_ptr<Socket> socket = Socket::connect("localhost:80");
在典型的场景中,封装资源的类应该是不可复制的和多态的。支持这一点的一个好方法是提供一个返回共享指针的工厂方法,并将所有构造函数声明为非公共的。共享指针现在可以自由复制和分配。当没有对它的引用时,该对象将自动销毁,然后析构函数释放资源。
/** A TCP/IP connection. */
class Socket
{
public:
static boost::shared_ptr<Socket> connect(const std::string& address);
virtual ~Socket();
protected:
Socket(const std::string& address);
private:
// not implemented
Socket(const Socket&);
Socket& operator=(const Socket&);
};
但是这种方法存在一个问题。析构函数不能抛出,因此释放资源的失败将不会被检测到。
解决这个问题的一个常见方法是添加一个公共方法来释放资源。
class Socket
{
public:
virtual void close(); // may throw
// ...
};
不幸的是,这种方法引入了另一个问题:我们的对象现在可能包含已经释放的资源。这使资源类的实现复杂化。更糟糕的是,它使该类的客户可能错误地使用它。以下示例可能看起来有些牵强,但它是多线程代码中的常见缺陷。
socket->close();
// ...
size_t nread = socket->read(&buffer[0], buffer.size()); // wrong use!
要么我们确保在对象被销毁之前不释放资源,从而失去任何处理失败资源释放的方法。或者我们提供了一种在对象生命周期内显式释放资源的方法,从而可以错误地使用资源类。
有办法摆脱这种困境。但解决方案涉及使用修改后的共享指针类。这些修改可能会引起争议。
典型的共享指针实现,例如 boost::shared_ptr,要求在调用其对象的析构函数时不抛出异常。通常,任何析构函数都不应该抛出,所以这是一个合理的要求。这些实现还允许指定自定义删除函数,当没有对对象的引用时调用该函数以代替析构函数。不抛出要求扩展到此自定义删除器功能。
这个要求的基本原理很清楚:共享指针的析构函数不能抛出。如果删除函数不抛出,共享指针的析构函数也不会抛出。然而,对于导致资源释放的共享指针的其他成员函数也是如此,例如reset():如果资源释放失败,则不会抛出异常。
这里提出的解决方案是允许自定义删除函数抛出。这意味着修改后的共享指针的析构函数必须捕获删除函数抛出的异常。另一方面,除析构函数之外的成员函数,例如reset(),不应捕获删除函数的异常(并且它们的实现变得更加复杂)。
这是原始示例,使用抛出删除器函数:
/** A TCP/IP connection. */
class Socket
{
public:
static SharedPtr<Socket> connect(const std::string& address);
protected:
Socket(const std::string& address);
virtual Socket() { }
private:
struct Deleter;
// not implemented
Socket(const Socket&);
Socket& operator=(const Socket&);
};
struct Socket::Deleter
{
void operator()(Socket* socket)
{
// Close the connection. If an error occurs, delete the socket
// and throw an exception.
delete socket;
}
};
SharedPtr<Socket> Socket::connect(const std::string& address)
{
return SharedPtr<Socket>(new Socket(address), Deleter());
}
我们现在可以使用 reset() 显式释放资源。如果在另一个线程或程序的另一部分中仍然存在对该资源的引用,则调用 reset() 只会减少引用计数。如果这是对该资源的最后一次引用,则释放该资源。如果资源释放失败,则会引发异常。
SharedPtr<Socket> socket = Socket::connect("localhost:80");
// ...
socket.reset();
编辑:
这是删除器的完整(但依赖于平台)实现:
struct Socket::Deleter
{
void operator()(Socket* socket)
{
if (close(socket->m_impl.fd) < 0)
{
int error = errno;
delete socket;
throw Exception::fromErrno(error);
}
delete socket;
}
};