C++ RAII для управления изменением и возвратом состояния объекта

У меня есть класс foo. Операции над foo требуют вызова foo::open(), ряда foo::write() и должны заканчиваться вызовом foo::close():

#include <iostream>

class foo
{
public:
    foo()
    {
        std::cout << "foo::foo()" << std::endl;
    }
    ~foo()
    {
        std::cout << "foo::~foo()" << std::endl;
    }    
    void open()
    {
        std::cout << "foo::open()" << std::endl;
    }
    void close()
    {
        std::cout << "foo::close()" << std::endl;
    }
    void write(const std::string& s)
    {
        std::cout << "foo::write(" << s << ")" << std::endl;
    }    
private:    
    // state that must be retained for entire lifetime of object
};

static void useFoo(foo& my_foo)
{
    my_foo.open();
    my_foo.write("string1");
    my_foo.write("string2");
    my_foo.close();
}

int main(  int argc, char* argv[] )
{
    foo my_foo;
    useFoo(my_foo);
    useFoo(my_foo);
}

Как и ожидалось, это выводит следующее:

foo::foo()
foo::open()
foo::write(string1)
foo::write(string2)
foo::close()
foo::open()
foo::write(string1)
foo::write(string2)
foo::close()
foo::~foo()

Я хочу дать пользователям моего класса foo способ гарантировать, что они не забудут вызвать foo::close(), и гарантировать, что foo::close() будет вызываться в случае возникновения исключения. Я не могу использовать деструктор foo, так как foo должен продолжать существовать после foo::close(), готовый к следующему foo::open().

Я придумал эту реализацию RAII:

#include <iostream>

class foo
{
public:
    class opener
    {
    public:
        explicit opener(foo& my_foo):foo_(my_foo)
        {
            foo_.open();
        };
        ~opener()
        {
            foo_.close();
        };    
    private:
        foo& foo_;
    };    
    foo()
    {
        std::cout << "foo::foo()" << std::endl;
    }
    ~foo()
    {
        std::cout << "foo::~foo()" << std::endl;
    }    
    void open()
    {
        std::cout << "foo::open()" << std::endl;
    }
    void close()
    {
        std::cout << "foo::close()" << std::endl;
    }
    void write(const std::string& s)
    {
        std::cout << "foo::write(" << s << ")" << std::endl;
    } 
    opener get_opener()
    {
        return(opener(*this));
    }   
private:
    // state that must be retained for entire lifetime of object    
};


static void useFoo(foo& my_foo)
{
    foo::opener my_foo_opener = my_foo.get_opener();
    my_foo.write("string1");
    my_foo.write("string2");
}

int main(  int argc, char* argv[] )
{
    foo my_foo;
    useFoo(my_foo);
    useFoo(my_foo);
}

Для простоты я не включил очевидное улучшение, заключающееся в том, что класс foo::opener предоставляет метод foo::write(), хотя в реальном объекте я бы сделал это, чтобы предотвратить возможность write() до open( ).

EDIT Как указывает Наваз ниже, реальному классу также потребуется конструктор копирования и оператор присваивания.

Это кажется довольно шаблонным только для того, чтобы гарантировать, что вызов close() будет вызван. Возникают два вопроса:

  1. Это все же проще, чем заставлять пользователей моего класса использовать try/catch?

  2. Есть ли более простой способ добиться того, что я хочу: предоставить базовую гарантию исключения и убедиться, что close() всегда следует за open()?


person Simon Elliott    schedule 05.10.2011    source источник


Ответы (5)


Вложенный класс opener должен реализовывать семантику копирования, поскольку код по умолчанию, сгенерированный компилятором, даст нежелательный результат, если я правильно понял ваше намерение.

Поэтому, пожалуйста, реализуйте конструктор копирования и назначение копирования.

Или, в качестве альтернативы, вы можете полностью отключить семантику копирования, сделав их объявления1 private, как реализация всех стандартных классов потока. Я бы предпочел такой подход.

1. Обратите внимание, что вам не нужно определять их. Достаточно просто объявить их в разделе private.

