Visual Studio 2019预览2 是C++代码分析团队的一个令人兴奋的版本。在这个版本中,我们提供了一组新的实验性规则来帮助您捕获代码库中的bug,即:移动后使用和协同程序检查。本文概述了新规则以及如何在项目中启用它们。
移动后使用检查
C++ 11引入了移动语义,通过替换一些昂贵的复制操作和更便宜的移动操作来帮助编写性能代码。然而,随着语言的新能力,我们有了犯错误的新方法。重要的是要有工具来帮助发现和修复这些错误。
为了理解这些错误是什么,让我们看一下下面的代码示例:MyType m ; 消费 ( 标准 :: 移动 ( 米 ) ) ; 米 . 方法 ( ) ;
MyType m; consume(std::move(m)); m.method();
打电话 consume
将移动 m
. 根据标准,移动构造器必须离开 m
在一个有效的状态,所以它可以安全地销毁。然而,我们不能依赖这种状态。我们不应该调用任何方法 m
有先决条件,但我们可以安全地重新分配 m
,因为赋值运算符在左侧没有前置条件。因此,上面的代码可能包含潜在的bug。当我们以一种可能非故意的方式使用moved-from对象时,use-after-move检查旨在准确地找到这样的代码。
在上面的例子中发生了一些有趣的事情:
-
std::move
不会移动m
. 它只能转换为右值引用。实际移动发生在函数内部consume
. - 分析不是过程间的,因此我们将标记上面的代码,即使
consume
实际上并没有移动m
. 这是有意的,因为我们不应该在不涉及移动的情况下使用r值引用—这很容易混淆。我们建议以更干净的方式重写这样的代码。 - 该检查是路径敏感的,因此它将遵循执行流程,并避免对以下代码发出警告。
Y y; if (condition) consume(std::move(y)); if (!condition) y.method();
在我们的分析中,我们基本上是跟踪物体发生了什么。
- 如果我们重新分配一个从移动的对象,它将不再从移动。
- 对容器调用clear函数也会清除容器中的“moved from”。
- 我们甚至可以理解“交换”的作用,下面的代码示例按预期工作:
Y y1, y2; consume(std::move(y1)); std::swap(y1, y2); y1.method(); // No warning, this is a valid object due to the swap above. y2.method(); // Warning, y2 is moved-from.
协同程序相关检查
协同程序 它们还没有标准化,但正朝着标准化的方向发展。它们是过程的概括,为我们提供了处理一些并发相关问题的有用工具。
在C++中,我们需要思考对象的寿命。尽管这本身是一个具有挑战性的问题,但在并发程序中,它变得更加困难。
下面的代码示例容易出错。你能发现问题吗?
std::future async_coro(int &counter) { Data d = co_await get_data(); ++counter; }
此代码本身是安全的,但极易被误用。让我们看看这个函数的一个潜在调用方:
int c; async_coro(c);
问题的根源是 async_coro
暂停的时间 get_data
被称为。当它被挂起时,控制流将返回到调用者和变量的生存期 c
会结束的。到…的时候 async_coro
参数引用将指向悬挂内存。
要解决这个问题,我们要么按值获取参数,要么在堆上分配整数并使用共享指针,这样它的生存期就不会过早结束。
稍加修改的代码版本是安全的,我们不会发出警告:
std::future async_coro(int &counter) { ++counter; Data d = co_await get_data(); }
在这里,我们只使用 counter
在暂停协同进程之前。因此,此代码中没有生存期问题。虽然我们不会对上述代码段发出警告,但我们建议不要使用这种行为编写聪明的代码,因为随着代码的发展,这种行为更容易出错。在coroutine被暂停之后,可能会引入一个新的参数用法。
让我们看一个更复杂的例子:
int x = 5; auto bad = [x]() -> std::future { co_await coroutine(); printf("%d", x); }; bad();
在上面的代码中,我们通过值捕获变量。但是,包含捕获变量的闭包对象是在堆栈上分配的。当我们叫lambda的时候 bad
,最终将被暂停。此时,控制流将返回到调用者,并且捕获的生命周期 x
会结束的。当lambda的主体恢复时,闭包对象已经消失了。通常,将捕获和协同路由一起使用容易出错。我们将警告这种用法。
由于协同程序还不是标准的一部分,这些示例的语义将来可能会发生变化。但是,当前在Clang和MSVC中实现的版本都遵循上述模型。
最后,考虑以下代码:
generator mutex_acquiring_generator(std::mutex& m) { std::lock_guard grab(m); co_yield 1; }
在这个代码段中,我们在持有锁时生成一个值。产生一个值将挂起协同程序。我们不能确定联队将暂停多久。我们有可能把锁锁保持很长时间。为了获得良好的性能并避免死锁,我们希望关键部分保持简短。我们将警告上面的代码,以帮助解决潜在的并发相关问题。
在IDE中启用新检查
既然我们已经讨论了新的支票,现在是时候看看他们的实际行动了。下面一节介绍如何在项目中为Preview 2生成启用新检查的分步说明。
要启用这些检查,我们需要执行两个基本步骤。首先,我们选择适当的规则集,然后,我们对文件/项目运行代码分析。
免费后使用
协同程序相关检查
- 选择:项目>属性>代码分析>常规>并发规则。
- 右键单击文件>分析>对文件运行代码分析,对源代码运行代码分析。
- 注意下面代码段中的警告C26810:
- 请注意下面代码段中的警告C26811:
- 注意下面代码段中的警告C26138:
总结
我们很高兴听到您在代码库中使用这些新检查的经验,并希望您告诉我们您希望在VS的未来版本中看到哪些检查。如果您对这些检查或任何Visual Studio功能有任何建议或问题 报告问题 或张贴在 开发者社区 告诉我们。我们还在推特上 @视觉 .