The C++ Core Guidelines’ Lifetime Profile, which is part of the C++ Core Guidelines, aims to detect lifetime problems, like dangling pointers and references, in C++ code. It uses the type information already present in the source along with some simple contracts between functions to detect defects at compile time with minimal annotation.
These are the basic contracts that the profile expects code to follow:
- Don’t use a potentially dangling pointer.
- Don’t pass a potentially dangling pointer to another function.
- Don’t return a potentially dangling pointer from any function.
For more information on the history and goals of the profile, check out Herb Sutter’s blog post about version 1.0.
What’s New in Visual Studio 2019 Preview 2
In Preview 2, we’ve shipped a preview release of the Lifetime Profile Checker which implements the published version of the Lifetime Profile. This checker is part of the C++ Core Checkers in Visual Studio.
- Support for iterators, string_views, and spans.
- Better detection of custom Owner and Pointer types which allows custom types that behave like Containers, Owning-Pointers, or Non-Owning Pointers to participate in the analysis.
- Type-aware default rules for function call pre and post conditions help reduce false-positives and improve accuracy.
- Better support for aggregate types.
- General correctness and performance improvements.
- Some simple nullptr analysis.
Enabling the Lifetime Profile Checker Rules
The checker rules are not enabled by default. If you want to try out the new rules, you’ll have to update the code analysis ruleset selected for your project. You can either select the “C++ Core Check Lifetime Rules” – which enables only the Lifetime Profile rules – or you can modify your existing ruleset to enable warnings 26486 through 26489.
Warnings will appear in the Error List when code analysis is run (Analyze > Run Code Analysis), or if you have Background Code Analysis enabled, lifetime errors will show up in the editor with green squiggles.
Examples
Dangling Pointer
The simplest example – using a dangling pointer – is the best place to start. Here px
points to x
and then x
leaves scope leaving px
dangling. When px
is used, a warning is issued.
void simple_test() { int* px; { int x = 0; px = &x; } *px = 1; // error, dangling pointer to 'x' }
Dangling Output Pointer
Returning dangling pointers is also not allowed. In this case, the parameter ppx
is presumed to be an output parameter. In this case, it’s set to point to x
which goes out of scope at the end of the function. This leaves *ppx
dangling.
void out_parameter(int x, int** ppx) // *ppx points to 'x' which is invalid { *ppx = &x; }
Dangling String View
The last two examples were obvious, but temporary instances can introduce subtle bugs. Can you find the bug in the following code?
std::string get_string(); void dangling_string_view() { std::string_view sv = get_string(); auto c = sv.at(0); }
In this case, the string view sv
is constructed with the temporary string instance returned from get_string()
. The temporary string is then destroyed which leaves the string view referencing an invalid object.
Dangling Iterator
Another hard to spot lifetime issue happens when using an invalidated iterator into a container. In the case below, the call to push_back
may cause the vector to reallocate its underlying storage which invalidates the iterator it
.
void dangling_iterator() { std::vector<int> v = { 1, 2, 3 }; auto it = v.begin(); *it = 0; // ok, iterator is valid v.push_back(4); *it = 0; // error, using an invalid iterator }
One thing to note about this example is that there is no special handling for ‘std::vector::push_back’. This behavior falls out of the default profile rules. One rule classifies containers as an ‘Owner’. Then, when a non-const method is called on the Owner, its owned memory is assumed invalidated and iterators that point at the owned memory are also considered invalid.
Modified Owner
The profile is prescriptive in its guidance. It expects your that code uses the type system idiomatically when defining function parameters. In this next example, std::unique_ptr
, an ‘Owner’ type, is passed to another function by non-const reference. According to the rules of the profile, Owners that are passed by non-const reference are assumed to be modified by the callee.
void use_unique_ptr(std::unique_ptr<int>& upRef); void assumes_modification() { auto unique = std::make_unique<int>(0); // Line A auto ptr = unique.get(); *ptr = 10; // ok, ptr is valid use_unique_ptr(unique); *ptr = 10; // error, dangling pointer to the memory held by 'unique' at Line A }
In this example, we get a raw pointer, ptr
, to the memory owned by unique
. Then unique
is passed to the function use_unique_ptr
by non-const reference. Because this is a non-const use of unique
where the function could do anything, the analysis assumes that unique
‘ is invalidated somehow (e.g. unique_ptr::reset) which would cause ptr
to dangle.
More Examples
There are many other cases that the analysis can detect. Try it out in Visual Studio on your own code and see what you find. Also check out Herb’s blog for more examples and, if you’re curious, read through the Lifetime Profile paper.
Known Issues
The current implementation doesn’t fully support the analysis as described in the Lifetime Profile paper. Here are the broad categories that are not implemented in this release.
- Annotations – The paper introduces annotations (i.e.
[[gsl::lifetime-const]]
) which are not supported. Practically this means that if the default analysis rules aren’t working for your code, there’s not much you can do other than suppressing false positives. - Exceptions – Exception handling paths, including the contents of
catch
blocks, are not currently analyzed. - Default Rules for STL Types – In lieu of a
lifetime-const
annotation, the paper recommends that for the rare STL container member functions where we want to override the defaults, we treat them as if they were annotated. For example, one overload ofstd::vector::at
is notconst
because it can return a non-const reference – however we know that calling it islifetime-const
because it doesn’t invalidate the vector’s memory. We haven’t completed the work to do this implicit annotation of all the STL container types. - Lambda Captures – If a stack variable is captured by reference in a lambda, we don’t currently detect if the lambda leaves the scope of the captured variable.
auto lambda_test() { int x; auto captures_x = [&x] { return x; }; return captures_x; // returns a dangling reference to 'x' }
Wrap Up
Try out the Lifetime Profile Checker in Visual Studio 2019 Preview 2. We hope that it will help identify lifetime problems in your projects. If you find false positives or false negatives, please report them so we can prioritize the scenarios that are important to you. If you have suggestions or problems with this check — 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