C++|如何使用C++20解决一系列运行时的错误

C++|如何使用C++20解决一系列运行时的错误

文章图片

C++|如何使用C++20解决一系列运行时的错误

文章图片

C++|如何使用C++20解决一系列运行时的错误

文章图片


C++20已经发布了一阵子了 , 并已经被MSVC v16.11及以上版本所支持 。 今天的文章的主题 , 不是讲如何使用它 , 而是讲我们作为编译器开发人员 , 是如何借助C++20来有效地清除一些列运行时错误 。 不多啰嗦 , 直接上东西 。
最初的做法【C++|如何使用C++20解决一系列运行时的错误】在设计编译器时 , 你需要考虑的第一件事就是需要提醒开发者他的代码是否有错误或者潜在的非预期行为 。 在MSVC编译器的实现中 , 我们采用了如下图所示的错误模型:

上图中的error函数确实能正常工作 , 因为每个错误代码(ErrorNumber)都有一个对应的错误字符串 , 用来提示给开发人员 , 字符串的对应关系千差万别 , 可以是简单的如 C2056 -> “illegal expression” , 到复杂的如 C7627 -> “‘%1$T’: is not a valid template argument for ‘%2$S'” , 但是细心的朋友可能注意到了 , 错误字符串里的%1$T和%2$S代表什么意思呢?
这些是一些编译器特有的格式说明符 , 用来向开发者显示比较容易理解的一些特定的结构体类型 。
格式说明符是一把双刃剑我们作为编译器开发者 , 格式说明符确实提供了很大的灵活性和功能 。 格式说明符可以更清楚地说明错误触发的原因 , 并为用户提供有关问题的更多上下文信息 。 格式说明符的问题在于它们在调用上文中提到的error函数时没有进行类型检查 , 所以如果我们碰巧参数类型错误或根本没有传递参数 , 它几乎肯定会在稍后出现运行时错误 。当你想要将错误信息字符串重构为更清晰的内容时也会出现其他问题 , 但要做到这一点 , 你需要查询该错误消息的每个调用者 , 并确保这项重构与传递给错误的实际参数保持一致 。
在设计一个可以对格式说明符进行检查的系统时 , 我们有三个高层目标:
1. 验证在编译时传递到我们的诊断API的参数类型 , 以便尽早地发现错误 。
2. 尽量减少对诊断API调用者所做的更改 。 这是为了确保格式良好的调用保留其原始结构(也不会中断未来的调用) 。
3. 最小化对被调用者的实现细节所做的更改 。 我们不应该在运行时改变诊断程序的行为 。
当然 , 后来的C++标准引入了一些解决方案 , 可以帮助解决这个问题 。 一方面 , 一旦将可变参数模板引入语言中 , 我们可以尝试一些模板元编程来尝试对错误调用进行类型检查 , 但这需要单独地查找表 , 因为constexpr和模板的功能有限 。 C++14/17都对constexpr和非类型模板参数进行了很多改进 。 类似下图中的一种做法就比较有用:

因此 , 我们最终找到了一个工具用来在编译期检查格式说明符的正确性 。 但是还有一个问题没有解决:我们仍然没有办法静默地检查所有现有的错误调用 , 这意味着我们必须在错误调用点之间添加一个额外的间接层 , 以确保ErrorNumber可以在编译时获取字符串并检查对应的参数类型 。
在C++17中 , 下面的代码不会正常工作:

我们不能让error函数本身成为constexpr , 因为它做了很多对constexpr不友好的事情 。此外 , 将所有调用站点调整为以下内容:error(a b c) 以便我们可以将错误号作为编译时表达式进行检查 , 这是令人讨厌的 , 并且会在编译器中导致大量不必要的折腾 。