【问题标题】:When is a function try block useful?什么时候函数 try 块有用?
【发布时间】:2011-08-02 12:28:06
【问题描述】:

我想知道程序员何时使用函数 try 块。什么时候有用?

void f(int i)
try
{
   if ( i  < 0 ) 
      throw "less than zero";
   std::cout << "greater than zero" << std::endl;
}
catch(const char* e)
{
    std::cout << e << std::endl;
}

int main() {
        f(1);
        f(-1);
        return 0;
}

输出:(ideone

greater than zero
less than zero

编辑:有些人可能认为函数定义的语法不正确(因为语法看起来不熟悉),我不得不说不,它不是不正确的。它称为功能尝试块。请参阅 C++ 标准中的 §8.4/1 [dcl.fct.def]。

【问题讨论】:

  • 我看到它被滥用来在 C++ 中实现 void f() synchronized { }
  • @Johannes:这个synchronized 来自哪里?任何人都可以使用synchronized 滥用它,无论它在 C++ 中意味着什么?
  • @Nawaz See drdobbs.com/184401728, "最后,Java 的 synchronized 关键字适用于整个方法..."
  • @Johannes:将此作为答案,以便我们投票。

标签: c++ function function-try-block


【解决方案1】:

您在构造函数中使用它来捕获初始化程序的错误。通常,您不会发现这些错误,因此这是一个非常特殊的用途。

否则,它是无用的:除非我被证明是错误的,

void f() try { ... } catch (...) { ... }

严格等价于

void f() { try { ... } catch (...) { ... } }

【讨论】:

  • ctor-initializer。初始化列表是另一回事。
  • 警告:在构造函数和析构函数中,异常是在catch结束后仍然_implicitly_重新抛出,所以它们仍然几乎没用 - 参见例如:informit.com/guides/content.aspx?g=cplusplus&seqNum=496
  • @akavel:它们不是没用的。您可能希望在重新抛出异常之前执行一些清理或记录。或者您可能想抛出另一个异常(这是允许的)。其实这个行为是可以理解的:如果子对象或者基础构造失败,那么对象就不能构造成功,不得不抛出。
  • @akavel 您可以从析构函数的 catch 块中 return,在这种情况下,不会重新抛出异常,因为正确销毁对象的目标被认为已完成。对于构造函数,我同意 Alexandre C. 的评论。
【解决方案2】:

函数 try 块在两种情况下对我很有用。

a) 在main() 周围设置一个 catch all 子句,允许编写小型实用程序而不必担心本地错误处理:

int main()
try {
    // ...
    return 0;
}
catch (...) {
    // handle errors
    return -1;
}

这显然只是 main() 本身内部的 try/catch 的语法糖。

b) 处理基类构造函数抛出的异常:

struct B {
     B() { /*might throw*/ }
};

struct A : B {
     A() 
     try : B() { 
         // ... 
     } 
     catch (...) {
         // handle exceptions thrown from inside A() or by B() 
     } 
};

【讨论】:

  • (a) 糖通常是甜的。我认为那是“甜蜜的”。 (b) 基类构造失败通常会导致派生类构造失败,如果您未捕获的异常并不表示阻止基类构造的异常情况,那么您就是在滥用异常;这是 IMO 的代码气味。
  • @hkaiser:如果您的基础对象抛出但您仍然可以让派生对象处于“有效状态”,我认为您使用继承不当。如果基础对象不处于有效状态,则派生对象不能处于有效状态。 (注意,objects 不是 classes。)
  • 有时你并不关心构造函数抛出了什么;您只想捕获 any 异常,然后抛出 known 异常,因为上游有一个处理程序需要区分“我的构造函数出于某种原因抛出”与“其他一些代码mine throw",阻止它使用包罗万象。
  • @Tomalak:构造函数的catch 块总是抛出或重新抛出。在它的末尾添加了一个隐含的throw;。所以,即使@hkaiser 写了这个,它并不意味着该对象是有效的。
  • @Tomalak:我同意你的观点:我会接受这种结构的唯一有效情况是当你从第 3 方类继承时,它会抛出一个不在你的树中的异常,并且你想要转换。其他所有情况(成员对象构造错误也是如此)都有异味。
【解决方案3】:

除了提到的函数用途之外,您还可以使用 function-try-block 来为自己节省一层缩进。 (Ack,关于编码风格的答案!)

通常你会看到这样的函数try-block示例:

void f(/*...*/)
try {
   /*...*/
}
catch(/*...*/) {
    /*...*/
}

函数范围缩进到与没有函数尝试块相同的级别。这在以下情况下很有用:

  • 您的列限制为 80 个字符,并且由于有额外的缩进,您必须换行。
  • 您正在尝试使用 try catch 改造某些现有函数,并且不想触及函数的所有行。 (是的,我们可以使用git blame -w。)

