c++ - 如何处理未能释放包含在智能指针中的资源?

标签 c++ smart-pointers raii

资源释放过程中的错误应该如何处理,当
表示资源的对象是否包含在共享指针中?

编辑 1:

To put this question in more concrete terms: Many C-style interfaces have a function to allocate a resource, and one to release it. Examples are open(2) and close(2) for file descriptors on POSIX systems, XOpenDisplay and XCloseDisplay for a connection to an X server, or sqlite3_open and sqlite3_close for a connection to an SQLite database.

I like to encapsulate such interfaces in a C++ class, using the Pimpl idiom to hide the implementation details, and providing a factory method returning a shared pointer to ensure that the resource is deallocated when no references to it remain.

But, in all the examples given above and many others, the function used to release the resource may report an error. If this function is called by the destructor, I cannot throw an exception because generally destructors must not throw.

If, on the other hand, I provide a public method to release the resource, I now have a class with two possible states: One in which the resource is valid, and one in which the resource has already been released. Not only does this complicate the implementation of the class, it also opens a potential for wrong usage. This is bad, because an interface should aim to make usage errors impossible.

I would be grateful for any help with this problem.

The original statement of the question, and thoughts about a possible solution follow below.



编辑 2:

There is now a bounty on this question. A solution must meet these requirements:

  1. The resource is released if and only if no references to it remain.
  2. References to the resource may be destroyed explicitly. An exception is thrown if an error occured while releasing the resource.
  3. It is not possible to use a resource which has already been released.
  4. Reference counting and releasing of the resource are thread-safe.

A solution should meet these requirements:

  1. It uses the shared pointer provided by boost, the C++ Technical Report 1 (TR1), and the upcoming C++ standard, C++0x.
  2. It is generic. Resource classes only need to implement how the resource is released.

Thank you for your time and thoughts.



编辑 3:

Thanks to everybody who answered my question.

Alsk's answer met everything asked for in the bounty, and was accepted. In multithreaded code, this solution would require a separate cleanup thread.

I have added another answer where any exceptions during cleanup are thrown by the thread that actually used the resource, without need for a separate cleanup thread. If you are still interested in this problem (it bothered me a lot), please comment.



智能指针是安全管理资源的有用工具。例子
这些资源包括内存、磁盘文件、数据库连接或
网络连接。
// 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&);
};

但是这种方法存在一个问题。析构函数不能
抛出,因此无法释放资源的失败将保持未被检测到。

解决这个问题的一个常见方法是添加一个公共(public)方法来发布
资源。
class Socket
{
public:
    virtual void close(); // may throw
    // ...
};

不幸的是,这种方法引入了另一个问题:我们的对象
现在可能包含已经释放的资源。这
使资源类的实现复杂化。更糟糕的是,它
使该类的客户可能错误地使用它。这
下面的例子可能看起来牵强,但它是一个常见的陷阱
多线程代码。
socket->close();
// ...
size_t nread = socket->read(&buffer[0], buffer.size()); // wrong use!

要么我们确保在对象之前不释放资源
被破坏,从而失去任何处理失败资源的方法
解除分配。或者我们提供一种显式释放资源的方法
在对象的生命周期内,从而可以使用
资源类不正确。

有办法摆脱这种困境。但解决方案涉及使用
修改共享指针类。这些修改很可能是
有争议的。

典型的共享指针实现,例如 boost::shared_ptr,
要求当其对象的析构函数为
叫。通常,任何析构函数都不应该抛出,所以这是一个
合理的要求。这些实现还允许自定义
要指定的删除器函数,它被调用来代替
没有对对象的引用时的析构函数。不 throw
要求扩展到此自定义删除器功能。

这个要求的基本原理很清楚:共享指针的
析构函数不能抛出。如果删除函数不抛出,也不
将共享指针的析构函数。然而,同样适用于
共享指针的其他成员函数导致资源
释放,例如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;
     }
};

最佳答案

我们需要将分配的资源存储在某处(正如 DeadMG 已经提到的),并在任何析构函数之外显式调用一些报告/抛出函数。但这并不妨碍我们利用 boost::shared_ptr 中实现的引用计数。

