It's a good decision to add at least some checks into C++ standard library. But no runtime check can find a bug in code like this:
std::vector<int> v;
v.push_back(123);
auto& n= v.front();
v.push_back(456);
auto n_doubled= n * 2;
A better language is needed in order to prevent such bugs, where such compile-time correctness checks are possible. Some static analyzers are able to detect it in C++, but only in some cases.Nirvana fallacy. Some checks are better than no checks.
It seems to me statically checking this should be possible. The liveness of the result of std::vector::front() should be invalidated and be considered dead after the second invocation to push_back(). Then a static analyser would correctly mark the final line with red squiggles. Of course, compilers would still be happy to compile this, which they really ought not to.
> It seems to me statically checking this should be possible.
Statically checking this specific example (or similarly simple examples) could be possible, sure. I'm not so sure about more complex cases, such as opaque functions (whether because the function is literally opaque or because not enough inlining occurred), stored references (e.g., std::span), unintentional mutation of the underlying data structure, etc.
Thats basically one of the main reason Rust's lifetimes exist - to explicitly encode information about when lifetimes are valid in the type system. C++ doesn't have an equivalent (yet?), so unless you're willing to use global analysis an/or non-standard annotations there's only so much static analysis can do.
I'd be surprised if some combination of ASAN and UBSAN wouldn't catch this and similar dangling references
I thought it should too, but it doesn't seem to, unless I made a mistake, which I probably did: https://godbolt.org/z/Ex63vxj4r
I think you need to actually use the dangling reference: https://godbolt.org/z/f4s3fT3nM
Ah, that would make sense, yes. Thanks!
What does this do?
Potential use-after-free. push_back() may reallocate, which would invalidate the reference returned by front(), rendering its subsequent use invalid.
Thank you!
In my own experience with “modern-ish C++” (the platform I work with only supports up to C++17 for now), once we started using smart pointers, like unique_ptr and shared_ptr, iterator invalidation has been the primary source of memory safety errors. You have to be so careful any time you have a reference into a container.
In a lot of cases the solution is already sitting there for you in <algorithms> though. One of the more common places we’ve encountered this problem is when someone has a simple task like “delete items from this vector that match some predicate” and then just write a for-loop that does that but doesn’t handle the fact that the iterators can go bad when you modify the vector. The algorithms library has functions in it to handle that, but without a good mental checklist of what’s all in there people will generally just do the simple (and unfortunately wrong) thing.
How much performance do you give up with smart pointers?
Not enough that we’ve ever noticed it being at all significant in the profiling we do regularly. The system is an Edge ML processor running about 600 megapixel/sec through a pipeline that does pre- and post-processing on multiple CPUs and does inference on the GPU using TensorRT. In general allocation isn’t anywhere near our bottleneck, it’s all of the image processing work that is.