【问题标题】:Correct way to initialize an object with exception throwing constructor使用抛出异常的构造函数初始化对象的正确方法
【发布时间】:2009-06-30 15:13:31
【问题描述】:

这似乎是一个微不足道的问题,但我已经坚持了几个小时(也许太多的 Java 杀死了我的 C++ 脑细胞)。

我创建了一个具有以下构造函数的类(即没有默认构造函数)

VACaptureSource::VACaptureSource( std::string inputType, std::string inputLocation ) {
    if( type == "" || location == "" ) {
    throw std::invalid_argument("Empty type or location in VACaptureSource()");
}
type = inputType;
location = inputLocation;

// Open the given media source using the appropriate OpenCV function.
if( type.compare("image") ) {
    frame = cvLoadImage( location.c_str() );
    if( !frame ) {
        throw std::runtime_error("error opening file");
    }
}
else {
    throw std::invalid_argument("Unknown input type in VACaptureSource()");
}

}

当我想创建一个实例时,我使用

    // Create input data object
try {
    VACaptureSource input = VACaptureSource("image", "/home/cuneyt/workspace/testmedia/face_images/jhumpa_1.jpg");
}
catch( invalid_argument& ia ) {
    cerr << "FD Error: " << ia.what() << endl;
    usage(argv[0]);
}
catch( runtime_error& re ) {
    cerr << "FD Error: " << re.what() << endl;
    usage(argv[0]);
}

但是,在这种情况下,实例是该块的本地实例,我无法在其他任何地方引用它。另一方面,我不能说

VACAptureSource input;

在程序的开头,因为没有默认构造函数。

这样做的正确方法是什么?

谢谢!

【问题讨论】:

  • 你会如何在 Java 中解决这个问题?应用相同的解决方案。这个问题与语言无关。不同的语言只是为了相同的目的提供不同的语法,考虑到当你在 Java 中定义一个变量时,你不是在声明一个对象,而是一个 reference(C++ 术语中的指针),所以 Java 和 C++ 中的代码看起来相似不是真正的等效代码。

标签: c++ exception constructor


【解决方案1】:

为什么需要在try 块之外引用它?

代替

try {
  VACaptureSource input = VACaptureSource("image", "/home/cuneyt/workspace/testmedia/face_images/jhumpa_1.jpg");
}
//catch....

//do stuff with input

您可以将所有内容移到 try 块中:

try {
  VACaptureSource input = VACaptureSource("image", "/home/cuneyt/workspace/testmedia/face_images/jhumpa_1.jpg");
  //do stuff with input
}
//catch....

或者你可以把它分解成一个单独的函数,从 try 块中调用:

void doStuff(VACaptureSource& input){
  //do stuff with input
}

try {
  VACaptureSource input = VACaptureSource("image", "/home/cuneyt/workspace/testmedia/face_images/jhumpa_1.jpg");
  doStuff(input);
}
//catch....

最后一个甚至为您提供了将构造与使用分离的好处,它很好地放置在单元测试中,您可能希望函数在模拟对象上工作。

