January 24th, 2019

Lifetime Profile Update in Visual Studio 2019 Preview 2

Kyle Reed
Principal Software Engineer

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:

  1. Don’t use a potentially dangling pointer.
  2. Don’t pass a potentially dangling pointer to another function.
  3. 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.

Screenshot of the Code Analysis properties page that shows the C++ Core Check Lifetime Rules ruleset selected.
Screenshot of the Code Analysis properties page that shows the C++ Core Check Lifetime Rules ruleset selected.

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.

Screenshot showing a Lifetime Profile Checker warning with a green squiggle in source code.
Screenshot showing a Lifetime Profile Checker warning with a green squiggle in source code.

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 of std::vector::at is not const because it can return a non-const reference – however we know that calling it is lifetime-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.

Author

Kyle Reed
Principal Software Engineer

Joined Microsoft in 2007. Worked on SharePoint, Security, C++ Static Analysis, and C++ Language Services.

0 comments

Discussion are closed.