Is there a way to prevent the move constructor, followed by the forwarding assignment operator, when the elison copy fails?

I have a situation where I want to call a function with a parameter and return the result to the same argument

foo = f(foo); 

In addition, I assume that the parameter x very large, so I do not want to call its copy constructor, but rather its move constructor. Finally, I do not want to pass the argument by reference, because I would like to link the function f with another function g . Therefore, to things like

 foo = g(f(foo)); 

are possible. Now, with the semantics of movement, this is basically possible, as shown in the next program

 #include <iostream> struct Foo { Foo() { std::cout << "constructor" << std::endl; } Foo(Foo && x) { std::cout << "move" << std::endl; } Foo(Foo const & x) { std::cout << "copy" << std::endl; } ~Foo() { std::cout << "destructor" << std::endl; } Foo & operator = (Foo && x) { std::cout << "move assignment" << std::endl; return *this; } Foo & operator = (Foo & x) { std::cout << "copy assignment" << std::endl; return *this; } }; Foo f(Foo && foo) { std::cout << "Called f" << std::endl; return std::move(foo); } Foo g(Foo && foo) { std::cout << "Called g" << std::endl; return std::move(foo); } int main() { Foo foo; foo = f(std::move(foo)); std::cout << "Finished with f(foo)" << std::endl; foo = g(f(std::move(foo))); std::cout << "Finished with g(f(foo))" << std::endl; } 

Exiting this program:

 constructor Called f move move assignment destructor Finished with f(foo) Called f move Called g move move assignment destructor destructor Finished with g(f(foo)) destructor 

that makes sense. What worries me now is that when we call f for the first time or into composition, the move constructor is followed by the move assignment operator. Ideally, I would like to use a copy of elison to prevent any of these constructors from being called, but I don't know how to do this. In particular, the functions f and g call std::move on foo , because otherwise the constructor copy, not move, constructor is called. This is specified in the C ++ standard in sections 12.8.31 and 12.8.32. In particular,

When certain criteria are met, the implementation may skip copying / moving the class object, even if the constructor of the selected copy / move and / or destructor operation for the object has side effects. In such cases, the implementation considers the source and purpose of the missed copy / move operation as just two different ways of accessing the same object and destroying this object at later times when two objects would be destroyed without optimization. These copy / move operations, called a copy, are permitted in the following circumstances (which can be combined to eliminate multiple copies):

- in the return statement in a function with a return type of the class, when the expression is the name of a non-volatile automatic object (other than the parameter of the function or catch-clause) with the same cvunqualified type as the return type of the function, the copy / move operation can be omitted by creating an automatic object directly in function returns value

Since we are returning a function argument, we are not getting a copy of elison. Besides:

When the criteria for excluding the copy operation are fulfilled or will be saved for the fact that the source object is a parameter of the function, and the object to be copied is indicated by the value lvalue, overloading the permission to select the constructor for the copy is first performed as if the object was designated rvalue. If overload resolution fails, or if the type of the first parameter of the selected constructor is not a rvalue reference to the type of objects (possibly cv-qualified), overload resolution is performed again, considering the object as an lvalue. [Note. This two-step overload resolution should be performed regardless of whether copying occurs. This determines the calling constructor if elision fails, and the selected constructor should be available, even if the call is omitted. -end note]

Since we return the argument of the function, we return the value of l, so we are forced to use std::move . Now, in the end, I just want the memory to return to the argument, and the call to both the move constructor and the transfer operator seems too big. There seems to be one move or a copy of Alison. Is there any way to do this?

Change 1

