简体   繁体   中英

Detecting unsafe const reference binding in C++

I've just spent quite a lot of time debugging an obscure memory corruption problem in one of my programs. It essentially boils down to a function which returns a structure by value being called in a way which passes it into an object constructor. Pseudocode follows.

extern SomeStructure someStructureGenerator();

class ObjectWhichUsesStructure {
  ObjectWhichUsesStructure(const SomeStructure& ref): s(ref) {}
  const SomeStructure& s;
}

ObjectWhichUsesStructure obj(someStructureGenerator());

My reasoning was: someStructureGenerator() is returning a temporary; this is being bound to a const reference, which means the compiler is extending the lifetime of the temporary to match the place of use; I'm using it to construct an object, so the temporary lifetime is being extended to match that of the object.

That last bit turns out not to be the case. Once the constructor exits, the compiler deletes the temporary and now obj contains a reference to hyperspace, with hilarious results when I try to use it. I need to explicitly bind the const reference to the scope, like this:

const auto& ref = someStructureGenerator();
ObjectWhichUsesStructure obj(ref);

That's not the bit I'm asking about.

What I'm asking about is this: my compiler is gcc 8, I build with -Wall , and it was perfectly happy to compile the code above --- cleanly, with no warnings. My program ran happily (but incorrectly) under valgrind, likewise with no warnings.

I have no idea how many other places in my code I'm using the same idiom. What compiler tooling will detect and flag these places so that I can fix my code, and alert me if I make the same mistake in the future?

First, reference binding does “extend lifetime” here—but only to the lifetime of the constructor parameter (which is no longer than that of the temporary materialized anyway). s(ref) isn't binding an object (since ref is, well, already a reference), so no further extension occurs.

It's therefore possible to perform the extension you expected via aggregate initialization :

struct ObjectWhichUsesStructure {
  const SomeStructure &s;
};

ObjectWhichUsesStructure obj{someStructureGenerator()};

Here there is no constructor parameter (because there is no constructor at all!) and so only the one, desired binding occurs.

It's worth seeing why the compiler doesn't warn about this: even if a constructor does retain a reference to a temporary argument, there are legitimate situations where that works:

void useWrapper(ObjectWhichUsesStructure);
void f() {useWrapper(someStructureGenerator());}

Here the SomeStructure lives until the end of the statement, during which time useWrapper can make profitable use of the reference in the ObjectWhichUsesStructure .

At the expense of forbidding the valid use cases above, you can have the compiler trap the problematic case by providing a deleted constructor taking an rvalue reference:

struct ObjectWhichUsesStructure {
  ObjectWhichUsesStructure(const SomeStructure& ref): s(ref) {}
  ObjectWhichUsesStructure(SomeStructure&&)=delete;
  const SomeStructure& s;
};

This might be worth doing temporarily as a diagnostic measure without having it be a permanent restriction.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM