如何处理无法释放智能指针中包含的资源?

cj.*_*cj. 9 c++ smart-pointers raii

当表示资源的对象包含在共享指针中时,如何处理资源释放期间的错误?

编辑1:

用更具体的术语来说明这个问题:许多C风格的接口都有一个分配资源的功能,一个用来释放它.POSIX系统上的文件描述符是open(2)和close(2),XOpenDisplay和XCloseDisplay是连接到X服务器的例子,sqlite3_open和sqlite3_close是连接到SQLite数据库的例子.

我喜欢将这些接口封装在C++类中,使用Pimpl习惯用法隐藏实现细节,并提供一个返回共享指针的工厂方法,以确保在没有对它的引用时释放资源.

但是,在上面给出的所有示例和许多其他示例中,用于释放资源的函数可能会报告错误.如果析构函数调用此函数,我不能抛出异常,因为通常析构函数不能抛出.

另一方面,如果我提供一个公共方法来释放资源,我现在有一个具有两种可能状态的类:一个资源有效,另一个资源已经释放.这不仅会使类的实现变得复杂,而且还会导致错误使用.这很糟糕,因为界面的目标应该是无法使用错误.

如果对这个问题有任何帮助,我将不胜感激.

问题的原始陈述以及对可能解决方案的想法如下.

编辑2:

现在有一个关于这个问题的赏金.解决方案必须满足以下要求:

  1. 当且仅当没有对它的引用时才释放资源.
  2. 可以明确地销毁对资源的引用.如果在释放资源时发生错误,则抛出异常.
  3. 无法使用已发布的资源.
  4. 引用计数和释放资源是线程安全的.

解决方案满足以下要求:

  1. 它使用boost提供的共享指针,C++技术报告1(TR1)和即将推出的C++标准C++ 0x.
  2. 它是通用的.资源类只需要实现资源的释放方式.

感谢您的时间和想法.

编辑3:

感谢所有回答我问题的人.

阿尔斯克的回答满足了赏金中所要求的一切,并被接受了.在多线程代码中,此解决方案需要单独的清理线程.

我添加了另一个答案,其中清理期间的任何异常都被实际使用资源的线程抛出,而不需要单独的清理线程.如果你仍然对这个问题感兴趣(这让我很烦恼),请发表评论.

智能指针是安全管理资源的有用工具.此类资源的示例是内存,磁盘文件,数据库连接或网络连接.

// open a connection to the local HTTP port
boost::shared_ptr<Socket> socket = Socket::connect("localhost:80");
Run Code Online (Sandbox Code Playgroud)

在典型的场景中,封装资源的类应该是不可复制的和多态的.支持这一点的一个好方法是提供一个返回共享指针的工厂方法,并声明所有构造函数都是非公共的.现在可以自由地复制和分配共享指针.当没有对它的引用仍然存在时,该对象将被自动销毁,然后析构函数将释放该资源.

/** 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&);
};
Run Code Online (Sandbox Code Playgroud)

但是这种方法存在问题.析构函数不能抛出,因此无法释放资源将无法检测到.

解决此问题的常见方法是添加公共方法以释放资源.

class Socket
{
public:
    virtual void close(); // may throw
    // ...
};
Run Code Online (Sandbox Code Playgroud)

不幸的是,这种方法引入了另一个问题:我们的对象现在可能包含已经发布的资源.这使资源类的实现变得复杂.更糟糕的是,这使得该类客户端可能无法正确使用它.以下示例可能看起来很牵强,但它是多线程代码中常见的陷阱.

socket->close();
// ...
size_t nread = socket->read(&buffer[0], buffer.size()); // wrong use!
Run Code Online (Sandbox Code Playgroud)

要么我们确保在对象被销毁之前不释放资源,从而失去处理失败的资源释放的任何方法.或者我们提供了一种在对象生命周期内显式释放资源的方法,从而可以错误地使用资源类.

有一种摆脱这种困境的方法.但解决方案涉及使用修改后的共享指针类.这些修改可能会引起争议.

典型的共享指针实现(例如boost :: shared_ptr)要求在调用其对象的析构函数时不会抛出异常.一般来说,没有析构函数应该抛出,所以这是一个合理的要求.这些实现还允许指定自定义删除函数,当没有对对象的引用时,调用该函数来代替析构函数.无投掷要求扩展到此自定义删除功能.

这个要求的基本原理很明确:共享指针的析构函数不能抛出.如果删除函数没有抛出,也不会抛出共享指针的析构函数.但是,对于导致资源释放的共享指针的其他成员函数也是如此,例如reset():如果资源释放失败,则不会抛出异常.

这里提出的解决方案是允许自定义删除函数抛出.这意味着修改后的共享指针的析构函数必须捕获由删除函数抛出的异常.另一方面,除了析构函数之外的成员函数,例如reset(),不应该捕获删除函数的异常(并且它们的实现变得稍微复杂一些).

这是原始示例,使用throw throwter函数:

/** 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());
}
Run Code Online (Sandbox Code Playgroud)

我们现在可以使用reset()来显式释放资源.如果在另一个线程或程序的另一部分中仍然存在对资源的引用,则调用reset()将仅减少引用计数.如果这是对资源的最后一个引用,则释放资源.如果资源释放失败,则抛出异常.

SharedPtr<Socket> socket = Socket::connect("localhost:80");
// ...
socket.reset();
Run Code Online (Sandbox Code Playgroud)

编辑:

这是删除器的完整(但依赖于平台)实现:

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;
     }
};
Run Code Online (Sandbox Code Playgroud)

Als*_*lsk 4

我们需要将分配的资源存储在某个地方(正如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&);
};
Run Code Online (Sandbox Code Playgroud)

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);
Run Code Online (Sandbox Code Playgroud)