mardi 7 juin 2022

Lifetime extension of temporary objects: what is the full expression containing a function call?

Introduction

Say there is a Container class which stores Widget objects. There is an iterator class in charge of navigating such container. This iterator class (MyIterator) takes a const-reference to a vector of Stuff in its constructor, which it needs to iterate over the right elements in the container. The code may look like this:

class MyIterator
{
public:
    MyIterator(
        const Container& container,    // Container to be navigated
        const std::vector<Stuff>& vec) // Parameter which controls the behaviour of the iteration
    : container(container)
    , stuffVector(vec)
    {}
    
    ...

private:
    const Container& container;
    const std::vector<Stuff>& stuffVector;
};

The problem

I'm curious about the binding of rvalues to const references, and how the lifetime extensions work.

While vec can be bound to a temporary rvalue, that will cause stuffVector to be a dangling reference once the rvalue's lifetime is over. So code like this, aimed to create an iterator for object myContainer is wrong:

MyIterator it = {myContainer, {stuff1, stuff2, stuff3}};
// Here it.stuffVector is a dangling reference!

Reading up on lifetime extension of temporary objects in cppreference I found:

Whenever a reference is bound to a temporary object or to a subobject thereof, the lifetime of the temporary object is extended to match the lifetime of the reference (...) There are following exceptions to this lifetime rule: (...)

  • a temporary bound to a reference parameter in a function call exists until the end of the full expression containing that function call: if the function returns a reference, which outlives the full expression, it becomes a dangling reference.

The question

The highlighted part leads me to this question: would this piece of code succeed in getting a filtered version of the contents of myContainer?

std::vector<Widget> filteredWidgets;
std::copy_if (
    MyIterator{myContainer, {stuff1, stuff2, stuff3}},
    MyContainer.end(),
    std::back_inserter(filteredWidgets),
    [&](Widget w) { return ...; });

The second argument is an rvalue, so there is the danger of creating a dangling reference, but my understanding of the documentation is that the rvalue's lifetime will be extended untill the end of std::copy_if, so it would be fine. Which one is it?

NOTE: I am aware the problem and the design, as stated, might seem a bit strange, but it's inspired in real code I've found in the wild. I'm interested in the limits of the lifetime extension, not so much in coming up with different designs which would make the question irrelevant.

Aucun commentaire:

Enregistrer un commentaire