【问题标题】:exception handling in constructor’s initializer list构造函数初始化列表中的异常处理
【发布时间】:2010-10-16 08:54:00
【问题描述】:

在我的项目中,我发现了一段代码,其中在构造函数的初始化列表中调用了一个方法。

Test2(Test* pTest):m_pTest(pTest), m_nDuplicateID(pTest->getTestID())
    {
    }

我观察到 Test2 的用户可能会将 NULL 传递给构造函数。由于指针在未经验证的情况下使用,因此存在访问冲突的可能性。

这促使我在构造函数的初始化列表中查看异常处理。我在一篇文章中发现 try 可以在初始化列表中使用。我写了一个小测试程序来测试这个概念:

//Test class stores the unique ID and returns the same with API getTestID
class Test
{
public:

    Test(int nID):m_nID(nID){
    }

    int getTestID() const
    {
            return m_nID;
    }
private:
    int m_nID;

};


class Test2
{
public:

    Test2(Test* pTest) 
        try :m_pTest(pTest), m_nDuplicateID(pTest->getTestID())
    {
    }
    catch (...) 
    {
        cout<<"exception cought "<< endl;
    }

    void printDupID()
    {
        cout<<"Duplicate ID" << m_nDuplicateID << endl;
    }
private:

    Test* m_pTest;
    int m_nDuplicateID;
};

int main(int argc, char* argv[])
{

    Test* pTest = new Test(10);


    Test2 aTest2(pTest);
    aTest2.printDupID();


    delete pTest;

    return 0;
}

此代码未在 VC6.0 中编译。我是否需要进行任何更改才能在 VC 6.0 中编译?

另外,在一篇文章中,我发现在构造函数的初始化列表中使用 try 并不严格符合 C++ 标准。在这种情况下,我们如何处理构造函数的初始化列表中的异常(标准处理方式)?

谢谢。

【问题讨论】:

  • VC6 因不符合标准 C++ 而臭名昭著。

标签: c++ exception-handling


【解决方案1】:

已经有很多有用的答案,但我会尝试添加一点,也许它会对某人有所帮助。

首先,正如其他人已经提到的 - 取消引用 nullptr 或无效指针(地址)不会在标准 C++ 中引发异常。 MSVC 通过其Structured Exception Handling 支持它,但它不是可移植的。在this answer 中阅读更多相关信息。

构造函数中的函数 try 块不允许您抑制异常,如果您不抛出另一个异常,它无论如何都会传播。而当你进入 catch 子句时,类的所有成员都已经被销毁了。因此,您可以做的唯一合理的事情是以某种方式记录错误,或者可能更改一些全局变量。这就是为什么它或多或少被认为是无用的。

至于你的初始代码

Test2(Test* pTest):m_pTest(pTest), m_nDuplicateID(pTest->getTestID())
    {
    }

您可以使用三元运算符在初始化列表中检查 pTest 是否为空,并在它为空的情况下执行一些适当的操作 - 只需根据其类型将 m_nDuplicateID 设置为 nullptr 或其他值,调用另一个函数并使用它返回类型等:

Test2(Test* pTest):
    m_pTest(pTest),
    m_nDuplicateID( pTest ? pTest->getTestID() : /*some value or call*/ )
{
}

您甚至可以使用多个嵌套的三元运算符来创建更复杂的执行流程。

为了完整起见,您的代码并非如此,但它可能会在相同情况下磨损某人。如果您使用类的成员 m_pTest 来初始化 m_nDuplicateID,这将取决于这些成员在类的声明中的顺序,因为初始化列表中的类成员按照声明的顺序进行初始化,而不是按照它们出现在初始化列表本身中的顺序进行初始化,所以这可能是个问题,最好避免成员初始化顺序依赖:

class A
{
    A( B* pTest );
    int m_nDuplicateID;
    B* m_pTest;
};

A::A( B* pTest ) :
    m_pTest( pTest ),
    m_nDuplicateID( m_pTest->someMethod() ) // here m_pTest isn't initialized yet,
                                            // so access violation probably
{
}