/** A TCP/IP connection. */
class Socket
{
private:
    //store internally every allocated resource here
    static std::vector<boost::shared_ptr<Socket> > pool;
public:
    static boost::shared_ptr<Socket> connect(const std::string& address)
    {
         //...
         boost::shared_ptr<Socket> socket(new Socket(address));
         pool.push_back(socket); //the socket won't be actually 
                                 //destroyed until we want it to
         return socket;
    }
    virtual ~Socket();

    //call cleanupAndReport() as often as needed
    //probably, on a separate thread, or by timer 
    static void cleanupAndReport()
    {
        //find resources without clients
        foreach(boost::shared_ptr<Socket>& socket, pool)
        {
            if(socket.unique()) //there are no clients for this socket, i.e. 
                  //there are no shared_ptr's elsewhere pointing to this socket
            {
                 //try to deallocate this resource
                 if (close(socket->m_impl.fd) < 0)
                 {
                     int error = errno;
                     socket.reset(); //destroys Socket object
                     //throw an exception or handle error in-place
                     //... 
                     //throw Exception::fromErrno(error);
                 }
                 else
                 {
                     socket.reset();
                 } 
            } 
        } //foreach socket
    }
protected:
    Socket(const std::string& address);
private:
    // not implemented
    Socket(const Socket&);
    Socket& operator=(const Socket&);
};

cleanupAndReport() 的实现应该稍微复杂一点:在当前版本中,池在清理后填充了空指针,并且在抛出异常的情况下,我们必须调用该函数直到它不再抛出等等,但我希望,它很好地说明了这个想法。

现在,更通用的解决方案:
//forward declarations
template<class Resource>
boost::shared_ptr<Resource> make_shared_resource();
template<class Resource>
void cleanupAndReport(boost::function1<void,boost::shared_ptr<Resource> deallocator);

//for every type of used resource there will be a template instance with a static pool
template<class Resource>
class pool_holder
{
private:
        friend boost::shared_ptr<Resource> make_shared_resource<Resource>();
        friend void cleanupAndReport(boost::function1<void,boost::shared_ptr<Resource>);
        static std::vector<boost::shared_ptr<Resource> > pool;
};
template<class Resource>
std::vector<boost::shared_ptr<Resource> > pool_holder<Resource>::pool;

template<class Resource>
boost::shared_ptr<Resource> make_shared_resource()
{
        boost::shared_ptr<Resource> res(new Resource);
        pool_holder<Resource>::pool.push_back(res);
        return res;
}
template<class Resource>
void cleanupAndReport(boost::function1<void,boost::shared_ptr<Resource> > deallocator)
{
    foreach(boost::shared_ptr<Resource>& res, pool_holder<Resource>::pool)
    {
        if(res.unique()) 
        {
             deallocator(res);
        }
    } //foreach
}
//usage
        {
           boost::shared_ptr<A> a = make_shared_resource<A>();
           boost::shared_ptr<A> a2 = make_shared_resource<A>();
           boost::shared_ptr<B> b = make_shared_resource<B>();
           //...
        }
        cleanupAndReport<A>(deallocate_A);
        cleanupAndReport<B>(deallocate_B);

关于c++ - 如何处理未能释放包含在智能指针中的资源?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/2845183/

相关文章:

c++ - 创建RAII类时应如何处理错误?

c++ - 使用 std::initializer_list 的显式构造函数和初始化

c++ - deep_const_ptr 复制构造函数

c++ - 当我获得指向智能指针的指针时,是否需要释放内存?

c++ - Bison 中的 std::shared_ptr 导致成员错误

c# - 如何在 C# 程序中以 RAII 样式管理 COM 对象运行时?

c++ - 使用多线程时性能几乎没有提高

c++ - 在 C++ 中,如何从不同的库中调用具有相同签名的 dll 函数?

c++ - 用于编译原始二进制文件的 Windows 的 C 编译器?

c++ - 难以为文件处理类重载运算符<<