构造函数如何向调用方发出信号,它抛出的异常是否致命?

How can a constructor signal to caller, whether an exception it is throwing is fatal?

提问人:Mikhail T. 提问时间:8/25/2023 最后编辑:Jarod42Mikhail T. 更新时间:8/26/2023 访问量:91

问:

我正在处理一个应用程序,它需要读取 10+ CSV 文件(不同类型的)作为输入。数据被读入容器 -- 或 .std::mapvector

以前,每种类型都有自己的解析函数,我正在努力将其统一为一个模板化函数:以节省未来的代码维护,并为损坏的文件提供统一的错误报告。

此函数读取每一行,辨别容器类型是否具有 (like ) 的概念,并用于这些和 (like )。keymapemplaceemplace_backvector

容器值类的唯一期望是其构造函数可以从 CSV 行实例化。构造函数的任何异常都是致命错误 -- 输入文件名和行号被报告,程序退出:

try {
        if constexpr (is_associative_container<Container>(NULL)) {
                result->emplace(typename Container::key_type(
                    key, keylen), value);
        } else {
                result->emplace_back(value);
        }
} catch (const std::exception &e) {
        fprintf(stderr, "%s:%zd: %s\n", path, line, e.what());
        goto failure;
}

这一切都有效,我很高兴 - 大约75%的CSV解析现在由这个函数完成。

我现在面对的是剩下的四分之一的 CSV 文件,它们不那么简单:因为其中的某些行需要特殊处理,并且它们的内容不应该存储在容器中。

的构造函数如何向函数发出信号,即它抛出的异常不应被视为致命?一种方法是选择一个标准异常 (?) 作为信号,但这意味着,选择的异常不能意外发生——这是不可靠的......value_typestd::bad_function_call

别的东西?

C++ 模板 C++17 IF-constExpr

评论

0赞 user17732522 8/25/2023
throw(...)已从 C++ 中删除,并带有 C++17(C++ 为 20)。不再有类型化异常规范。函数可以潜在地将任何类型作为异常抛出,也可以根本不能引发。throw()
1赞 273K 8/26/2023
您可以将未列出的例外转换为自定义例外。如果函数引发其异常规范中未列出的类型的异常,则调用该函数。默认函数调用 ,但它可能会被用户提供的函数 (via ) 替换,该函数可能会调用或抛出异常。std::unexpectedstd::terminatestd::set_unexpectedstd::terminate
2赞 Eljay 8/26/2023
struct non_fatal_error : std::except { };和。使用适当的 .struct fatal_error {};throw
1赞 463035818_is_not_an_ai 8/26/2023
我真的不明白动机。当构造函数抛出时,它不会构造对象。您的意思是此对象的构造可能会失败,但您想继续解析 csv 的其余部分?(目前有一个例外使您跳过其余部分)
1赞 463035818_is_not_an_ai 8/26/2023
我们开始:“行解析是特定于每个value_type的——因此应该发生在该类的上下文中,无论它是什么。 将”行解析“替换为”错误处理和是否插入的决定“,你就得到了我刚才建议的解决方案。

答:

2赞 463035818_is_not_an_ai 8/26/2023 #1

问题编辑与编写此答案重叠。原始答案如下。

value_type的构造函数如何向函数发出信号,表明它抛出的异常不应被视为致命?

请注意,在当前代码中,您已经区分了继承自的异常和不继承自 的异常。好的风格是继承所有的例外,但现实通常是不同的。std::exceptionstd::exceptionstd::exception

您可以引入一些特殊类型的异常来抛出:

            try {
                 //...
            } catch (const non_fatal_exception &e) {
                    // do something
            } catch (...) { // all other exceptions are "fatal"
                    fprintf(stderr, "%s:%zd: %s\n", path, line, e.what());
                    goto failure;
            }

关于动态异常规范的旧答案...

如注释中所述,动态异常规范已从 C++17 中删除。

在 C++17 之前,一个确实抛出异常规范中未列出的异常的函数执行了以下操作(来自 cppreference):

如果函数引发其异常规范中未列出的类型的异常,则调用该函数。默认函数调用 ,但它可能会被用户提供的函数 (via ) 替换,该函数可能会调用或抛出异常。如果异常规范接受抛出的异常,则堆栈展开将照常继续。如果不是,但异常规范允许,则引发。否则,将调用。std::unexpectedstd::terminatestd::set_unexpectedstd::terminatestd::unexpectedstd::bad_exceptionstd::bad_exceptionstd::terminate

