如果未检查函数返回值,如何在 C++(17) 中强制编译错误?理想情况下通过类型系统

问题描述

我们正在编写安全关键代码,我想要一种比 [[nodiscard]] 更强大的方法来确保检查函数返回值被编译器捕获。

[更新]

感谢评论中的所有讨论。让我澄清一下,这个问题可能看起来是人为的,或者不是“典型用例”,或者不是其他人会怎么做。如果这样可以更容易地忽略“为什么你不这样做呢?”,请将此作为学术练习。问题正是如果没有将其分配给左值作为函数调用的返回结果,是否有可能创建一个编译失败的类型。
我知道 [[nodiscard]]、warnings-as-errors 和异常,这个问题询问是否有可能实现类似的东西,即编译时间 错误 ,不是在运行时捕获的东西。我开始怀疑这是不可能的,因此非常感谢解释为什么。

约束:

  • MSVC++ 2019
  • 依赖警告的东西
  • Warnings-as-Errors 也不起作用
  • 持续运行静态分析是不可行的
  • 宏没问题
  • 不是运行时检查,而是被编译器捕获
  • 不是基于异常

我一直在想如何创建一个类型,如果它没有从函数返回值分配给一个变量,编译器就会标记一个错误

示例:

struct MustCheck
{
  bool success;
  ...???... 
};

MustCheck DoSomething( args )
{
  ...
  return MustCheck{true};
}

int main(void) {
  MustCheck res = DoSomething(blah);
  if( !res.success ) { exit(-1); }

  DoSomething( bloop ); // <------- compiler error
}
  

如果通过类型系统可以证明这样的事情是不可能的,我也会接受这个答案;)

解决方法

(EDIT) 注 1:我一直在思考你的问题,并得出结论,这个问题是不恰当的。由于一个小细节,不清楚您在寻找什么:什么算作检查?检查是如何组成的,离调用点还有多远?

例如,这算作检查吗?请注意,布尔值(结果)和/或其他运行时变量的组合很重要。

bool b = true; // for example
auto res1 = DoSomething1(blah);
auto res2 = DoSomething2(blah);

if((res1 and res2) or b){...handle error...};

与其他运行时变量的组合使得无法在编译时做出任何保证,并且对于与其他“结果”的组合,您必须排除某些逻辑运算符,例如 OR 或 XOR。


(EDIT) 注 2:我之前应该问过,但 1) 处理是否应该总是中止:为什么不直接从 DoSomething 函数中止? 2) 如果处理在失败时执行特定操作,则将其作为 lambda 传递给 DoSomething (毕竟您正在控制它返回的内容以及它需要的内容)。 3)故障或传播的组合是唯一不平凡的情况,并且在您的问题中没有明确定义。

以下是原始答案。


这并不能满足您的所有(编辑过的)要求(我认为它们是多余的),但我认为这确实是唯一的前进道路。 在我的评论下方。

正如您所暗示的,为了在运行时执行此操作,网上有关于“爆炸”类型的方法(如果它们未被检查,则在销毁时断言/中止,由内部标志跟踪)。 请注意,这不使用异常(但它是运行时并且如果您经常测试代码,这并不是那么糟糕,毕竟这是一个逻辑错误)。

对于编译时,它更棘手,用 bool 返回(例如一个 [[nodiscard]])是不够的,因为有不检查例如分配给 (bool) 就没有丢弃的方法变量。

我认为下一层是激活 -Wunused-variable -Wunused-expression -Wunused-parameter(并将其视为错误 -Werror=...)。 那么不检查 bool 就会困难得多,因为比较几乎与您真正可以使用 bool 进行的操作差不多。 (您可以分配给另一个布尔值,但随后您将不得不使用该变量)。

我想这已经足够了。

仍然有马基雅维利的方式来标记一个已使用的变量。 为此,您可以发明一个类似 bool 的类型(类),即 1) [[nodiscard]] 本身(类可以标记为 nodiscard),2)唯一支持的操作是 {{1} } 或 ==(bool) (甚至可能无法复制)并从您的函数中返回它。 (作为奖励,您无需将函数标记为 !=(bool),因为它是自动的。)

我想避免像 [[nodiscard]] 这样的东西是不可能的,但它本身就成了一个标志。 即使无法避免没有检查,您也可以强制使用至少会立即引起注意的图案。

您甚至可以结合运行时和编译时策略。 (使 CheckedBool 爆炸。) 这将涵盖很多情况,此时您必须感到高兴。 如果编译器标志不能保护你,你仍然有一个可以在单元测试中检测到的备份(不管采取错误路径!)。 (现在不要告诉我你不对关键代码进行单元测试。)

,

您想要的是 substructural types 的特例。 Rust 以实现一种称为“仿射”类型的特殊情况而闻名,您可以在其中“最多一次”“使用”某些东西。在这里,您需要“相关”类型,您必须至少使用一次。

C++ 没有对这些东西的官方内置支持。也许我们可以伪造它?我以为不是。在这个答案的“附录”中,我包含了我为什么这么认为的原始逻辑。同时,这是如何做到的。

(注意:我没有测试过这些;我多年没有写过任何 C++;使用风险自负。)

首先,我们在 protected 中创建一个 MustCheck 析构函数。因此,如果我们简单地忽略返回值,我们将得到一个错误。但是如果我们不忽略返回值,我们如何避免出错呢?像这样的东西。 (这看起来很可怕:别担心,我们将大部分内容封装在一个宏中。)

int main(){
    struct Temp123 : MustCheck {
        void f() {
            MustCheck* mc = this;
            *mc = DoSomething();
        }
    } res;
    res.f();
    if(!res.success) print "oops";
}

好吧,这看起来很糟糕,但是在定义了一个合适的宏之后,我们得到:

int main(){
    CAPTURE_RESULT(res,DoSomething());
    if(!res.success) print "oops";
}

我将宏作为练习留给读者,但它应该是可行的。您可能应该使用 __LINE__ 或其他东西来生成名称 Temp123,但应该不会太难。

免责声明

请注意,这是各种骇人听闻的和可怕的,您可能不想实际使用它。使用 [[nodiscard]] 的优点是允许您使用自然返回类型,而不是这种 MustCheck 东西。这意味着您可以创建一个函数,然后在一年后添加 nodiscard,您只需修复做错事的调用者。如果迁移到 MustCheck,则必须迁移所有调用者,即使是那些做正确事情的调用者。

这种方法的另一个问题是,如果没有宏,它是不可读的,但 IDE 不能很好地遵循宏。如果您真的很在意避免错误,那么如果您的 IDE 和其他静态分析器尽可能地理解您的代码,那真的很有帮助。

,

如评论中所述,您可以按照以下方式使用 [[nodiscard]]:

https://docs.microsoft.com/en-us/cpp/cpp/attributes?view=msvc-160

并修改为使用此警告作为编译错误:

https://docs.microsoft.com/en-us/cpp/preprocessor/warning?view=msvc-160

这应该涵盖您的用例。