person Nawaz    schedule 05.10.2011
comment
Хорошая точка зрения. Или сделайте конструктор копирования и оператор присваивания закрытыми, если не было варианта использования для копирования. - person Simon Elliott; 05.10.2011
comment
Используя const foo::opener & my_foo_opener = my_foo.get_opener();, он не создает копию - person crazyjul; 05.10.2011
comment
@crazyjul: Да. И если он отключит копирование-семантику, как я предложил, то ему останется только этот вариант, т.е. работать только с ссылкой. - person Nawaz; 05.10.2011
comment
@Nawaz Да, работа со ссылкой выглядит наиболее полезной опцией. Единственная оставшаяся проблема заключается в том, чтобы обработать случай, когда foo уничтожается до foo::opener. Я видел, как люди используют для этого умные указатели. - person Simon Elliott; 05.10.2011
comment
Фактически, вы не должны не определять механизмы приватного копирования. В противном случае, если вы по ошибке их используете (в классе или его друзьях), вы можете узнать об этом только во время выполнения. Это ошибка, которую может быть трудно отследить. Если вы не определите их и используете по ошибке, ваш компоновщик обнаружит ошибку. - person wilhelmtell; 06.10.2011

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

Действительно ли foo настолько сложен (или он глобальный?) для создания и уничтожения, что вы не можете просто закрыть его деструктор, вместо того, чтобы использовать средство открытия, чтобы выполнить соответствующее открытие/закрытие?

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

person Mark B    schedule 05.10.2011
comment
Да, мне нужно подумать о том, как обрабатывать сбои, особенно в close(). foo сохраняет некоторое состояние, поэтому его нельзя уничтожить и создать заново. - person Simon Elliott; 05.10.2011

Я думаю, вы должны разделить свои проблемы:

  • один класс для хранения состояния, которое передается повсюду
  • один класс для обработки переходного состояния при открытии/закрытии, который также заботится обо всех «переходных» операциях, таких как запись

«Переходный» класс принимает класс «данные» в качестве параметра (по ссылке) и будет обновлять его во время различных вызовов метода.

Затем вы можете использовать типичный RAII для переходного класса и по-прежнему распространять состояние повсюду.

person Matthieu M.    schedule 05.10.2011

Это классический случай использования RAII. Используйте конструктор и деструктор: если вы хотите снова open(), создайте новый экземпляр foo. Идея RAII заключается в том, что создание экземпляра представляет собой использование одного ресурса. Так вы получаете гарантию на очистку ресурса.

struct foo {
    foo() { open(); }
    ~foo() { close(); }
    void write(const std::string& s);
private:  // or public
    foo(const foo&);
    foo& operator=(const foo&);
};

// ...
{ foo fooa;
    fooa.write("hello 0");
} { foo foob;
    foob.write("hello 1");
}
person wilhelmtell    schedule 05.10.2011
comment
Он снова хочет открыть его тем же предметом! - person Nawaz; 05.10.2011
comment
foo сохраняет некоторое состояние, поэтому его нельзя уничтожить и создать заново. Возможно, это было непонятно из моего примера. Я хотел, чтобы пример был максимально простым, возможно, он был слишком простым. Я добавил комментарий в частный раздел класса, чтобы указать, что объект должен сохранять какое-то состояние. - person Simon Elliott; 05.10.2011
comment
@wilhelmtell: стандартные потоковые классы предоставляют эту функцию. Вы можете использовать один и тот же объект потока для открытия и закрытия, а затем открыть другой файл. - person Nawaz; 05.10.2011

Вы можете добавить в класс флаг, для которого установлено значение true, если вы вызвали open(), и значение false, если вы вызвали close(). Если вызывается open(), вы можете проверить, является ли флаг истинным или ложным, и закрыть, если вам нужно это сделать, прежде чем продолжить.

person sta    schedule 05.10.2011
comment
Это будет работать, а также позволит проверить запись() без предшествующей операции open(). Однако он не будет вызывать close(), если возникнет неперехваченное исключение или при выходе из программы, так как он будет вызывать close() только в том случае, если за ним следует open(). Я думаю, что решение RAII, вероятно, проще и более детерминировано. - person Simon Elliott; 05.10.2011
comment
Какой способ вы предпочитаете! Однако вы также можете проверить флаг в деструкторе. Поскольку при возникновении исключения должен вызываться деструктор, вы можете проверить там флаг и вызвать close() перед уничтожением объекта. - person sta; 05.10.2011