Search code examples
c++lambdaapi-designrvalue

Designing function API to avoid object lifetime problems


I've got a function that returns a lambda.

auto make_scanner(std::vector<int> const &v) {
    auto begin = v.cbegin();
    auto end = v.cend();
    return [begin, end] () mutable -> int {
        return (begin != end) ? *begin++ : 0;
    };
}

Note that the lambda captures iterators into the vector, but not the vector itself. In the common use case, this is fine:

// Common Use Case
std::vector<int> data = {1, 2, 3};
auto scanner = make_scanner(data);
for (int x = scanner(); x != 0; x = scanner()) {
    // compute something
}

But if the caller makes a scanner from an unnamed temporary vector, the lambda is left with dangling iterators:

// Incorrect Use Case
auto scanner = make_scanner(std::vector<int>{1, 2, 3});
// Invoking scanner() is UB because the iterators reference
// the vector after its lifetime.

In a non-optimized build (i.e., a debug build), this will usually work as intended even though the code is wrong. In an optimized build, if you're lucky, it will crash or at least cause tests to fail.

Is there a way to fail compilation when make_scanner is called with an unnamed temporary vector while still allowing the common use case?

I've been trying out various overloads with r-value references, but the answer eludes me.

(Yes, I realize it's possible to use a non-temporary vector and still end up with dangling iterators in the lambda. I'm not concerned with those case at this time.)


Solution

  • This is fairly easy to achieve leveraging deleted functions. The first thing we need to know is that when overloading a function the parameter T&& takes precedence over an overload with the parameter const T& when the function is called with an rvalue.

    If you then delete the rvalue reference overload then overload resolution will chose that function for rvalues and you will get a compiler error for using a deleted function.

    For your code that becomes

    auto make_scanner(std::vector<int> const &v) {
        auto begin = v.cbegin();
        auto end = v.cend();
        return [begin, end] () mutable -> int {
            return (begin != end) ? *begin++ : 0;
        };
    }
    
    auto make_scanner(std::vector<int> &&v) = delete;
    

    and you can see it working in this live example.