Search code examples
c++constructortype-conversionlifetime

Temporary object lifetime issue while creating a span from a vector


Let's consider the following proxy struct that merely keeps track of a reference:

template<typename T>
struct proxy
{
    template<typename U> proxy  (const U& u) : t_(u) {}
    auto operator*() const { return t_; }
    const T& t_;
};

Now, we can use proxy in the bar function (here on godbolt):

#include <vector>
#include <span>
#include <numeric>
#include <iostream>

template<typename T>
struct proxy
{
    template<typename U> proxy  (const U& u) : t_(u) {}
    auto operator*() const { return t_; }
    const T& t_;
};

auto bar (proxy<std::span<const int>> p)
{
    // The computation doesn't matter here -> just an example of use of the span contents.
    return std::accumulate ((*p).begin(), (*p).end(), 0);
}

int main()
{
    std::vector<int> v {1,2,3};

    // EXAMPLE 1:
    // The following works well (no error from valgrind).
    std::cout << bar (std::span<const int> {v}) << std::endl;

    // EXAMPLE 2:
    // clang++ -> error: reference member 't_' binds to a temporary object whose lifetime
    // would be shorter than the lifetime of the constructed object
    std::cout << bar (v) << std::endl;
}

In example 1, a call to bar with a std::span created on the fly from an existing std::vector works perfectly.

On the other hand, when I try in example 2 to directly pass the std::vector to the function:

  • clang++ doesn't compile: error: reference member 't_' binds to a temporary object whose lifetime would be shorter than the lifetime of the constructed object
  • g++ compiles, the result seems to be ok but valgrind is not happy because of use of uninitialized values

Question 1: why does clang++ accept example 1 and not example 2 ?

Question 2: what is the flaw in the proxy struct that makes example 2 fail ?

Question 3: it is quite annoying/worrying that g++ (12.3.1) compiles but generates a binary with UB while clang++ (12) seems smarter here by generating an error. Should one consider this is a bug in g++ ?


Solution

  • GCC does not implement CWG1696. This prohibits const reference members binding to temporaries in a mem-initializer.

    This is prohibited because the lifetime of the temporary would end when the constructor finishes.

    Consider what is called when you pass a std::vector<int>:

    template
    struct proxy<std::span<const int>> {
        template
        proxy<std::vector<int>>(const std::vector<int>& u) : t_(u) {}
        auto operator*() const { return t_; }
        const std::span<const int>& t_;
    };
    

    When t_ is bound to u, it would construct a temporary std::span<const int> and bind it to the const reference (which is what const references do). This temporary's lifetime would end after returning from the constructor, so now it would be a dangling reference.

    That means that operator* would copy from a dangling reference, so it would be UB.

    If you instead make a constructor taking a const T&:

    template<typename T>
    struct proxy
    {
        proxy  (const T& u) : t_(u) {}
        auto operator*() const { return t_; }
        const T& t_;
    };
    

    Then the temporary std::span<const int> would be created in main and be destroyed at the end of the full expression, which is after the call to bar returns.

    But at this point you might as well make bar take a const std::span<const int>& (and for span specifically, you might as well take it by value)