Visual Studio 2019 Preview 2 is an exciting release for the C++ code analysis team. In this release, we shipped a new set of experimental rules that help you catch bugs in your codebase, namely: use-after-move and coroutine checks. This article provides an overview of the new rules and how you can enable them in your project.
Use-after-move check
C++11 introduced move semantics to help write performant code by replacing some expensive copy operations with cheaper move operations. With the new capabilities of the language, however, we have new ways to make mistakes. It’s important to have the tools to help find and fix these errors.
To understand what these errors are, let’s look at the following code example: MyType m; consume(std::move(m)); m.method();
MyType m; consume(std::move(m)); m.method();
Calling consume
will move the internal representation of m
. According to the standard, the move constructor must leave m
in a valid state so it can be safely destroyed. However, we can’t rely on what that state is. We shouldn’t call any methods on m
that have preconditions, but we can safely reassign m
, since the assignment operator does not have a precondition on the left-hand side. Therefore, the code above is likely to contain latent bugs. The use after move check is intended to find exactly such code, when we are using a moved-from object in a possibly unintended way.
There are several interesting things happening in the above example:
std::move
does not actually movem
. It’s only cast to a rvalue reference. The actual move happens inside the functionconsume
.- The analysis is not inter-procedural, so we will flag the code above even if
consume
is not actually movingm
. This is intentional, since we shouldn’t be using rvalue references when moving is not involved – it’s plain confusing. We recommend rewriting such code in a cleaner way. - The check is path sensitive, so it will follow the flow of execution and avoid warning on code like the one below.
Y y; if (condition) consume(std::move(y)); if (!condition) y.method();
In our analysis, we basically track what’s happening with the objects.
- If we reassign a moved-from object it is no longer moved from.
- Calling a clear function on a container will also cleanse the “moved-from”ness from the container.
- We even understand what “swap” does, and the code example below works as intended:
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.
Coroutine related checks
Coroutines are not standardized yet but they are well on track to become standard. They are the generalizations of procedures and provide us with a useful tool to deal with some concurrency related problems.
In C++, we need to think about the lifetimes of our objects. While this can be a challenging problem on its own, in concurrent programs, it becomes even harder.
The code example below is error prone. Can you spot the problem?
std::future async_coro(int &counter) { Data d = co_await get_data(); ++counter; }
This code is safe on its own, but it’s extremely easy to misuse. Let’s look at a potential caller of this function:
int c; async_coro(c);
The source of the problem is that async_coro
is suspended when get_data
is called. While it is suspended, the flow of control will return to the caller and the lifetime of the variable c
will end. By the time async_coro
is resumed the argument reference will point to dangling memory.
To solve this problem, we should either take the argument by value or allocate the integer on the heap and use a shared pointer so its lifetime will not end too early.
A slightly modified version of the code is safe, and we will not warn:
std::future async_coro(int &counter) { ++counter; Data d = co_await get_data(); }
Here, we’ll only use the counter
before suspending the coroutine. Therefore, there are no lifetime issues in this code. While we don’t warn for the above snippet, we recommend against writing clever code utilizing this behavior since it’s more prone to errors as the code evolves. One might introduce a new use of the argument after the coroutine was suspended.
Let’s look at a more involved example:
int x = 5; auto bad = [x]() -> std::future { co_await coroutine(); printf("%d\n", x); }; bad();
In the code above, we capture a variable by value. However, the closure object which contains the captured variable is allocated on the stack. When we call the lambda bad
, it will eventually be suspended. At that time, the control flow will return to the caller and the lifetime of captured x
will end. By the time the body of the lambda is resumed, the closure object is already gone. Usually, it’s error prone to use captures and coroutines together. We will warn for such usages.
Since coroutines are not part of the standard yet, the semantics of these examples might change in the future. However, the currently implemented version in both Clang and MSVC follows the model described above.
Finally, consider the following code:
generator mutex_acquiring_generator(std::mutex& m) { std::lock_guard grab(m); co_yield 1; }
In this snippet, we yield a value while holding a lock. Yielding a value will suspend the coroutine. We can’t be sure how long the coroutine will remain suspended. There’s a chance we will hold the lock for a very long time. To have good performance and avoid deadlocks, we want to keep our critical sections short. We will warn for the code above to help with potential concurrency related problems.
Enabling the new checks in the IDE
Now that we have talked about the new checks, it’s time to see them in action. The section below describes the step-by-step instructions for how to enable the new checks in your project for Preview 2 builds.
To enable these checks, we go through two basic steps. First, we select the appropriate ruleset and second, we run code analysis on our file/project.
Use after free
- Select: Project > Properties > Code Analysis > General > C++ Core Check Experimental Rules.
- Run code analysis on the source code by right clicking on File > Analyze > Run code analysis on file.
- Observe warning C26800 in the code snippet below:
Coroutine related checks
- Select: Project > Properties > Code Analysis > General > Concurrency Rules.
- Run code analysis on the source code by right clicking on File > Analyze > Run code analysis on file.
- Observe warning C26810 in the code snippet below:
- Observe warning C26811 in the code snippet below:
- Observe warning C26138 in the code snippet below:
Wrap Up
We’d love to hear from you about your experience of using these new checks in your codebase, and also for you to tell us what sorts of checks you’d like to see from us in the future releases of VS. If you have suggestions or problems with these checks — or any Visual Studio feature — either Report a Problem or post on Developer Community and let us know. We’re also on Twitter at @VisualC.
0 comments