In a longer answer to @didierc's answer than comment, technically, yes, this will work for this situation. At the same time, the big goal is to allow multi-return functions to stack together so that nothing is copied. I can also do this using move semantics, but this requires a trick from C ++ 14. This also exacerbates the problem with a lot of moves. However, technically there are no copies. In particular:

 #include <tuple> #include <iostream> #include <utility> // This comes from the N3802 proposal for C++ template <typename F, typename Tuple, size_t... I> decltype(auto) apply_impl(F&& f, Tuple&& t, std::index_sequence<I...>) { return std::forward<F>(f)(std::get<I>(std::forward<Tuple>(t))...); } template <typename F, typename Tuple> decltype(auto) apply(F&& f, Tuple&& t) { using Indices = std::make_index_sequence<std::tuple_size<std::decay_t<Tuple>>::value>; return apply_impl(std::forward<F>(f), std::forward<Tuple>(t), Indices{}); } // Now, for our example struct Foo { Foo() { std::cout << "constructor" << std::endl; } Foo(Foo && x) { std::cout << "move" << std::endl; } Foo(Foo const & x) { std::cout << "copy" << std::endl; } ~Foo() { std::cout << "destructor" << std::endl; } Foo & operator = (Foo && x) { std::cout << "move assignment" << std::endl; return *this; } Foo & operator = (Foo & x) { std::cout << "copy assignment" << std::endl; return *this; } }; std::tuple <Foo,Foo> f(Foo && x,Foo && y) { std::cout << "Called f" << std::endl; return std::make_tuple <Foo,Foo> (std::move(x),std::move(y)); } std::tuple <Foo,Foo> g(Foo && x,Foo && y) { std::cout << "Called g" << std::endl; return std::make_tuple <Foo,Foo> (std::move(x),std::move(y)); } int main() { Foo x,y; std::tie(x,y) = f(std::move(x),std::move(y)); std::cout << "Finished with f(foo)" << std::endl; std::tie(x,y) = apply(g,f(std::move(x),std::move(y))); std::cout << "Finished with g(f(foo))" << std::endl; } 

It generates

 constructor constructor Called f move move move assignment move assignment destructor destructor Finished with f(foo) Called f move move Called g move move move assignment move assignment destructor destructor destructor destructor Finished with g(f(foo)) destructor destructor 

In principle, the same problem arises as above: we get transfer destinations that would be good if they disappeared.

Edit 2

As suggested by @MooingDuck, you can actually return rref from functions. This will usually be a very bad idea, but since memory is allocated outside the function, it becomes a non-problem. Then the number of moves decreases sharply. Unfortunately, if someone tries to assign the result to rref, this will lead to undefined behavior. Below is the code and results.

For the case of a single argument:

 #include <iostream> struct Foo { // Add some data to see if it gets moved correctly int data; Foo() : data(0) { std::cout << "default constructor" << std::endl; } Foo(int const & data_) : data(data_) { std::cout << "constructor" << std::endl; } Foo(Foo && x) { data = x.data; std::cout << "move" << std::endl; } Foo(Foo const & x) { data = x.data; std::cout << "copy" << std::endl; } ~Foo() { std::cout << "destructor" << std::endl; } Foo & operator = (Foo && x) { data = x.data; std::cout << "move assignment" << std::endl; return *this; } Foo & operator = (Foo & x) { data = x.data; std::cout << "copy assignment" << std::endl; return *this; } }; Foo && f(Foo && foo) { std::cout << "Called f: foo.data = " << foo.data << std::endl; return std::move(foo); } Foo && g(Foo && foo) { std::cout << "Called g: foo.data = " << foo.data << std::endl; return std::move(foo); } int main() { Foo foo(5); foo = f(std::move(foo)); std::cout << "Finished with f(foo)" << std::endl; foo = g(f(std::move(foo))); std::cout << "Finished with g(f(foo))" << std::endl; Foo foo2 = g(f(std::move(foo))); std::cout << "Finished with g(f(foo)) a second time" << std::endl; std::cout << "foo2.data = " << foo2.data << std::endl; // Now, break it. Foo && foo3 = g(f(Foo(4))); // Notice that the destuctor for Foo(4) occurs before the following line. // That means that foo3 points at destructed memory. std::cout << "foo3.data = " << foo3.data << ". If there a destructor" " before this line that'd mean that this reference is invalid." << std::endl; } 

