【问题标题】:C++ RAII to manage change and reversion of object stateC++ RAII 用于管理对象状态的更改和恢复
【发布时间】:2011-10-05 14:20:14
【问题描述】:

我有一个班级 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()一个打开()。

编辑 正如 Nawaz 在下面指出的那样,一个真正的类还需要一个复制构造函数和赋值运算符。

这似乎有很多样板,只是为了确保调用 close()。出现两个问题:

  1. 这还是比强制我的班级的用户使用 try/catch 更简单吗?

  2. 有没有更简单的方法来实现我想要的:提供基本的异常保证并确保 close() 始终遵循 open()?

【问题讨论】:

    标签: c++ raii


    【解决方案1】:

    嵌套类opener 应该实现复制语义,因为如果我正确理解您的意图,编译器生成的默认代码会产生不良结果。

    所以请实现copy-constructor和copy-assignment。

    或者,您可能希望完全禁用复制语义,通过声明他们1private,就像implementation of all standard stream classes。我更喜欢这种方法。

    1.请注意,您不需要定义它们。只需在 private 部分声明它们就足够了。

    【讨论】:

    • 好点。或者,如果没有用于复制的用例,则将复制构造函数和赋值运算符设为私有。
    • 使用const foo::opener &amp; my_foo_opener = my_foo.get_opener();不会创建副本
    • @crazyjul:是的。如果他按照我的建议禁用了复制语义,那么他就剩下该选项,即仅使用 reference
    • @Nawaz 是的,使用参考似乎是最有用的选项。剩下的唯一问题是处理 foo 在 foo::opener 之前被销毁的情况。我见过人们为此使用智能指针。
    • 事实上,您应该定义私有复制机制。否则,如果你不小心使用了它们(在类或它的朋友中),你只能在运行时发现它。这是一个可能难以追踪的错误。如果您没有定义它们并错误地使用它们,您的链接器会发现错误。
    【解决方案2】:

    关闭会失败吗?如果可以,那么无论采用何种方法,您都需要格外小心。我认为 RAII 方式比强制异常处理/关闭用户更简单。

    foo 真的如此复杂(或者它是一个全局的?)来创建和销毁,以至于你不能只让它的析构函数调用 close 而不是使用开启器来执行匹配的打开/关闭?

    或者,如果这是实现某种事务语义,我看不到比 opener 类更简单的方法(但如其他答案中所述,您可能希望禁用 opener 类的复制和分配)。

    【讨论】:

    • 是的,我需要考虑一下如何处理失败,尤其是在 close() 中。 foo 保留了一些状态,因此不能被销毁和重新创建。
    【解决方案3】:

    我认为你应该把你的顾虑分开:

    • 一个类来存储贯穿始终的状态
    • 一个类来处理打开/关闭中的瞬态,它还负责所有“瞬态”操作,如写入

    “瞬态”类将“数据”类作为参数(通过引用),并将在各种方法调用期间对其进行更新。

    然后您可以在瞬态类上使用典型的 RAII,并且仍然可以在整个过程中传播状态。

    【讨论】:

      【解决方案4】:

      这是使用 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");
      }
      

      【讨论】:

      • 他又想用同一个对象打开它!
      • foo 保留了一些状态,因此不能被销毁和重新创建。这在我的例子中可能不太清楚。我想让这个例子尽可能简单,也许它太简单了。我在类的私有部分添加了一条注释,表明该对象必须保留一些状态。
      • @wilhelmtell:标准流类提供了这个特性。您可以使用相同的流对象打开和关闭,然后打开另一个文件。
      【解决方案5】:

      您可以在类中添加一个标志,如果您调用 open(),则设置为 true,如果调用 close(),则设置为 false。如果调用了 open(),您可以检查标志是真还是假,如果需要,在继续之前关闭。

      【讨论】:

      • 这会起作用,并且还允许在没有前面的 open() 的情况下检查 write()。但是,如果有未捕获的异常或在程序退出时,它不会调用 close(),因为它只会在后面跟着一个 open() 时调用 close()。我认为 RAII 解决方案可能更简单、更具确定性。
      • 您喜欢哪种方式!但是,您也可以检查析构函数中的标志。由于发生异常时应该调用析构函数,因此您可以检查那里的标志并在销毁对象之前调用 close()。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-09-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-08-26
      • 1970-01-01
      相关资源
      最近更新 更多