decltype(auto) return type and lifetime issues

126 Views Asked by At

I am just looking at some code with the following rough outline.

#include <iostream>

template<typename T, typename F>
decltype(auto) do_something(T&& object, F f) {
    return f(std::forward<T>(object));
}

struct object {
    int x;
    int& ref() { return x; }
    int const& ref() const { return x; } 
};

struct f {
template<typename T>
decltype(auto) operator()(T&& x) {
   return x.ref();
}
};


int main(int argc, char** argv) {

    auto & x = do_something(object{}, f{});

    return 0;
}

so if i called like this

auto& do_something(object{}, f{);

Some questions around this are that if f returns a reference and f takes ownership of object via move semantics are we not left with a lifetime issue?

What are the potential issues of returning by decltype(auto)?

Or is this better

#include <iostream>
#include <type_traits>

struct object {
    int x{};
    
    int const& ref() const noexcept { return x; }
    int& ref() noexcept { return x; }
};

struct functor {
    
    template<typename T>
    decltype(auto) operator()(T&& arg) {
        return arg.ref();
    }
};

template<typename T, typename F>
decltype(auto) apply(T&& o, F f) {
    if constexpr(std::is_lvalue_reference_v<T>) {
        return f(std::forward<T>(o)); 
    }else {
        auto result = f(std::forward<T>(o));
        return result;
    }
}
1

There are 1 best solutions below

7
On

f takes ownership of object via move semantics

If f is a function, then it's not possible for it to return a valid reference to it in the first place, even without your wrapper.

But your code doesn't take ownership of the object it's given, so it's fine.


There's a different, rather obscure failure scenario: f is a functor that moves the parameter into its member variable, then returns a reference to it. Then you'd get a dangling reference after f dies:

struct A
{
    object x;
    object &operator()(object y)
    {
        x = std::move(y);
        return x;
    }
};

Here, A{}(object{}).foo() is legal, but do_something(object{}, A{}).foo() is UB.

The solution is to use F &&f (which is a good idea anyway, to avoid a copy). You should also std::forward<F>(f) when calling it:

template <typename T, typename F>
decltype(auto) do_something(T &&object, F &&f)
{
    return std::forward<F>(f)(std::forward<T>(object));
}