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

时间:2010-07-22 11:33:26

标签: c++ design-patterns constructor destructor

在OpenGL中,通常会编写如下代码:

glPushMatrix();
// modify the current matrix and use it
glPopMatrix();

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

现在有两个问题:

  1. 很容易忘记恢复状态。
  2. 如果中间的代码抛出异常,则状态永远不会恢复。
  3. 在真正的基于对象的编程风格中,我编写了一些实用程序类来克服这些问题,如下所示:

    struct WithPushedMatrix {
        WithPushedMatrix() { glPushMatrix(); }
        ~WithPushedMatrix() { glPopMatrix(); }
    };
    

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

    WithPushedMatrix p;
    // modify the current matrix and use it
    

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

    不过,我并不是很开心。特别是如果构造函数接受一些参数(例如glEnable的标志),很容易忘记将对象分配给变量:

    WithEnabledFlags(GL_BLEND); // whoops!
    

    临时被立即销毁,状态变化过早被逆转。

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

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

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

10 个答案:

答案 0 :(得分:24)

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

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

    ~WithPushedMatrix()
    {
        glPopMatrix();
    }

    template <typename Func>
    void Execute(Func action)
    {
        action();
    }
};

并像这样使用它:

WithPushedMatrix().Execute([]
{
    glBegin(GL_LINES);
    //etc. etc.
});

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

你甚至可以更进一步,制作一个通用的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;
};

现在,您可以将其与最初定义的上下文类型结合使用:

struct PushedMatrix 
{
    PushedMatrix() { glPushMatrix(); }
    ~PushedMatrix() { glPopMatrix(); }
};

并像这样使用它:

With<PushedMatrix>([]
{
    glBegin(GL_LINES);
    //etc. etc.
});

With<EnabledFlag>(GL_BLEND, []
{
    //...
});

优点:

  1. 异常安全现在由auto_ptr处理,因此如果action抛出,则上下文仍会被正确销毁。
  2. 不再需要Execute方法,所以它看起来又干净了! :)
  3. 你的“上下文”类很简单;所有逻辑都由With类处理,因此您只需为每种新类型的上下文定义一个简单的ctor / dtor。
  4. 一个小问题:正如我上面写的那样,你需要为ctor声明手动重载,以获得所需数量的参数;虽然即使只有一个应该覆盖大多数OpenGL用例,但这并不是很好。 这应该用可变参数模板巧妙地修复 - 只需用typename Arg替换ctor中的typename ...Args - 但它依赖于编译器对此的支持(MSVC2010还没有它们)。

答案 1 :(得分:22)

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

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

答案 2 :(得分:6)

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

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

PushedMatrix pushed_matrix;;
pushed_matrix.transform( /*...*/ );

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

PushedMatrix pushed_matrix;
transform_matrix( pushed_matrix, /*...*/ );

答案 3 :(得分:5)

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


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

#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");
    }
}

打印:

locking
..action
unlocking

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

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

现在我们使用的事实是,只要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");
            }
        }
    }

<强> TATA!

这种方法的另一个好处是,所有“受保护”区域都是通过WITH(...) {...}模式统一识别的 - 这是代码评论et.al的一个很好的属性。

答案 4 :(得分:4)

警告:面向C ++ 0x的答案

您使用的模式是RAII,它广泛用于资源管理。唯一可行的替代方法是使用try-catch块,但它通常会使您的代码太乱。

现在的问题。 首先,如果您不想为OpenGL函数的每个组合编写不同的类,那么C ++ 0x的另一个好处是您可以编写lambda函数并将它们存储在变量中。所以,如果我是你,我会创建一个这样的类:

template<typename Destr>
class MyCustom {
public:
    template<typename T>
    MyCustom(T onBuild, Destr onDestroy) : 
        _onDestroy(std::move(onDestroy))
    {
        onBuild();
    }

    ~MyCustom() { _onDestroy(); }

private:
    Destr    _onDestroy;
};

template<typename T1, typename T2>
MyCustom<T2> buildCustom(T1 build, T2 destruct)   { return MyCustom<T2>(std::move(build), std::move(destruct)); }

然后你可以像这样使用它:

auto matrixPushed = buildCustom([]() { glPushMatrix(); }, []() { glPopMatrix(); });

甚至更好:

auto matrixPushed = buildCustom(&glPushMatrix, &glPopMatrix);

这也解决了“为什么这里有无用的变量”的问题,因为它的目的现在变得明显了。

传递给构造函数的函数应该内联,因此没有性能开销。析构函数应该像函数指针一样存储,因为在括号[]中没有任何内容的lambda函数应该像普通函数一样实现(根据标准)。

使用“buildCustom”也可以部分解决“变量立即销毁”问题,因为您可以更容易地看到忘记变量的位置。

答案 5 :(得分:3)

为了帮助您了解c ++程序员多长时间这样做,我在90年代后期与COM一起学习了这项技术。

我认为,使用c ++堆栈帧和析构函数的基本属性来确保对象生命周期管理更容易的确切机制是个人选择。我不会走得太远,以避免需要分配变量。

(接下来的事情我不是百分百肯定,但是我希望有人会插话 - 我知道我过去已经这样做但我现在在谷歌找不到它我一直在努力记住......看,垃圾收集者已经把我的思绪弄糊涂了!)

我相信你可以用一对普通的旧卷曲(POPOC)强制范围。

{ // new stack frame
  auto_ptr<C> instanceA(new C);
  {
     auto_ptr<C> instanceB(new C);
  }
  // instanceB is gone
} 
// instanceA is gone

答案 6 :(得分:1)

这是典型的RAII示例。这种方法的缺点是出现了许多其他类。 要解决此问题,您可以创建通用的“guard”类(如果可能)。 还有另一种选择:提升“范围退出”库(http://www.boost.org/doc/libs/1_43_0/libs/scope_exit/doc/html/index.html)。你可以尝试一下,如果你当然可以依靠提升。

答案 7 :(得分:1)

想到了{p> ScopeGuard。请注意,使用C ++ 0x绑定和可变参数模板,它可能会被重写为更短。

答案 8 :(得分:0)

我以前从未见过这个,但我必须承认,这有点酷。 但我不会使用它,因为它不是很直观。

编辑:正如尖锐的指出,这被称为RAII。我在维基百科上找到的例子也包含了方法调用中对资源的操作。在您的示例中,这将如下所示:

WithPushedMatrix p;
p.setFLag(GL_BLEND);
p.doSomething();

然后很清楚变量是什么,如果他们读取你的代码,其他开发人员将获得直觉。当然,OpenGL代码会被隐藏,但我认为人们很快就会习惯它。

答案 9 :(得分:0)

我认为这很棒,而且是惯用的C ++。缺点是你基本上在C OpenGL库周围编写一个(自定义)包装器。如果这样的图书馆存在,那可能会很棒,也许就像一个(半)官方的OpenGL ++ lib。 也就是说,我已经编写了这样的代码(来自Memory),并对它非常满意:

{
  Lighting light = Light(Color(128,128,128));
    light.pos(0.0, 1.0, 1.0);
  Texture tex1 = Texture(GL_TEXTURE1);
    tex1.set(Image("CoolTex.png"));

  drawObject();
}

编写包装器的开销并不是非常繁琐,并且生成的代码与手写代码一样好。恕我直言,相比OpenGL代码更容易阅读,即使你不知道包装纸。