Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c++

c++11

c++14

I've a situation where I want to call a function with a parameter and return the result into this same argument

foo = f(foo);

In addition, I assume that the parameter x is very large, so I don't want to call its copy constructor, but rather its move constructor. Finally, I don't want to pass the argument by reference because I would like to compose the function f with another function g. Hence, so that things like

foo = g(f(foo));

are possible. Now, with move semantics, this is all mostly possible as demonstrated by the following 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;
}

The output from this program is:

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

which makes sense. Now, what's bothering me is that when we call f the first time, or the composition, the move constructor is followed by the move assignment operator. Ideally, I'd like to use copy elison to prevent any of these constructors from being called, but I'm not sure how. Specifically, the functions f and g call std::move on foo because otherwise the copy, not move, constructor is called. This is specified in the C++ standard under section 12.8.31 and 12.8.32. Specifically,

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the constructor selected for the copy/move operation and/or the destructor for the object have side effects. In such cases, the implementation treats the source and target of the omitted copy/move operation as simply two different ways of referring to the same object, and the destruction of that object occurs at the later of the times when the two objects would have been destroyed without the optimization. This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):

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

Since we return a function argument, we don't get copy elison. In addition:

When the criteria for elision of a copy operation are met or would be met save for the fact that the source object is a function parameter, and the object to be copied is designated by an lvalue, overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue. If overload resolution fails, or if the type of the first parameter of the selected constructor is not an rvalue reference to the object’s type (possibly cv-qualified), overload resolution is performed again, considering the object as an lvalue. [ Note: This two-stage overload resolution must be performed regardless of whether copy elision will occur. It determines the constructor to be called if elision is not performed, and the selected constructor must be accessible even if the call is elided. —end note ]

Since we return a function argument, we return an l-value, so we're forced to use std::move. Now, at the end of the day, I just want the memory moved back into the argument and calling both a move constructor and move assignment operator seems like too much. It feels like there should be a single move or copy elison. Is there a way to accomplish this?

Edit 1

In a longer response to @didierc's answer than a comment would allow, technically, yes, that would work for this situation. At the same time, the greater goal is to allow functions with multiple returns to be composed together in a way where nothing is copied. I can also do this with move semantics, but it requires a trick from C++14 to work. It also exacerbates the issue with lots of moves. However, technically, there's no copies. Specifically:

#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;
}

This 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

Basically, the same issue as above occurs: We get move assignments that would be nice if they disappeared.

Edit 2

Per @MooingDuck's suggestion, it's actually possible to return an rref from the functions. Generally, this would be a really bad idea, but since the memory is allocated outside of the function, it becomes a non-issue. Then, the number of moves is dramatically reduced. Unfortunately, if someone tries to assign the result to an rref, this will cause undefined behavior. All of the code and results are below.

For the single argument case:

#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's a destructor"
        " before this line that'd mean that this reference is invalid."
        << std::endl;
}

This 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's a destructor before this line that'd mean that this reference is invalid.
destructor
destructor

In the multi-argument case

#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's a destructor"
        " before this line that'd mean that this reference is invalid."
        << std::endl;
}

This 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's a destructor before this line that'd mean that this reference is invalid.
destructor
destructor
destructor
destructor
like image 914
wyer33 Avatar asked Feb 09 '15 14:02

wyer33


2 Answers

It seems to me that what you need is not a mechanism to move back and forth a value through a function call, since references do that adequately, but a device to compose functions working that way.

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

Of course, you could make this more generic on the parameter type.

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){
  v.x *= 2;
}

int main(){
  Foo v = { 9 };

  compose<Foo, f, g, f, g>(v);

  std::cout << v.x << "\n"; // output "42"
}

Note that you could even parameterize the template on the procedure prototype, but at this time on my machine, only clang++ (v3.5) seems to accept it, g++ (4.9.1) doesn't like it.

like image 156
didierc Avatar answered Oct 05 '22 23:10

didierc


You can do it without move if you use a little bit of indirection and compiler optimizations:

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
}
like image 27
StenSoft Avatar answered Oct 05 '22 23:10

StenSoft