Как справиться с невозможностью освободить ресурс, который содержится в умном указателе? - PullRequest
8 голосов
/ 16 мая 2010

Как следует обрабатывать ошибку во время освобождения ресурса, когда объект, представляющий ресурс, содержится в общем указателе?

РЕДАКТИРОВАТЬ 1:

Чтобы сформулировать этот вопрос более конкретно: многие интерфейсы в стиле C есть функция для выделения ресурса и одна для освобождения Это. Примеры open (2) и close (2) для файловых дескрипторов в POSIX системы, 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");

В типичном сценарии класс, инкапсулирующий ресурс, должен быть не копируемый и полиморфный. Хороший способ поддержать это - предоставить фабричный метод, возвращающий общий указатель и объявляющий все конструкторы непубличные. Общие указатели теперь можно скопировать из и назначен свободно. Объект автоматически уничтожается, когда нет ссылка на него остается, и деструктор затем освобождает ресурс.

/** 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&);
};

Но есть проблема с этим подходом. Деструктор не долженthrow, поэтому сбой при освобождении ресурса останется незамеченным.

Распространенным выходом из этой проблемы является добавление публичного метода в релиз ресурс.

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 (): в случае сбоя освобождения ресурса нет может быть выдано исключение.

Предлагаемое здесь решение заключается в том, чтобы пользовательские функции удаления могли бросить. Это означает, что деструктор модифицированного общего указателя должен ловить исключения, выданные функцией удаления. С другой стороны, функции-члены, отличные от деструктора, например, сброс (), не должен перехват исключений функции удаления (и их реализация становится несколько сложнее).

Вот оригинальный пример использования функции удаления метания:

/** 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();

EDIT:

Вот полная (но зависящая от платформы) реализация средства удаления:

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;
     }
};

Ответы [ 6 ]

4 голосов
/ 25 мая 2010

Нам нужно где-то хранить выделенные ресурсы (как это уже упоминалось 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);
4 голосов
/ 17 мая 2010

Если освобождение какого-либо ресурса может действительно завершиться с ошибкой , то деструктор явно является неправильной абстракцией для использования. Деструкторы предназначены для очистки в обязательном порядке, независимо от обстоятельств. close() метод (или как вы хотите его назвать), вероятно, единственный путь.

Но подумайте об этом. Если освободить ресурс на самом деле не удастся , что вы можете сделать? Такая ошибка исправима? Если да, то какая часть вашего кода должна его обрабатывать? Способ восстановления, вероятно, сильно зависит от приложения и привязан к другим частям приложения. Крайне маловероятно, что вы на самом деле хотите, чтобы это произошло автоматически в произвольном месте в коде, который выпустил ресурс и вызвал ошибку. Общая абстракция указателя на самом деле не моделирует то, чего вы пытаетесь достичь. Если это так, то вам явно необходимо создать собственную абстракцию, которая моделирует требуемое поведение. Злоупотребление общими указателями для выполнения действий, которые они не должны делать, - неправильный путь.

Также, пожалуйста, прочитайте это .

EDIT:
Если все, что вам нужно сделать, это сообщить пользователю о том, что произошло до сбоя, подумайте о том, чтобы обернуть Socket в еще один объект-обертку , который вызовет средство удаления при его уничтожении, перехватить все возникшие исключения и обработать их с помощью показывая пользователю окно сообщения или что-то еще. Затем поместите этот объект-обертку в boost::shared_ptr.

1 голос
/ 17 июня 2010

Как объявлено в вопросе, отредактируйте 3:

Вот еще одно решение, которое, насколько я могу судить, выполняет требования в вопросе. Это похоже на решение, описанное в первоначальном вопросе, но использует boost::shared_ptr вместо настраиваемый умный указатель.

Основная идея этого решения - предоставить release() операция на shared_ptr. Если мы можем заставить shared_ptr отказаться от его право собственности, мы можем вызвать функцию очистки, удалить объект, и выдать исключение, если во время очистки произошла ошибка.

Boost имеет хорошо причина не предоставлять release() операцию на shared_ptr:

shared_ptr не может передать право собственности, если он не уникален (), потому что другая копия все равно уничтожит объект.

Рассмотрим:

shared_ptr<int> a(new int);
shared_ptr<int> b(a); // a.use_count() == b.use_count() == 2

int * p = a.release();

// Who owns p now? b will still call delete on it in its destructor.

Кроме того, указатель, возвращаемый функцией release (), будет трудно надежно освободить, поскольку источник shared_ptr мог быть создан с пользовательским удалителем.

Первый аргумент против операции release() заключается в том, что характер shared_ptr, многие указатели разделяют право собственности на объект, так что ни один из них не может просто освободить это право собственности. Но что если функция release() вернула нулевой указатель, если еще другие ссылки остались? shared_ptr может надежно определить это без условий гонки.

Второй аргумент против операции release() заключается в том, что если Пользовательский удалитель был передан в shared_ptr, вы должны использовать это для освободить объект, а не просто удалить его. Но release() может возвращать объект функции, в дополнение к необработанному указателю, разрешить вызывающей стороне надежно освободить указатель.

Однако в нашем конкретном сценарии пользовательские средства удаления не будут проблема, потому что мы не имеем дело с произвольным обычаем удалившие. Это станет понятнее из приведенного ниже кода.

Предоставление операции release() на shared_ptr без изменения его реализация, конечно, невозможна без взлома. взлом, который используется в приведенном ниже коде, полагается на локальную переменную потока чтобы наш пользовательский инструмент удаления фактически не удалил объект.

Тем не менее, вот код, состоящий в основном из заголовка Resource.hpp, плюс небольшой файл реализации Resource.cpp. Заметка что это должно быть связано с -lboost_thread-mt из-за локальная переменная потока.

// ---------------------------------------------------------------------
// Resource.hpp
// ---------------------------------------------------------------------

#include <boost/assert.hpp>
#include <boost/ref.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/thread/tss.hpp>


/// Factory for a resource.
template<typename T>
struct ResourceFactory
{
    /// Create a resource.
    static boost::shared_ptr<T>
    create()
    {
        return boost::shared_ptr<T>(new T, ResourceFactory());
    }

    template<typename A1>
    static boost::shared_ptr<T>
    create(const A1& a1)
    {
        return boost::shared_ptr<T>(new T(a1), ResourceFactory());
    }

    template<typename A1, typename A2>
    static boost::shared_ptr<T>
    create(const A1& a1, const A2& a2)
    {
        return boost::shared_ptr<T>(new T(a1, a2), ResourceFactory());
    }

    // ...

    /// Destroy a resource.
    static void destroy(boost::shared_ptr<T>& resource);

    /// Deleter for boost::shared_ptr<T>.
    void operator()(T* resource);
};


namespace impl
{

// ---------------------------------------------------------------------

/// Return the last reference to the resource, or zero. Resets the pointer.
template<typename T>
T* release(boost::shared_ptr<T>& resource);

/// Return true if the resource should be deleted (thread-local).
bool wantDelete();

// ---------------------------------------------------------------------

} // namespace impl


template<typename T>
inline
void ResourceFactory<T>::destroy(boost::shared_ptr<T>& ptr)
{
    T* resource = impl::release(ptr);

    if (resource != 0) // Is it the last reference?
    {
        try
        {
            resource->close();
        }
        catch (...)
        {
            delete resource;

            throw;
        }

        delete resource;
    }
}

// ---------------------------------------------------------------------

template<typename T>
inline
void ResourceFactory<T>::operator()(T* resource)
{
    if (impl::wantDelete())
    {
        try
        {
            resource->close();
        }
        catch (...)
        {
        }

        delete resource;
    }
}


namespace impl
{

// ---------------------------------------------------------------------

/// Flag in thread-local storage.
class Flag
{
public:
    ~Flag()
    {
        m_ptr.release();
    }

    Flag& operator=(bool value)
    {
        if (value != static_cast<bool>(*this))
        {
            if (value)
            {
                m_ptr.reset(s_true); // may throw boost::thread_resource_error!
            }
            else
            {
                m_ptr.release();
            }
        }

        return *this;
    }

    operator bool()
    {
        return m_ptr.get() == s_true;
    }

private:
    boost::thread_specific_ptr<char> m_ptr;

    static char* s_true;
};

// ---------------------------------------------------------------------

/// Flag to prevent deletion.
extern Flag t_nodelete;

// ---------------------------------------------------------------------

/// Return the last reference to the resource, or zero.
template<typename T>
T* release(boost::shared_ptr<T>& resource)
{
    try
    {
        BOOST_ASSERT(!t_nodelete);

        t_nodelete = true; // may throw boost::thread_resource_error!
    }
    catch (...)
    {
        t_nodelete = false;

        resource.reset();

        throw;
    }

    T* rv = resource.get();

    resource.reset();

    return wantDelete() ? rv : 0;
}

// ---------------------------------------------------------------------

} // namespace impl

И файл реализации:

// ---------------------------------------------------------------------
// Resource.cpp
// ---------------------------------------------------------------------

#include "Resource.hpp"


namespace impl
{

// ---------------------------------------------------------------------

bool wantDelete()
{
    bool rv = !t_nodelete;

    t_nodelete = false;

    return rv;
}

// ---------------------------------------------------------------------

Flag t_nodelete;

// ---------------------------------------------------------------------

char* Flag::s_true((char*)0x1);

// ---------------------------------------------------------------------

} // namespace impl

А вот пример класса ресурсов, реализованного с использованием этого решения:

// ---------------------------------------------------------------------
// example.cpp
// ---------------------------------------------------------------------
#include "Resource.hpp"

#include <cstdlib>
#include <string>
#include <stdexcept>
#include <iostream>


// uncomment to test failed resource allocation, usage, and deallocation

//#define TEST_CREAT_FAILURE
//#define TEST_USAGE_FAILURE
//#define TEST_CLOSE_FAILURE

// ---------------------------------------------------------------------

/// The low-level resource type.
struct foo { char c; };

// ---------------------------------------------------------------------

/// The low-level function to allocate the resource.
foo* foo_open()
{
#ifdef TEST_CREAT_FAILURE
    return 0;
#else
    return (foo*) std::malloc(sizeof(foo));
#endif
}

// ---------------------------------------------------------------------

/// Some low-level function using the resource.
int foo_use(foo*)
{
#ifdef TEST_USAGE_FAILURE
    return -1;
#else
    return 0;
#endif
}

// ---------------------------------------------------------------------

/// The low-level function to free the resource.
int foo_close(foo* foo)
{
    std::free(foo);
#ifdef TEST_CLOSE_FAILURE
    return -1;
#else
    return 0;
#endif
}

// ---------------------------------------------------------------------

/// The C++ wrapper around the low-level resource.
class Foo
{
public:
    void use()
    {
        if (foo_use(m_foo) < 0)
        {
            throw std::runtime_error("foo_use");
        }
    }

protected:
    Foo()
        : m_foo(foo_open())
    {
        if (m_foo == 0)
        {
            throw std::runtime_error("foo_open");
        }
    }

    void close()
    {
        if (foo_close(m_foo) < 0)
        {
            throw std::runtime_error("foo_close");
        }
    }

private:
    foo* m_foo;

    friend struct ResourceFactory<Foo>;
};

// ---------------------------------------------------------------------

typedef ResourceFactory<Foo> FooFactory;

// ---------------------------------------------------------------------

/// Main function.
int main()
{
    try
    {
        boost::shared_ptr<Foo> resource = FooFactory::create();

        resource->use();

        FooFactory::destroy(resource);
    }
    catch (const std::exception& e)
    {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}

Наконец, вот небольшой Makefile для сборки всего этого:

# Makefile

CXXFLAGS = -g -Wall

example: example.cpp Resource.hpp Resource.o
    $(CXX) $(CXXFLAGS) -o example example.cpp Resource.o -lboost_thread-mt

Resource.o: Resource.cpp Resource.hpp
    $(CXX) $(CXXFLAGS) -c Resource.cpp -o Resource.o

clean:
    rm -f Resource.o example
1 голос
/ 24 мая 2010

Цитата Херба Саттера, автора "Исключительного C ++" (из здесь ):

Если деструктор выдает исключение, Плохие вещи могут случиться. В частности, рассмотрим код, подобный следующему:

//  The problem
//
class X {
public:
  ~X() { throw 1; }
};

void f() {
  X x;
  throw 2;
} // calls X::~X (which throws), then calls terminate()

Если деструктор выдает исключение в то время как другое исключение уже активный (т.е. во время разматывания стека), программа прекращена. Это как правило, не очень хорошая вещь.

Другими словами, независимо от того, что вы хотите верить в элегантность в этой ситуации, вы не можете бездумно бросить исключение в деструктор, если не можете гарантировать, что оно не будет выброшено при обработке другого исключения.

Кроме того, что вы можете сделать, если не можете успешно избавиться от ресурса? Исключения должны быть брошены для вещей, которые могут быть обработаны выше, а не ошибок. Если вы хотите сообщить о странном поведении, зарегистрируйте ошибку выпуска и просто продолжайте. Или прекратить.

0 голосов
/ 22 мая 2010

Вообще говоря, если закрытие C-стиля ресурса завершается неудачей, то это проблема с API, а не проблема в вашем коде. Тем не менее, я бы хотел соблазн сделать следующее: если уничтожение не удалось, добавьте его в список ресурсов, которые необходимо повторить позже, например, при выходе из приложения, периодически или когда другие подобные ресурсы уничтожаются затем попытайтесь повторно уничтожить. Если они остались в произвольное время, выдайте ошибку пользователя и выйдите.

0 голосов
/ 17 мая 2010

Ну, во-первых, я не вижу здесь вопроса. Во-вторых, я должен сказать, что это плохая идея. Что вы получите во всем этом? Когда последний общий указатель на ресурс будет уничтожен, а ваш вызывающий удалитель вызван, вы обнаружите утечку ресурса. Вы потеряете все дескрипторы ресурса, который не удалось освободить. Вы никогда не сможете повторить попытку.

Ваше желание использовать объект RAII хорошее, но умного указателя просто недостаточно для этой задачи. То, что вам нужно, должно быть еще умнее. Вам нужно что-то, что может восстановить себя в случае полного краха. Деструктора недостаточно для такого интерфейса.

Вы знакомитесь с неправомерным использованием, когда кто-то может заставить ресурс иметь дескриптор, но быть недействительным. Тип ресурса, с которым вы здесь работаете, просто поддается решению этой проблемы. Есть много способов, которыми вы можете подойти к этому. Одним из методов может быть использование идиомы handle / body вместе с шаблоном состояния. Реализация интерфейса может находиться в одном из двух состояний: подключен или не подключен. Дескриптор просто передает запросы внутреннему телу / состоянию. Подключенный работает как обычный, неподключенный создает исключения / утверждения во всех применимых запросах.

Эта штука нуждается в функции, отличной от ~, чтобы уничтожить дескриптор к ней. Вы можете рассмотреть функцию destroy (), которая может генерировать. Если вы улавливаете ошибку при вызове, вы не удаляете дескриптор, а вместо этого решаете проблему любым конкретным приложением, каким вам нужно. Если вы не уловили ошибку от destroy (), вы позволите дескриптору выйти из области видимости, сбросить его или что-то еще. Затем функция destroy () дешифрует счетчик ресурсов и пытается освободить внутренний ресурс, если этот счетчик равен 0. При успешном завершении дескриптор переключается в неподключенное состояние, при сбое генерирует перехватываемую ошибку, которую клиент может попытаться обработать, но оставляет ручка в подключенном состоянии.

Писать не совсем тривиально, но то, что вы хотите сделать, ввести исключения в уничтожение, просто не сработает.

...