(Ab)使用构造函数和析构函数进行副作用的不良做法?备择方案?

Tho*_*mas 21 c++ constructor destructor design-patterns

在OpenGL中,人们经常编写如下代码:

glPushMatrix();
// modify the current matrix and use it
glPopMatrix();
Run Code Online (Sandbox Code Playgroud)

本质上,状态已更改,然后执行一些使用新状态的操作,最后恢复状态.

现在这里有两个问题:

  1. 忘记恢复状态很容易.
  2. 如果中间的代码抛出异常,则永远不会恢复状态.

在真正的基于对象的编程风格中,我编写了一些实用程序类来克服这些问题,如下所示:

struct WithPushedMatrix {
    WithPushedMatrix() { glPushMatrix(); }
    ~WithPushedMatrix() { glPopMatrix(); }
};
Run Code Online (Sandbox Code Playgroud)

现在我可以简单地写下我之前的例子:

WithPushedMatrix p;
// modify the current matrix and use it
Run Code Online (Sandbox Code Playgroud)

恢复的确切时刻取决于生命周期p.如果抛出异常,则会p调用析构函数,恢复状态,并且生命是好的.

不过,我并不完全开心.特别是如果构造函数接受一些参数(例如flags glEnable),很容易忘记将对象赋值给变量:

WithEnabledFlags(GL_BLEND); // whoops!
Run Code Online (Sandbox Code Playgroud)

临时性立即被破坏,状态变化过早地被逆转.

另一个问题是,阅读我的代码的其他人可能会感到困惑:"为什么这里声明的变量从未被使用?让我们摆脱它!"

所以,我的问题:这是一个好模式吗?它甚至可能有名字吗?我忽略了这种方法有什么问题吗?最后但并非最不重要的:有什么好的选择吗?

更新:是的,我想这是RAII的一种形式.但不是通常使用RAII的方式,因为它涉及一个看似无用的变量; 永远不会明确访问有问题的"资源".我只是没有意识到这种特殊用法是如此常见.

tza*_*man 24

我喜欢使用RAII控制OpenGL状态的想法,但我实际上更进一步:让你的WithFoo类构造函数将一个函数指针作为参数,它包含你想在该上下文中执行的代码.然后不要创建命名变量,只需使用临时工具,将要在该上下文中执行的操作作为lambda传递.(需要C++ 0x,当然 - 也可以使用常规函数指针,但它不是那么漂亮.)
像这样:( 编辑恢复异常安全)

class WithPushedMatrix
{
public:
    WithPushedMatrix()
    {
        glPushMatrix();
    }

    ~WithPushedMatrix()
    {
        glPopMatrix();
    }

    template <typename Func>
    void Execute(Func action)
    {
        action();
    }
};
Run Code Online (Sandbox Code Playgroud)

并像这样使用它:

WithPushedMatrix().Execute([]
{
    glBegin(GL_LINES);
    //etc. etc.
});
Run Code Online (Sandbox Code Playgroud)

临时对象将设置您的状态,执行操作然后自动将其拆除; 你没有浮动的"松散"状态变量,并且在上下文中执行的动作与它紧密相关.您甚至可以嵌套多个上下文操作,而无需担心析构函数顺序.

您甚至可以更进一步,制作一个通用WithContext类,它需要额外的设置和拆卸功能参数.

编辑:必须将action()调用移动到单独的Execute函数以恢复异常安全 - 如果在构造函数中调用并抛出,则不会调用析构函数.

edit2:通用技术 -

所以我更多地提出了这个想法,并想出了更好的东西:
我将定义一个With类,它创建上下文变量并将其填充到std::auto_ptr它的初始化器中,然后调用action:

template <typename T>
class With
{
public:
    template <typename Func>
    With(Func action) : context(new T()) 
    { action(); }

    template <typename Func, typename Arg>
    With(Arg arg, Func action) : context(new T(arg))
    { action(); }

private:
    const std::auto_ptr<T> context;
};
Run Code Online (Sandbox Code Playgroud)

现在,您可以将它与您最初定义的上下文类型组合:

struct PushedMatrix 
{
    PushedMatrix() { glPushMatrix(); }
    ~PushedMatrix() { glPopMatrix(); }
};
Run Code Online (Sandbox Code Playgroud)

并像这样使用它:

With<PushedMatrix>([]
{
    glBegin(GL_LINES);
    //etc. etc.
});
Run Code Online (Sandbox Code Playgroud)

要么

With<EnabledFlag>(GL_BLEND, []
{
    //...
});
Run Code Online (Sandbox Code Playgroud)

