Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiple return values (structured bindings) with unmovable types and guaranteed RVO in C++17

With C++ 17, we will have the possibility to return unmovable (including uncopyable) types such as std::mutex, via what can be thought of as guaranteed return value optimization (RVO): Guaranteed copy elision through simplified value categories:

struct nocopy { nocopy(nocopy&) = delete; nocopy() = default; };
auto getRVO(){
    return nocopy();
}

We will also have structured bindings, allowing:

tuple<T1,T2,T3> f();
auto [x,y,z] = f();

or (here also using my understanding of the feature template argument deduction for constructors)

template<typename T1,typename T2,typename T3>
struct many {
  T1 a;
  T2 b;
  T3 c;
};
// (Original questions missed 'many' on the next line. Thanks, T.C.)
auto f(){ return many{string(),5.7, false} }; 
auto [x,y,z] = f();

But do these features compose to enable something like this?

auto get_ensured_rvo_str(){
    return std::pair(std::string(),nocopy());
}

auto get_class_and_mutex(){
    return many{SomeClass(),std::mutex(),std::string()};
}

int main(){
    auto rvoStr = get_ensured_rvo_str().first;
    auto [ mtx,sc,str ] = get_class_and_mutex();
}

My thinking is that for this to work, it would required guaranteed RVO of the aggregate constructor arguments at forming std::tuple or many, but wouldn't that be named RVO (NRVO) which is specifically not included in the P0144R2 proposal?


Side note: P0144R2 specifically mentions that move-only types are supported:

2.6 Move-only types

Move-only types are supported. For example:

struct S { int i; unique_ptr<widget> w; };
S f() { return {0, make_unique<widget>()}; }
auto [ my_i, my_w ] = f();
like image 791
Johan Lundberg Avatar asked Jul 14 '16 22:07

Johan Lundberg


2 Answers

template<typename T1,typename T2,typename T3>
struct many {
  T1 a;
  T2 b;
  T3 c;
};
auto f(){ return {string(),5.7, false} };

This won't compile. First you never said f is to return a many. Second, class template argument deduction work with constructors, and the only constructors of many are the implicitly declared default, copy and move constructors.

You need a guide:

template<class T1, class T2, class T3>
many(T1, T2, T3) -> many<T1, T2, T3>;
auto get_ensured_rvo_str(){
    return std::pair(std::string(),nocopy());
}

This doesn't work either. nocopy() is materialized into a temporary that is bound to the reference parameter of pair's constructor, which then attempts to move from it and fails. No elision of that temporary is possible or allowed.

(Of course, as Nicol Bolas points out in his answer, the class member access in get_ensured_rvo_str().first materializes the pair return value of get_ensured_rvo_str, so rvoStr would in fact be moved constructed from the first member of that materialized temporary. But here you have a problem well before that.)

auto get_class_and_mutex(){
    return many{SomeClass(),std::mutex(),std::string()};
}
auto [ mtx,sc,str ] = get_class_and_mutex();

This is fine (assuming you have a deduction guide). Aggregate initialization doesn't call any constructor of many; it initializes the members directly with the corresponding prvalue initializer.

like image 54
T.C. Avatar answered Oct 31 '22 23:10

T.C.


Structured binding is defined to work on the basis of extracting references or pseudo-references to individual values. That is, if you do this:

auto [x,y,z] = f();

What you get is this something like this:

auto HIDDEN_VALUE = f();
auto &x = get<0>(HIDDEN_VALUE);
auto &y = get<1>(HIDDEN_VALUE);
auto &z = get<2>(HIDDEN_VALUE);

When dealing with structs, x, y, and z won't be references; they will be something which "refers to" the actual array member, but it isn't an actual reference. The main point is that x, y and z are never copies of anything.

As such, the question is whether HIDDEN_VALUE is copied. And it's clear that HIDDEN_VALUE is value constructed. And thus, if the return of f() is a prvalue, then the rules of guaranteed elision will apply.

auto rvoStr = get_ensured_rvo_str().first;

The expression get_ensured_rvo_str() is a prvalue. However, the result of applying .first to it is not a prvalue. Applying .first forces the prvalue (under guaranteed elision rules) to construct a temporary, with .first being applied to it. The element extracted, which is an xvalue, will be used to copy initialize rvoStr.

So under no version of the standard is the copy into rvoStr elided.

return many{SomeClass(),std::mutex(),std::string()};
...
auto [ mtx,sc,str ] = get_class_and_mutex();

I'm going to assume that you've made the necessary additions needed for the return statement to compile.

Given that, the construction in the function will directly initialize the HIDDEN_VALUE at the return site. And each of the members of the aggregate will be initialized directly by prvalues, so no copying will happen.

like image 39
Nicol Bolas Avatar answered Oct 31 '22 23:10

Nicol Bolas