【问题标题】:What could be a reason to not use bracket classes in C++?什么可能是不在 C++ 中使用括号类的原因?
【发布时间】:2009-08-12 12:29:53
【问题描述】:

通常需要完成以下任务:更改某物的状态,执行操作,然后将状态更改回原始状态。例如,在 Win32 GDI 中,需要更改背景颜色,然后进行一些绘图,然后将颜色更改回来。

可以直接完成:

COLORREF oldColor = SetBkColor( deviceContext, newColor );
drawStuff( deviceContext );
SetBkColor( deviceContext, oldColor );

或者通过括号类在构造函数中进行前向更改和在析构函数中进行后向更改:

CBkColorSwitcher switcher( deviceContext, newColor );
drawStuff( deviceContext );
//once control reaches end of block the switcher is destroyed and the change is reverted

方括号类的优势是显而易见的——如果在更改之间引发异常,则更改会被正确还原。有什么缺点?

【问题讨论】:

    标签: c++ raii


    【解决方案1】:

    这实际上是一个众所周知且广泛使用的 C++ 习语,称为RAII。 Win32 API 是 C API,它们的实现模式不同。如果您使用 C++ 进行编程,最好使用 RAII 习惯用法来处理资源分配和释放,方法是在 C API 上编写瘦包装器,或者更好地重用现有的、设计良好的 C++ 替代品。 Java 程序员可以将 RAII 视为 finally 子句的替代品。

    【讨论】:

    • Java 程序员可以将 finally 子句视为模仿更优雅的 RAII 技术效果的技术组合。最终将责任从设计师转移到用户身上,这会导致更多潜在错误,因为用户需要与设计师一样熟练才能正确。使用 RAII,用户受到保护,正确性的责任落在设计者身上。
    【解决方案2】:

    有几个缺点,您需要编写更多代码,最终会创建更多对象。 您无法控制它的使用。用错了,你就失去了好处。例如

    CBkColorSwitcher * switcher = new CBkColorSwitcher(......)
    

    也就是说,我认为优点远远大于缺点,并且更喜欢括号类方法

    【讨论】:

    • 花在编写 C++ 包装器上的额外努力得到了回报,因为更冗长和容易出错的 C 代码所产生的错误数量更少。
    • 我不同意必须编写更多代码。更多的牙套,是的。但是在最终代码中设置/恢复行的数量减少了 2 倍。
    • 通常不是更多的代码。如果您必须多次执行相同的操作,则能够重用该类可以节省代码。如果抛出异常,它还可以确保您的更改得到恢复。没有它,您必须编写一个 try/catch 才能恢复到初始状态。
    • @Glen:jalf 指出您断言更多代码是错误的。您为具有构造函数/析构函数的类添加 6 行。然后每次使用减去 1 行。因此,只要使用 6 次或更多,无需额外代码。
    • @Glen:使用不正确的东西总是会失去任何优势。就像忘记成对使用 C 函数一样。所以这不是一个真正的原因。注意像这样使用 NEW 总是错误的(异常安全)。在现代 C++ 中,指针总是包裹在智能指针中。
    【解决方案3】:

    我认为这是一种最佳做法。我没有看到任何缺点(除了可读性?)

    【讨论】:

      【解决方案4】:

      我看到的缺点是(至少在您给出的示例中):

      • 除非代码的读者理解括号类的职责,否则您实际上在做什么不太清楚。
      • 创建括号类的成本
      • 编写代码来实现括号类将比没有它的版本花费更长的时间。

      话虽如此,使用 RAII 是做到这一点的“正确”方式,而且利大于弊。

      【讨论】:

        【解决方案5】:

        我在这里吹毛求疵,但是:

        1. 代码大小,由于异常处理程序,您的代码会更大。
        2. 您需要编写很多类来处理各种开关。
        3. 更大的堆栈。
        4. 始终针对所有异常执行代码,即使不需要(例如,您只想让应用程序崩溃)

        【讨论】:

        • 哪个异常处理程序?堆栈大小非常微不足道。没有状态的对象通常只占用一个字节。
        • @jalf 我说的是编译器插入的代码,以便无论如何调用 dtor。至于你的其他评论,正如我所说,我在吹毛求疵,无论如何我不知道你是如何想出那个没有状态的对象需要一个字节的。
        • 技术上一个对象应该有一个地址,即使它的大小为 0 字节,但是如果没有人获取它的地址,编译器可以(也不会)执行任何指令来为其分配存储空间,而只是调用析构函数 (甚至为析构函数添加内联代码)在“对象”将被销毁的位置。
        • 在 C/C++ 中,根据相应规范中“对象”的定义,对象的大小不能为 0 字节。 IE。对于没有类型 T,sizeof(T) 永远可以为 0。当然,如果没有人以任何直接或间接的方式实际观察到该大小,编译器可以按照 Charles 的描述进行优化。在实践中,我所知道的所有 C++ 编译器都会将一个典型的无状态作用域保护类优化为一对构造函数和析构函数调用(并且通常也将它们内联)。
        【解决方案6】:

        一个缺点是您通常实际上必须定义一个新类,这往往会产生一些开销。

        除此之外,它是(另一个)非常常见的 RAII 示例,通常是一种非常好的方法。

        编辑:如果您可以使用shared_ptr 在退出范围时执行任意代码块,而不是编写一个类,而不是编写一个函数。我认为这对于大多数应用程序来说可能有点太可爱了。

        【讨论】:

        • 您的意思是使用 shared_ptr 的 RAII 属性吗?你能详细说明一下吗?
        • 查看链接。 shared_ptr 可以使用自定义删除器。您可以(ab)使用它(并且可能绑定)通过执行任意函数来“删除” void 指针。离开块作用域时,将调用自定义删除器。
        • Alexandrescu 的 ScopeGuard 可能是一个更具可读性和表现力的替代方案
        • @Phil Nash:我同意,我试图找到一个例子来反驳“编写新课程是相当多的开销”这一点,shared_ptr 滥用首先浮现在脑海中。
        • shared_ptr 带有自定义删除器通常用作管理句柄的一种方式。要执行任意代码,ScopeGuard 确实是更合适的选择。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2013-02-16
        • 2021-09-01
        • 2012-01-01
        • 1970-01-01
        • 2011-07-28
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多