优点:

  1. 异常安全由auto_ptrnow 处理,因此如果action抛出,上下文仍将被正确销毁.
  2. 不再需要一种Execute方法,所以它看起来又干净了!:)
  3. 你的"上下文"类很简单; 所有逻辑都由With类处理,因此您只需为每种新类型的上下文定义一个简单的ctor/dtor.

一个小问题:正如我上面所写,你需要为ctor声明手动重载,以获得所需数量的参数; 虽然即使只有一个应该涵盖大多数OpenGL用例,但这并不是很好.这应该用可变参数模板整齐地修复 - 只需typename Arg在ctor中替换typename ...Args- 但它将依赖于编译器支持(MSVC2010还没有它们).

  • 也是+1,它在语义上更好,因为"withPushedMatrix"中的代码实际上是"withPushedMatrix",它看起来不像黑客; 我想我会将它用于我自己的OpenGL应用程序:p (2认同)
  • 等等,有一个问题:如果lambda内发生异常,则不会调用~WithPushedMatrix的析构函数; 一个关于"action()"的try-catch的简单函数实际上会更好 (2认同)
  • -1:当我*在lambda块*时,如何从封闭函数返回?这个解决方案阻碍了控制流程,我无法突破阻止.不太好. (2认同)

sha*_*oth 22

使用这样的对象称为RAII,对于资源管理来说非常典型.是的,有时您会因为忘记提供可变名称而过早销毁临时对象.但是你在这里有一个很大的优势 - 代码变得更安全和更清晰 - 你不必在所有可能的代码路径上手动调用所有清理内容.

一个建议:使用合理的变量名,而不是p.称之为matrixSwitcher或类似的东西,以便读者不认为它是一个无用的变量.

  • 如何在4小时内获得+21投票?答案确定了RAII模式,就像**几乎所有其他答案一样**,除此之外**重复OP已经知道的**.它以一种含糊的建议结束,以有意义的方式命名变量.这充其量是一个平庸的答案,如果这意味着200个声誉,那么SO声誉系统就会被严重破坏.我已经看到了关于SO的聪明,深思熟虑和复杂的答案,结果是+3或更少,这是+21? (4认同)

sbi*_*sbi 6

正如其他人所指出的,这是C++中众所周知和鼓励的模式.

处理遗忘变量名的问题的一种方法是定义操作,以便它们需要变量.通过使可能的行动成为RAII类的成员:

PushedMatrix pushed_matrix;;
pushed_matrix.transform( /*...*/ );
Run Code Online (Sandbox Code Playgroud)

或者通过使函数将RAII类作为参数:

PushedMatrix pushed_matrix;
transform_matrix( pushed_matrix, /*...*/ );
Run Code Online (Sandbox Code Playgroud)


Nor*_*ame 5

我想指出,我的答案实际上包含了有用的信息(更多的是对RAII的模糊引用,显然有19个值得称赞).它不需要c ++ 0x工作,根本不是假设,而是修复了与声明变量需求相关的OP问题.


有一种非常好的方法可以在语法上增强RAII结构(或更精确地说:ScopeGuards):if()语句接受作用于if块的声明:

#include <stdio.h>

class Lock
{
    public:
    Lock() { printf("locking\n"); }
    ~Lock() { printf("unlocking\n"); }
    operator bool () const { return true;}
};
int main()
{
    // id__ is valid in the if-block only
    if (Lock id_=Lock()) {  
        printf("..action\n");
    }
}
Run Code Online (Sandbox Code Playgroud)

这打印:

locking
..action
unlocking
Run Code Online (Sandbox Code Playgroud)

如果我们添加一些语法糖,我们可以写

#define WITH(X) if (X with_id_=X())
int main()
{
    WITH(Lock) {    
        printf("..action\n");
        WITH(Lock) {
            printf("more action\n");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在我们使用这样一个事实:只要const引用保持在范围内,用于初始化const引用的temporaries仍然存活,以使其与参数一起工作(我们还修复了WITH(X)接受尾随的麻烦) :

   #include <stdio.h>
   class ScopeGuard 
   {
    public:
    mutable int dummy;
    operator bool () const { return false;}
    ScopeGuard(){}
    private:
    ScopeGuard(const ScopeGuard &); 
    }; 
    class Lock : public ScopeGuard
    {
        const char *s;
        public: 
        Lock(const char *s_) : s(s_) { printf("locking %s\n",s); }
        ~Lock() { printf("unlocking %s\n",s); }
    };

    #define WITH(X) if (const ScopeGuard& with_id_=X)  {} else 
    int main()
    {
        WITH(Lock("door")) {    
            printf("..action\n");
            WITH(Lock("gate")) {
                printf("more action\n");
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

TATA!

这种方法的一个很好的副作用是,所有"受保护的"区域都是通过WITH(...) {...}模式统一识别​​的- 这是代码评论等的一个很好的属性.