Temporary objects: lifetime and Extensions – part 2


This post is centered on “stack-use-after-scope” which is an issue we see often in code review and code sanitizer / valgrind reports. Here’s an example:

struct SimpleCalc{
    SimpleCalc( const std::vector<int>& my_vec ) : 
        m_vec (my_vec) {};

    int calculate() {
        // Uses the m_vec and its content.
    };
private:
    const std::vector<int>& m_vec;
};

// < some code >

// Solution #1
SimpleCalc my_calc( std::vector<int>{ /*some values*/ } ); // <- Problem here. 
// The temp created in the argument will have it's 
// lifetime end with this full expression.
my_calc.calculate(); // Stack-use-after-scope.


The first/simplest solution (let’s call it solution 1) that usually comes to mind is to create an lvalue and pass it to the constructor, as such:

std::vector<int>() container;
SimpleCalc my_calc( container ); 
int solution = my_calc.calculate(); // Scope is valid. 

But, can we do better and do away with the lvalue? Let’s explore solution #2.

int solution = SimpleCalc( std::vector<int>{ /*some values*/ } ).calculate();

Is this valid? Is the rvalue created in the argument’s lifetime valid until the end of calculate()? Let’s review what the standard says around this case (see part1 here):

A temporary object bound to a reference parameter in a function call (8.5.1.2) persists until the completion of the full-expression containing the call.” [class.temporary / 15.2.6.9 ].

The main thing to unpack here is “what is a full-expression?”. Does it include the constructor and the chained call to calculate()?

an expression that is not a subexpression of another expression and that is not otherwise part of a full-expression.
[intro.execution / 6.8.1.3.5.]


We then have nice and concise code, all in one line. But. And there is a but. What if the next developer decides he want it in 2 lines since he wants to use the same Calculator to do another calculation, say calculate2()? Oops, now were back to the same problem. What we gained in simplicity, we lost in maintainability. Back to the drawing board. This time, let’s aim to make our class easy to correctly, and hard to use incorrectly.


// Solution 3. Expect a rvalue in the constructor. 
struct SimpleObj{
    SimpleObj( std::vector<int>&& my_vec ) : m_vec (my_vec) {};

    void calculate() {
        // Uses the m_vec and its content.
    };
private:
    std::vector<int> m_vec; // No longer a const &
};


int solution = SimpleCalc( std::vector<int>{ /*some values*/ } ).calculate(); // Ok!

SimpleCalc my_calc( std::vector<int>{ /*some values*/ } )
int solution = my_calc.calculate(); // Ok!
int solution2 = my_calc.calculate2(); // Ok!

Conclusion:

As is often the case, there are easy solutions for a quickfix. If you have the luxury of being in the design phase, then this costs you nothing to design your class this way. This will make it easy to use correctly. In the most likely case, you’re finding this issue with code already in use and referenced in multiple places that may not make it easy to rewrite, as clearly this changes the signature of class constructor. Changing the signature is still often the right choice for long term maintainability.

Leave a Reply

Your email address will not be published. Required fields are marked *