2

How does one exploit structured binding and tuples to return objects local to a function?

In a function, I am making local objects that reference each other, and I want to return these objects in a tuple and use structured binding to identify them whenever I call the function. I currently have this:

std::tuple<Owner&&, State<Controller>&&, State<Ancillary>&&, State<Compressor>&&>
inline makeOwner() {
    State<Controller>&& controller = State<Controller>();
    State<Ancillary>&&  ancillary  = State<Ancillary>();
    State<Compressor>&& compressor = State<Compressor>();

    Owner&& owner = Owner(controller, ancillary, compressor);

    return {owner, controller, ancillary, compressor};
}

// using the function later

const &&[owner, controller, ancillary, compressor] = makeOwner();

This does not work, and I get an error saying the return value isn't convertable to a tuple of the aforementioned return type. I'm not sure why this is the case, since the types match up to the declarations.

Ultimately, I'm trying to create a convenience function so I don't have to type the four lines in the function every time I want to make a new Owner. This is my attempt at using structured binding to make this easier.

EDIT: I should note that I want the bindings in the last line to reference the objects inside of owner. So, copies are insufficient.

Anthony
  • 1,015
  • 8
  • 22
  • Remember that an rvalue reference is still just a reference, and returning a reference to a local variable is bad. You should be able to return by value and enjoy the performance already given to you by RVO. – alter_igel Aug 28 '18 at 18:03

2 Answers2

5

I want the bindings in the last line to reference the objects inside of owner.

Let's ignore all of the new language features and go back to basics. How do you expect this to work?

int&& f() { return 0; }
int&& r = f();

You want r to be a reference to the local variable inside of f? But that gets destroyed at the end of the execution of f(). This code compiles but r is a dangling reference.

The only way for this to be safe is to ensure that f() returns a reference to an object that definitely outlives the function. Maybe it's a local static, maybe it's global, maybe it's a member variable of the class that f is a member function of, etc:

int global = 0;
int&& f() { return std::move(global); }
int&& r = f(); // okay, r is a reference to global, no dangling

Or, if that doesn't make sense, then you need to return an object by value. You can still take a reference to it. Or not:

int f() { return 0; }
int&& r = f(); // okay, lifetime extension
int i = f();   // okay, prvalue elision

The same underlying principles apply once we add in all the complexities of tuple and structured bindings. Either return local, non-static objects by value, or return some other objects by reference. But do not return local, non-static objects by reference.


Ultimately, I'm trying to create a convenience function so I don't have to type the four lines in the function every time I want to make a new Owner. This is my attempt at using structured binding to make this easier.

Why not just make a type?

struct X {
    X() : owner(controller, ancillary, compressor) { }
    X(X const&) = delete;
    X& operator=(X const&) = delete;

    State<Controller> controller;
    State<Ancillary>  ancillary;
    State<Compressor> compressor;
    Owner owner;        
};

// lifetime extension on the X, no copies anywhere
// note that owner is last
auto&& [controller, ancillary, compressor, owner] = X();

// no lifetime extension, but also no copies because
// prvalue elision
auto [controller, ancillary, compressor, owner] = X();
Barry
  • 286,269
  • 29
  • 621
  • 977
1
inline auto makeOwner() {
   struct bundle {
     State<Controller> controller;
     State<Ancillary> ancillary;
     State<Compressor> compressor;
     Owner owner = Owner(controller, ancillary, compressor);
     bundle(bundle  const&)=delete;
     bundle& operator=(bundle  const&)=delete;
   };
   return bundle{};
}

// using the function later

const auto&&[owner, controller, ancillary, compressor] = makeOwner();

here we use the fact that structs, even anonymous ones, can be unbundled like tuples.

Live example.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524