【讨论】:

    【解决方案2】:

    (对于谷歌同事)

    如果我们不想存储 ptr / shared_ptr 的副本,另一种解决方案:

    class Foo::Pimpl
    {
    public:
        bool paramTest_;
    
        Pimpl(ConstNodePtr root)
        try  :
           paramTest_( root ? true : throw std::invalid_argument("Foo (pimpl) constructed from NULL node")),
        ...
    {
        ...
    }  catch (...)
    {
        throw; // rethrow
    }
    

    【讨论】:

      【解决方案3】:

      你不能只使用一个函数来检查 ptr,例如:

      template<typename P>
      P* checkPtr (P* p)
      {
          if (p == 0)
              throw std::runtime_error ("Null pointer");
          return p;
      }
      
      class Test2
      {
      public:
          Test2 (Test* pTest)
              : m_pTest (checkPtr (pTest))
          {
          }
      
          Test* m_pTest;
      };
      

      【讨论】:

        【解决方案4】:

        人们还在使用 VC6? 说真的,VC6 几乎不是一个符合标准的编译器。帮自己一个忙,至少得到VS2005。 VC6是你的问题。 试试VS2008 express,看看能不能编译。

        当然,另一种选择是在构造上获取引用,这需要绑定。

        【讨论】:

        • 不幸的是,VC6 中有许多数百万行的单体 C++ 应用程序,要移植到现代 C++ 需要花费开发人员数年的时间,而且许多公司不想进行转换。
        • @Pete:那时这些公司不了解渐近增长。就是成本的增长。维护如此古老的代码并不便宜,也不会变得更便宜。
        • 我同意,但你并不总是获得足够的影响力来做出改变(尤其是作为承包商),而且通常还有很多更紧迫的问题需要处理。
        【解决方案5】:

        C++ 标准第 15 / 3 节

        一个函数-try-block关联一个 handler-seq 和ctor-initializer, 如果存在,以及功能体。一个 执行过程中抛出的异常 中的初始化表达式 ctor-initializer 或在 函数体的执行 将控制权转移给一个处理程序 功能尝试块的方式与 期间抛出异常 执行 try-block 传输 控制权交给其他处理程序。

        class C 
        {  
            int i;  
            double d;  
        public:  
            C(int, double);  
        };  
        
        C::C(int ii, double id)  
        try  : i(f(ii)), d(id)  
        {  
        //constructor function body  
        }  catch (...)  
        {  
        //handles exceptions thrown from the ctor-initializer  
        //and from the constructor functionbody  
        }
        

        【讨论】:

        • “Eric P”的回答中的文章说它不是标准的。除此之外,我想知道为什么 VC 6.0 不支持。
        • 来自文章“你不能在 Visual C++ 6.0 中编译它,因为它没有严格确认 C++ 标准。”
        • VC++ 6.0 早于标准问世
        【解决方案6】:

        首先,如果你取消引用 NULL 指针,标准 C++ 不保证会抛出异常,所以你的代码在这种情况下是无用的。

        其次,如果抛出异常,你的异常处理程序会做什么?

        第三,构造函数/函数异常块被广泛认为是浪费时间 - 请查看 http://www.gotw.ca/gotw/066.htm 和 Herb Sutter 的 GotW 网站上的其他文章。

        【讨论】:

        • 即使构造函数中的 pTest->getTestID() 也可能抛出异常。
        • 您不符合 Herb 的文章正义:构造异常块仅用于重新抛出不同的异常,或用于记录日志等副作用。出于这些目的,它们当然不是浪费时间。他们只是不能做更多的事情。
        • 是的,不要带着构造函数异常的东西去那里。只需检查输入参数并根据需要断言或抛出。
        【解决方案7】:

        根据this article,看起来您在 VC++ 6.0 中无法做到这一点

        您要么必须升级到 7.0,要么只在构造函数主体中进行初始化。

        【讨论】:

        • 我会升级,因为有时无法避免初始化列表(例如初始化引用)。
        猜你喜欢
        • 1970-01-01
        • 2022-01-19
        • 1970-01-01
        • 2013-07-07
        • 2010-09-14
        • 1970-01-01
        • 2013-10-03
        • 1970-01-01
        相关资源
        最近更新 更多