C ++ RAII для управления изменением и возвращением состояния объекта - PullRequest
1 голос
/ 05 октября 2011

У меня есть класс 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 ().

РЕДАКТИРОВАТЬ Как указывает Наваз ниже, для реального класса также потребуется конструктор копирования и оператор присваивания.

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

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

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

Ответы [ 5 ]

2 голосов
/ 05 октября 2011

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

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

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

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

1 голос
/ 05 октября 2011

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

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

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

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

1 голос
/ 05 октября 2011

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

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

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

0 голосов
/ 05 октября 2011

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

0 голосов
/ 05 октября 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");
}
...