It generates

 constructor Called f: foo.data = 5 move assignment Finished with f(foo) Called f: foo.data = 5 Called g: foo.data = 5 move assignment Finished with g(f(foo)) Called f: foo.data = 5 Called g: foo.data = 5 move Finished with g(f(foo)) a second time foo2.data = 5 constructor Called f: foo.data = 4 Called g: foo.data = 4 destructor foo3.data = 4. If there a destructor before this line that'd mean that this reference is invalid. destructor destructor 

In the case of multiple arguments

 #include <tuple> #include <iostream> #include <utility> // This comes from the N3802 proposal for C++ template <typename F, typename Tuple, size_t... I> decltype(auto) apply_impl(F&& f, Tuple&& t, std::index_sequence<I...>) { return std::forward<F>(f)(std::get<I>(std::forward<Tuple>(t))...); } template <typename F, typename Tuple> decltype(auto) apply(F&& f, Tuple&& t) { using Indices = std::make_index_sequence<std::tuple_size<std::decay_t<Tuple>>::value>; return apply_impl(std::forward<F>(f), std::forward<Tuple>(t), Indices{}); } // Now, for our example struct Foo { // Add some data to see if it gets moved correctly int data; Foo() : data(0) { std::cout << "default constructor" << std::endl; } Foo(int const & data_) : data(data_) { std::cout << "constructor" << std::endl; } Foo(Foo && x) { data = x.data; std::cout << "move" << std::endl; } Foo(Foo const & x) { data = x.data; std::cout << "copy" << std::endl; } ~Foo() { std::cout << "destructor" << std::endl; } Foo & operator = (Foo && x) { std::cout << "move assignment" << std::endl; return *this; } Foo & operator = (Foo & x) { std::cout << "copy assignment" << std::endl; return *this; } }; std::tuple <Foo&&,Foo&&> f(Foo && x,Foo && y) { std::cout << "Called f: (x.data,y.data) = (" << x.data << ',' << y.data << ')' << std::endl; return std::tuple <Foo&&,Foo&&> (std::move(x),std::move(y)); } std::tuple <Foo&&,Foo&&> g(Foo && x,Foo && y) { std::cout << "Called g: (x.data,y.data) = (" << x.data << ',' << y.data << ')' << std::endl; return std::tuple <Foo&&,Foo&&> (std::move(x),std::move(y)); } int main() { Foo x(5),y(6); std::tie(x,y) = f(std::move(x),std::move(y)); std::cout << "Finished with f(x,y)" << std::endl; std::tie(x,y) = apply(g,f(std::move(x),std::move(y))); std::cout << "Finished with g(f(x,y))" << std::endl; std::tuple <Foo,Foo> x_y = apply(g,f(std::move(x),std::move(y))); std::cout << "Finished with g(f(x,y)) a second time" << std::endl; std::cout << "(x.data,y.data) = (" << std::get <0>(x_y).data << ',' << std::get <1> (x_y).data << ')' << std::endl; // Now, break it. std::tuple <Foo&&,Foo&&> x_y2 = apply(g,f(Foo(7),Foo(8))); // Notice that the destuctors for Foo(7) and Foo(8) occur before the // following line. That means that x_y2points at destructed memory. std::cout << "(x2.data,y2.data) = (" << std::get <0>(x_y2).data << ',' << std::get <1> (x_y2).data << ')' << ". If there a destructor" " before this line that'd mean that this reference is invalid." << std::endl; } 

