Search code examples
c++perfect-forwarding

C++ perfect forwarding: how to avoid dangling references


Consider the following problem: I have a number of classes, each implementing a get() function. The following container1 and container2 are examples of such classes:

struct expensive_type {
    int v;
};

struct container1 {
    expensive_type get() const {
        return { 1 };
    }
};

struct container2 {
    expensive_type x;
    expensive_type& get() {
        return x;
    }
};

I want to create a wrapper, templated on C and F, that implements the same get() functionality of C, but applies a function to the result:

template<typename C, typename F>
struct wrapper {
    F f;
    C c;
    decltype(auto) get() {
        return f(c.get());
    }
};

I would now like to create a function f for a trivial wrapper, that simply returns its argument unchanged. I thought this would work:

auto f = [](auto&& x) -> decltype(auto) {
    return forward<decltype(x)>(x);
};

wrapper<container1, decltype(f)> trivial_wrapper1 { f, {} };
wrapper<container2, decltype(f)> trivial_wrapper2 { f, {} };

, but unfortunately trivial_wrapper1.get() returns expensive_type{0} instead of expensive_type{1} (at least with the -O2 flag). I guess the problem has something to do with dangling references, but I cannot figure out how to fix it.

My question is: how to correctly implement the function f so that it it acts as a perfect identity, without ever copying its argument?

To clarify, here are examples of the intended behaviour:

cout << trivial_wrapper1.get().v << endl; // should print 1, prints 0 as of now
trivial_wrapper2.get().v = 2;
cout << trivial_wrapper2.c.x.v << endl; // should print 2, and it does as of now

Solution

  • This issue here is that there is no temporary extension through functions. When you do

    return f(c.get());
    

    when c is a container1 you return by value so you have temporary. That temporary only lives to the end of the full expression which means it dies once the return statement finishes. This is why you have a dangling reference.

    This leaves you in a little bit of a bind though. what f would need to do is return by value if it got a temporary but this could copy which could be expensive. There really isn't a way around that though if you are going to pass the return value through an intermediary function. That would give you an f like

    auto f = [](auto&& x) -> std::conditional_t<std::is_rvalue_reference_v<decltype(x)>,
                                                std::remove_reference_t<decltype(x)>, 
                                                decltype(x)> {
        return x;
    };
    

    which returns by value for rvalues and by reference for lvalues.