【问题标题】:How to force a compile error in C++(17) if a function return value isn't checked? Ideally through the type system如果未检查函数返回值,如何在 C++(17) 中强制编译错误?理想情况下通过类型系统
【发布时间】:2021-07-21 01:02:17
【问题描述】:

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

[更新]

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

约束:

  • 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
}
  

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

【问题讨论】:

  • 如果检查返回值那么重要,为什么不抛出异常呢?
  • 虽然“鼓励”编译器在返回值被丢弃时使用[[nodiscard]] 发出警告,但大多数编译器可以配置为将警告视为错误并停止编译。 Microsoft 编译器和 IDE 支持这样的选项。
  • 另外,请记住,任何(尝试)强制调用者检查返回值的方法都可以被调用者明确规避。例如,如果编译器由于[[nodiscard]] 而发出警告/错误,调用者仍然可以通过简单的强制转换(即(void)DoSomething(bloop))击败编译器提交。有一点你必须依靠其他程序员阅读文档并做需要做的事情。
  • 如果一个警告变成了一个错误,那就是一个真正的错误,会导致构建失败。为什么这是个问题?这不是你想要的吗?
  • DoSomething 可以采用两个参数: lambda 调用成功, lambda 调用失败。这样,调用者负责显式处理成功和失败的情况(即使处理程序是空操作)。

标签: c++ static-analysis type-systems


【解决方案1】:

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

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

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

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

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


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

以下是原答案。


这并不能满足您的所有(已编辑)要求(我认为它们过分了),但我认为这是唯一的前进道路。 在我的 cmets 下面。

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

对于编译时,它更棘手,返回(例如 bool)和 [[nodiscard]] 是不够的,因为有一些方法可以在不检查的情况下不丢弃,例如分配给 (bool) 变量。

我认为下一层是激活-Wunused-variable -Wunused-expression -Wunused-parameter(并将其视为错误-Werror=...)。 那么不检查bool 就更难了,因为比较几乎只是你可以用bool 真正做的操作。 (您可以分配给另一个布尔值,但您必须使用该变量)。

我想这已经足够了。

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

我想避免像(void)b; 这样的事情是不可能的,但这本身就是一个标志。 即使您无法避免不检查,您也可以强制使用至少会立即引起注意的模式。

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

【讨论】:

  • C++ 旨在防止墨菲,而不是马基雅维利。
  • @Eljay,我完全同意,这就是为什么我几乎会停留在“我想这已经足够了”。后来发生的只是一个小游戏(它有一点奖励)。我认为通用库中有[[nodiscard]] class CheckedBool{...}; 的空间。
【解决方案2】:

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

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

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

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

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,但应该不会太难。

免责声明

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

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

【讨论】:

  • 一般来说,在 C++ 中证明任何“不可能”的事情都很难。例如,您可能会认为,如果您在编译时评估 constexpr 函数,两次使用完全相同的参数,应该总是给出相同的答案。很明显,对吧?然后你会看到这样的事情:stackoverflow.com/questions/60082260/…
【解决方案3】:

如 cmets 中所述,您可以按以下方式使用 [[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

这应该涵盖您的用例。

【讨论】:

猜你喜欢
  • 2020-02-11
  • 1970-01-01
  • 2021-10-21
  • 1970-01-01
  • 2011-12-26
  • 1970-01-01
  • 2013-01-16
  • 2011-08-13
  • 2021-02-21
相关资源
最近更新 更多