【讨论】:

  • 您提供的另一个很好的答案,包括几个替代方案。我喜欢这些 :) 但为了公平起见并给出可能的理由,我也经常希望“尝试”块中的那些东西“泄露”到封闭范围中。但这可能很危险,请考虑以下代码:try { string a; /* / 字符串 b; } 捕捉(...) { } b.size(); / 糟糕,b 尚未构造! */
  • 是的,我经常想做同样的事情。有点,因为没有明显的方法可以做到这一点,我们必须接受解决方法。
  • 也有可能想要从构造函数中捕获 std::invalid_argument 但让所有异常从其余代码传播。考虑一种获取可能抛出的值的方法,但是在这段特定的代码中,您有一个默认值,以防操作失败。稍后您想将该值与另一个也可以抛出相同异常的方法一起使用,但在这种情况下您没有明智的方法来继续您的流程。您想捕获第一个但不是后面的异常。
【解决方案2】:

如何使用指针(或其某些 RAII 版本)?

VACaptureSource* input = NULL;

try {
    input = new VACaptureSource(...);
} catch(...) {
    //error handling
}

//And, of course, at the end of the program
delete input;

【讨论】:

  • 谢谢,我考虑过这一点,但认为指针在 C++ 中是“邪恶的”,即仅作为最后的手段使用?除了使用指针没有别的办法了吗?
  • “邪恶”是一个非常强烈的词。然而,正如 CAdaker 所建议的,RAII(即自动销毁)变体,例如 std::auto_ptr - 或任何适当的 Boost 自动指针 - 将是一个明智的选择。
  • 顺便说一下,您可能只想使用有效的实例,这意味着您可能希望在完成需要完成的操作后使用 catch 块重新抛出。
  • 这似乎很愚蠢。当普通变量可以工作时,为什么要使用指针!!!!
  • @Martin:当自动变量不起作用时,为什么不使用指针? (这一次我不同意你)。在您的答案中查看更长的评论。
【解决方案3】:

局部变量的作用域仅限于分配它的块(如 Java),但它会在块结束后立即销毁(与 Java 不同),因此您应该在 try 块本身中执行所有您想要的操作(如果您只想处理构造函数异常,这可能是不可取的)或者您应该将对象分配到其他地方(例如堆)并使用父块中的指针来访问它。

【讨论】:

    【解决方案4】:

    我可以观察到 any 但最简单的构造函数可能会引发异常。因此,您不应将异常视为某种意义上的“特殊”,而应编写代码使其自然地处理它们。这意味着使用 RAII,以及此处其他答案建议的其他技术。

    【讨论】:

    • 不是一个答案,而是在一般异常处理问题中推理的一个好点。
    【解决方案5】:

    你可以使用指针

    VACaptureSource* input;
    // Create input data object
    try {
        input = new VACaptureSource("image", "/home/cuneyt/workspace/testmedia/face_images/jhumpa_1.jpg");
    }
    catch( invalid_argument& ia ) {
        cerr << "FD Error: " << ia.what() << endl;
        usage(argv[0]);
    }
    catch( runtime_error& re ) {
        cerr << "FD Error: " << re.what() << endl;
        usage(argv[0]);
    }
    

    并且你需要在使用完对象后释放它

    delete input
    

    【讨论】:

      【解决方案6】:

      我不能说

      VACaptureSource input;
      

      在程序的开头,因为没有默认构造函数。

      您没有创建默认构造函数是有充分理由的:即 VACaptureSource 仅在与文件关联时才有意义。所以不要创建默认构造函数。而是简单地认识到VACaptureSource 对象的范围是try 块,并在那里使用它。

      【讨论】:

        【解决方案7】:

        如果添加一个默认构造函数使对象处于特殊的未配置状态呢?然后有一个 create() 函数来实际创建它。

        那么你可以这样做:

        VACaptureSource input;
        try
        {
           input.create("image", "...");
        }
        catch(...)
        {
           ...
        }
        

        视情况而定,这可能比乱用指针更好。虽然那时你还必须检查 create() 是否在做某事之前被实际调用......

        【讨论】:

        • 这很像 Java。在 C++ 中,您尝试在构造函数中将对象置于可用状态。这样做,您需要维护有关已创建对象的天气的状态信息,并且此状态信息将过滤到所有其他方法。这使得代码更难看(更臭)。
        • 不要。对象要么被构造,要么未被构造。中间没有。抛出构造函数意味着对象在该状态下不可用,因此明智的做法是不让这样的对象处于活动状态。如果你这样做了,你将开始添加带有明显正确对象的代码流,这些对象不是你必须验证和测试的,并且会有更多的地方可以犯错误,并且在某一时刻,无论你有多优秀会犯一个错误,它会发生在远离创建点的地方,您将度过一个愉快的后期调试之夜。
        • @Martin York:不,它不像 Java。类似 Java 的定义是初始化为 null/NULL/0 的指针,然后使用 new 在堆中分配。 Java 不是无指针语言,只是无指针算术语言,其中指针(按引用的名称)在每一行代码中爬升。
        • @dribeas:在构建后拥有实际上不可用的对象并不少见。例如 std::ofstream 可以通过在构造后调用 open() 以这种方式工作。我确实认为该对象在构造后应该有点可用,并且使用该对象的代码不应填充检查该对象是否正常。我认为这取决于对象是否可用。
        【解决方案8】:

        我在这里实际上没有看到任何问题:

        我要更新的几件事:

        • 通过 const 引用捕获异常。
        • 编译器可能会优化掉代码中的复制构造
          但是没有它看起来更整洁。只需声明输入及其参数即可。
        • 我将重构构造函数以获取 const 引用参数
          我会在初始化列表中初始化它们。
        • 我还要确保成员“框架”实际上是一个智能指针。

        所以我会这样做(为了清楚起见)。

        VACaptureSource::VACaptureSource( std::string const& inputType,
                                          std::string const& inputLocation )
              :type(inputType)
              ,location(inputLocation)
        {
            // Other Code that throws.
        }
        void playWithCode()
        {
            // Get input information from user.
            VACaptureSource input("image", "/home/cuneyt/workspace/testmedia/face_images/jhumpa_1.jpg");
        
            // use the input object.
            // Do not need to play with pointers here.
        }
        int main()
        {
            try
            {
                playWithCode();
            }
            catch( invalid_argument const& ia )
            {    cerr << "FD Error: " << ia.what() << endl;
                 usage(argv[0]);
            }
            catch( runtime_error const& re )
            {    cerr << "FD Error: " << re.what() << endl;
                 usage(argv[0]);
            }
        }
        

        【讨论】:

        • 这里的问题是playWithCode中所有的invalid_argument异常都被同时捕获,语义相同。在“用户输入对象”中,另一个操作可能会引发您希望以不同方式处理的无效参数。
        • 很抱歉,但我无法确定这一点。
        • 假设您想创建一个 VACaptureSource,但如果失败,您可以求助于另一个 CaptureSource,稍后您想在可能抛出 std::runtime_error 的调用中使用它,但您无法从那里的失败。您可能想尝试创建 VACaptureSource,捕获异常并继续,但如果稍后在“playWithCode()”方法上抛出 std::runtime_error,请让其向上传播。
        【解决方案9】:

        简单。不要在构造函数中抛出异常。您不仅必须将构造函数包装在 try 块中,而且在捕获异常时将无法很好地处理内存(您是否调用析构函数?需要删除多少类的内存? )

        UPDATE0:不过,如果您使用实例,我不确定内存管理是否存在问题。

        UPDATE1:嗯,也许我在考虑析构函数中的异常。

        int
        main2(int argc, char* argv[])
        {
          MyClass class;
          class.doSomething();
        }
        
        int
        main(int argc, char* argv[])
        {
          int result = 0;
            try {
              main2(argc, argv);
            } catch (std::exception& se) {
              // oh noes!
               result = 1;
            }
          return result;
        }
        

        【讨论】:

        • C++ 有一条关于从构造函数中抛出异常的规则:在异常之前创建的任何子对象都会被正确销毁,但构造函数失败的对象从未创建过,也不会被销毁。并且为对象分配的所有内存都被释放。
        • @Max Lybbert - 谢谢。我之前对构造函数应用了“从不在析构函数中抛出异常”规则,也许我现在会记得了。
        猜你喜欢
        • 2013-07-07
        • 2011-02-09
        • 1970-01-01
        • 2012-04-22
        • 1970-01-01
        • 1970-01-01
        • 2021-10-31
        • 1970-01-01
        • 2023-04-04
        相关资源
        最近更新 更多