不过,对于完全用函数尝试块包装的函数,我建议不要在一些使用函数尝试块的函数和一些不在同一代码库中的函数之间交替使用。一致性可能比换行问题更重要。 :)

【讨论】:

    【解决方案4】:

    关于函数 try 块如何操作的注意事项:

    • 对于构造函数,函数 try 块包含数据成员和基类的构造。

    • 对于析构函数,函数 try 块包含数据成员和基类的析构。它变得复杂,但对于 C++11,您必须在您的析构函数(或基类/成员类)的声明中包含 noexcept(false),否则任何破坏异常都将导致在 catch 块结束时终止。可以通过在 catch 块中添加 return 语句来防止这种情况发生(但这不适用于构造函数)。

    • 构造函数或析构函数中的 catch 块必须抛出一些异常 (or it will implicitly rethrow the caught exception)。简单地return 是不合法的(至少在构造函数的函数 catch 块中)。但是请注意,您可以调用 exit() 或类似名称,这在某些情况下可能有意义。

    • catch 块不能返回值,因此它不适用于返回非 void 的函数(除非它们故意使用 exit() 或类似名称终止程序)。至少那是what I've read

    • constructor-function-try 的 catch 块不能引用数据/基础成员,因为它们要么 1) 没有被构造,要么 2) 在 catch 之前被破坏。因此,函数 try 块对于清理对象的内部状态没有用——当您到达那里时,该对象应该已经完全“死”了。 这一事实使得在构造函数中使用函数 try 块非常危险,因为如果您的编译器没有碰巧标记它,随着时间的推移很难监管这条规则。

    有效(合法)用途

    • 将构造函数或其基/成员构造函数期间抛出的异常(转换为不同的类型/消息)。
    • 在析构函数或其基/成员析构函数期间转换或吸收和引发异常(尽管有destructor etiquette)。
    • 终止程序(可能带有有用的消息)。
    • 某种异常日志记录方案。
    • 返回 void 函数的语法糖恰好需要完全封装的 try/catch 块。

    【讨论】:

    • 看起来虽然return 语句在构造函数的函数-try-block 的catch-clause(s) 中是不允许的,但它们中是允许的正常功能;实际上,对于返回void 的函数,到达普通函数的catch 子句的末尾等效于return;,否则为undefined。这在 en.cppreference.com/w/cpp/language/function-try-block 的最后一个示例中显示,并且在 catch 子句中包含 return 语句的代码在 GCC 和 Visual Studio 上都可以编译。
    • 看起来析构函数使用了ctor和普通函数规则的组合:到达catch子句的末尾通常相当于throw;,但允许返回语句。
    • 有关在普通函数上演示此行为的程序,请参阅ideone.com/YM62JS
    【解决方案5】:

    如果您想从构造函数的初始化程序中捕获异常,这可能会很有用。

    但是,如果您确实以这种方式在构造函数中捕获异常,则 必须 重新抛出它或抛出新异常(即,您不能正常地从构造函数返回)。如果你不重新抛出,它只会隐式发生。

    #include <iostream>
    
    class A
    {
    public:
      A()
      try {
        throw 5;
      }
      catch (int) {
        std::cout << "exception thrown\n";
        //return; <- invalid
      }
    };
    
    int main()
    {
      try {
        A a;
      }
      catch (...) {
        std::cout << "was rethrown";
      }
    }
    

    【讨论】:

    • ctor-initializer。初始化列表是另一回事。
    【解决方案6】:

    您可以使用它们的另一件事是在调试期间提供额外的数据,以不干扰完成的构建的方式。我没有看到其他人使用或提倡它,但我觉得它很方便。

    // Function signature helper.
    #if defined(_WIN32) || defined(_WIN64)
        #define FUNC_SIG __FUNCSIG__
    #elif defined(__unix__)
        #define FUNC_SIG __PRETTY_FUNCTION__
    // Add other compiler equivalents here.
    #endif  /* Function signature helper. */
    
    void foo(/* whatever */)
    #ifdef     DEBUG
    try
    #endif  /* DEBUG */
    {
        // ...
    }
    #ifdef     DEBUG
    catch(SomeExceptionOrOther& e) {
        std::cout << "Exception " << e.what() << std::endl
                  << "* In function: " << FUNC_SIG << std::endl
                  << "* With parameters: " << /* output parameters */ << std::endl
                  << "* With internal variables: " << /* output vars */ << std::endl;
    
        throw;
    }
    #endif  /* DEBUG */
    

    这将允许您在测试代码时获得有用的信息,并轻松地将其模拟出来而不影响任何事情。

    【讨论】:

      猜你喜欢
      • 2012-03-31
      • 1970-01-01
      • 2021-11-12
      • 1970-01-01
      • 1970-01-01
      • 2019-09-04
      • 2010-12-28
      • 1970-01-01
      相关资源
      最近更新 更多