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