It generates

 constructor constructor Called f: (x.data,y.data) = (5,6) move assignment move assignment Finished with f(x,y) Called f: (x.data,y.data) = (5,6) Called g: (x.data,y.data) = (5,6) move assignment move assignment Finished with g(f(x,y)) Called f: (x.data,y.data) = (5,6) Called g: (x.data,y.data) = (5,6) move move Finished with g(f(x,y)) a second time (x.data,y.data) = (5,6) constructor constructor Called f: (x.data,y.data) = (7,8) Called g: (x.data,y.data) = (7,8) destructor destructor (x2.data,y2.data) = (7,8). If there a destructor before this line that'd mean that this reference is invalid. destructor destructor destructor destructor 
+6
source share
3 answers

As suggested by @MooingDuck, you can actually return rref from functions. This will usually be a very bad idea, but since memory is allocated outside the function, it becomes a non-problem. Then the number of moves decreases sharply. Unfortunately, if someone tries to assign the result to rref, this will lead to undefined behavior. Below is the code and results.

For the case of a single argument:

 #include <iostream> struct Foo { // Add some data to see if it gets moved correctly int data; Foo() : data(0) { std::cout << "default constructor" << std::endl; } Foo(int const & data_) : data(data_) { std::cout << "constructor" << std::endl; } Foo(Foo && x) { data = x.data; std::cout << "move" << std::endl; } Foo(Foo const & x) { data = x.data; std::cout << "copy" << std::endl; } ~Foo() { std::cout << "destructor" << std::endl; } Foo & operator = (Foo && x) { data = x.data; std::cout << "move assignment" << std::endl; return *this; } Foo & operator = (Foo & x) { data = x.data; std::cout << "copy assignment" << std::endl; return *this; } }; Foo && f(Foo && foo) { std::cout << "Called f: foo.data = " << foo.data << std::endl; return std::move(foo); } Foo && g(Foo && foo) { std::cout << "Called g: foo.data = " << foo.data << std::endl; return std::move(foo); } int main() { Foo foo(5); foo = f(std::move(foo)); std::cout << "Finished with f(foo)" << std::endl; foo = g(f(std::move(foo))); std::cout << "Finished with g(f(foo))" << std::endl; Foo foo2 = g(f(std::move(foo))); std::cout << "Finished with g(f(foo)) a second time" << std::endl; std::cout << "foo2.data = " << foo2.data << std::endl; // Now, break it. Foo && foo3 = g(f(Foo(4))); // Notice that the destuctor for Foo(4) occurs before the following line. // That means that foo3 points at destructed memory. std::cout << "foo3.data = " << foo3.data << ". If there a destructor" " before this line that'd mean that this reference is invalid." << std::endl; } 

It generates

 constructor Called f: foo.data = 5 move assignment Finished with f(foo) Called f: foo.data = 5 Called g: foo.data = 5 move assignment Finished with g(f(foo)) Called f: foo.data = 5 Called g: foo.data = 5 move Finished with g(f(foo)) a second time foo2.data = 5 constructor Called f: foo.data = 4 Called g: foo.data = 4 destructor foo3.data = 4. If there a destructor before this line that'd mean that this reference is invalid. destructor destructor 