除非您知道异常规范中可以接受的异常,否则没有出路,但通常您不知道这一点。我不知道在通用代码中推导出“允许”的异常。无论如何,该功能已被删除。从某种意义上说,抛出异常规范中未列出的异常已经是“致命的”,这不是您必须额外做的事情。

评论

0赞 Mikhail T. 8/26/2023
在这种情况下没有 - 因此,没有 - 在这种情况下。但是,是的,这似乎是一般方法。类型可以很简单:-)ee.what()catch (...)non_fatal_exceptionconst char *
0赞 Mooing Duck 8/26/2023 #2

如果行不应进入容器是正常的,那么这不是一个异常流,你不应该使用异常。该函数应采用另一个参数:Function<bool(RowData)> shouldInsertIntoContainer

评论

0赞 Mikhail T. 8/26/2023
这些特殊的行仍然需要解析 -- 例如,修改容器的 .它们不能导致新对象存储在容器中。value_type
0赞 Mooing Duck 8/26/2023
@MikhailT.:没问题。无论如何,您都需要解析它,以便首先将其传递给它。shouldInsertIntoContainer
1赞 Mooing Duck 8/26/2023
@MikhailT.:将它们移动到容器中,而不是复制。
1赞 Mooing Duck 8/26/2023
@MikhailT。如果您要移动普通的旧数据,那么什么都不需要释放。如果它不是普通的旧数据,那么你可以只移动几个指针,仍然不能释放任何东西。你的评论让我感到困惑。
1赞 Mooing Duck 8/26/2023
@MikhailT.:如果它是动态作用域的,那么“移动”只是一个指针写入。你说得对,它不是免费的,但它非常接近它。
0赞 Mikhail T. 8/26/2023 #3

好的,根据我自己的倾向——以及 @Eljay(评论)和 @463035818_is_not_an_ai(接受的答案)的建议,我这样修改了代码:

        try {
            if constexpr (is_associative_container<Container>(NULL)) {
                result->emplace(typename Container::key_type(
                    key, keylen), value);
            } else {
                result->emplace_back(value);
            }
        } catch (const std::string &message) {
            /*
             * Allow constructors to fail without aborting the
             * parsing of the file by throwing an std::string.
             * If the string is not empty, output it to stderr.
             */
            if (!message.empty())
                fprintf(stderr, "%s:%zd: %s\n", path, line,
                    message.c_str());
            continue;
        } catch (const std::exception &e) {
            fprintf(stderr, "%s:%zd: %s\n", path, line, e.what());
            goto failure;
        } catch (...) {
            fprintf(stderr, "%s:%zd: internal error while "
                "emplacing into %s\n",
                path, line, typeid(Container).name());
            goto failure;
        }

评论

1赞 Ben Voigt 8/26/2023
您不应为此使用任何现有类(包括 )。唯一能保证某个特定类不会被库代码抛出的,就是它是否是你自己定义的类。让你自己的类继承,你会得到便宜,并让解析器使用你的特定类来处理可恢复的故障。std::stringstd::exceptione.what()
0赞 Mikhail T. 8/26/2023
谢谢,@BenVoigt,但这是一个小的 - 表面 - 点。我们的类可能会抛出自己的异常(全部派生自 std::exception)——或者由标准库代码抛出的异常,这些异常也是正确的。有朝一日,一些代码意外抛出的可能性很小,但这并不超过要求这些类声明特殊异常类的不便......std::string#include
0赞 Wutz 8/26/2023
@MikhailT。我同意冲突的可能性不大,但我认为由于可读性,您仍然应该创建自己的异常类。如果我看到有什么东西抛出一个字符串,它会让我感到惊讶,我必须深入研究代码才能找出它的含义,因为该类型没有给我任何信息。如果你抛出一个实例,比如说,它会立即变得清晰和令人难忘。此外,虽然有例外 (har har),但通常只抛出 std::exception 派生的东西是一种很好的做法。RecoverableParsingError
0赞 Mikhail T. 8/26/2023
还有奥卡姆和他著名的剃刀......