In the case of multiple arguments

 #include <tuple> #include <iostream> #include <utility> // This comes from the N3802 proposal for C++ template <typename F, typename Tuple, size_t... I> decltype(auto) apply_impl(F&& f, Tuple&& t, std::index_sequence<I...>) { return std::forward<F>(f)(std::get<I>(std::forward<Tuple>(t))...); } template <typename F, typename Tuple> decltype(auto) apply(F&& f, Tuple&& t) { using Indices = std::make_index_sequence<std::tuple_size<std::decay_t<Tuple>>::value>; return apply_impl(std::forward<F>(f), std::forward<Tuple>(t), Indices{}); } // Now, for our example struct Foo { // Add some data to see if it gets moved correctly int data; Foo() : data(0) { std::cout << "default constructor" << std::endl; } Foo(int const & data_) : data(data_) { std::cout << "constructor" << std::endl; } Foo(Foo && x) { data = x.data; std::cout << "move" << std::endl; } Foo(Foo const & x) { data = x.data; std::cout << "copy" << std::endl; } ~Foo() { std::cout << "destructor" << std::endl; } Foo & operator = (Foo && x) { std::cout << "move assignment" << std::endl; return *this; } Foo & operator = (Foo & x) { std::cout << "copy assignment" << std::endl; return *this; } }; std::tuple <Foo&&,Foo&&> f(Foo && x,Foo && y) { std::cout << "Called f: (x.data,y.data) = (" << x.data << ',' << y.data << ')' << std::endl; return std::tuple <Foo&&,Foo&&> (std::move(x),std::move(y)); } std::tuple <Foo&&,Foo&&> g(Foo && x,Foo && y) { std::cout << "Called g: (x.data,y.data) = (" << x.data << ',' << y.data << ')' << std::endl; return std::tuple <Foo&&,Foo&&> (std::move(x),std::move(y)); } int main() { Foo x(5),y(6); std::tie(x,y) = f(std::move(x),std::move(y)); std::cout << "Finished with f(x,y)" << std::endl; std::tie(x,y) = apply(g,f(std::move(x),std::move(y))); std::cout << "Finished with g(f(x,y))" << std::endl; std::tuple <Foo,Foo> x_y = apply(g,f(std::move(x),std::move(y))); std::cout << "Finished with g(f(x,y)) a second time" << std::endl; std::cout << "(x.data,y.data) = (" << std::get <0>(x_y).data << ',' << std::get <1> (x_y).data << ')' << std::endl; // Now, break it. std::tuple <Foo&&,Foo&&> x_y2 = apply(g,f(Foo(7),Foo(8))); // Notice that the destuctors for Foo(7) and Foo(8) occur before the // following line. That means that x_y2points at destructed memory. std::cout << "(x2.data,y2.data) = (" << std::get <0>(x_y2).data << ',' << std::get <1> (x_y2).data << ')' << ". If there a destructor" " before this line that'd mean that this reference is invalid." << std::endl; } 

It generates

 constructor constructor Called f: (x.data,y.data) = (5,6) move assignment move assignment Finished with f(x,y) Called f: (x.data,y.data) = (5,6) Called g: (x.data,y.data) = (5,6) move assignment move assignment Finished with g(f(x,y)) Called f: (x.data,y.data) = (5,6) Called g: (x.data,y.data) = (5,6) move move Finished with g(f(x,y)) a second time (x.data,y.data) = (5,6) constructor constructor Called f: (x.data,y.data) = (7,8) Called g: (x.data,y.data) = (7,8) destructor destructor (x2.data,y2.data) = (7,8). If there a destructor before this line that'd mean that this reference is invalid. destructor destructor destructor destructor 
0
source

It seems to me that you do not need a mechanism for moving back and forth values ​​through a function call, since links do this adequately, and a device for compiling functions that work in this way.

 template <void f(Foo &), void g(Foo &)> void compose2(Foo &v){ f(v); g(v); } 

Of course, you can make it more general in type of parameter.

 template <typename T, void f(T&), void (...G)(T&)> void compose(T &v){ f(v); compose2<T,G...>(v); } template <typename T> void compose(Foo &){ } 

Example:

 #include <iostream> //... above template definitions for compose elided struct Foo { int x; }; void f(Foo &v){ v.x++; } void g(Foo &v){ vx *= 2; } int main(){ Foo v = { 9 }; compose<Foo, f, g, f, g>(v); std::cout << vx << "\n"; // output "42" } 

Please note that you can even parameterize the template on the prototype of the procedure, but at this time on my machine only clang ++ (v3.5) seems to accept it, g ++ (4.9.1) does not like it.

+3
source

You can do this without moving if you use a little optimization of indirect and compilers:

 void do_f(Foo & foo); // The code that used to in in f inline Foo f(Foo foo) { do_f(foo); return foo; // This return will be optimized away due to inlining } 
+2
source

Source: https://habr.com/ru/post/982208/


All Articles