当复制elison失败时,有没有办法阻止移动构造函数后跟移动赋值运算符?

时间:2015-02-09 14:41:28

标签: c++ c++11 c++14

我的情况是我想用参数调用一个函数并将结果返回到同一个参数

foo = f(foo);

另外,我假设参数x非常大,所以我不想调用它的复制构造函数,而是调用它的移动构造函数。最后,我不想通过引用传递参数,因为我想将函数f与另一个函数g组合在一起。因此,像

这样的事情
foo = g(f(foo));

是可能的。现在,通过移动语义,这几乎是可能的,如以下程序所示

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

该程序的输出是:

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

这是有道理的。现在,困扰我的是当我们第一次调用f或者组合时,移动构造函数后面跟着移动赋值运算符。理想情况下,我想使用复制elison来防止任何这些构造函数被调用,但我不确定如何。具体来说,函数fgstd::move上调用foo,因为否则会调用复制而不是移动构造函数。这在C ++标准的12.8.31和12.8.32节中规定。具体地,

  

当满足某些条件时,允许省略实现   复制/移动类对象的构造,即使构造函数   选择用于复制/移动操作和/或析构函数   对象有副作用。在这种情况下,实施处理   省略的复制/移动操作的源和目标只是两个   引用同一个对象的不同方式,以及对它的破坏   该对象发生在两个对象的后期   没有优化就会被破坏。这个省略   复制/移动操作,称为复制省略,是允许的   以下情况(可以合并以消除多个   份):

     

- 在具有类返回类型的函数的return语句中,何时   表达式是非易失性自动对象的名称(其他   比函数或catch子句参数)具有相同的cvunqualified   键入函数返回类型,复制/移动操作即可   通过直接构造自动对象省略   函数的返回值

由于我们返回一个函数参数,我们没有得到复制elison。另外:

  

当符合或将要执行复制操作的标准时   因为源对象是一个函数参数这个事实,   并且要复制的对象由左值,超载指定   首先执行选择复制的构造函数的分辨率   好像对象是由右值指定的。如果超载分辨率   失败,或者如果所选的第一个参数的类型   构造函数不是对象类型的右值引用(可能   cv-qualified),重新执行重载决策,考虑到   对象作为左值。 [注意:这个两级重载决议必须   无论是否会发生复制,都会执行。它   如果没有执行elision,则确定要调用的构造函数,   并且即使呼叫是必须的,也必须可以访问所选的构造函数   省略。 - 后注]

由于我们返回一个函数参数,我们返回一个l值,因此我们被迫使用std::move。现在,在一天结束时,我只想将内存移回参数并调用移动构造函数和移动赋值运算符似乎太多了。感觉应该有一个移动或复制elison。有没有办法实现这个目标?

编辑1

对@ didierc的回答的答复比评论所允许的更长,从技术上讲,是的,这对于这种情况是有效的。同时,更大的目标是允许具有多个返回的函数以不复制任何内容的方式组合在一起。我也可以通过移动语义来实现这一点,但它需要C ++ 14的技巧才能工作。它还通过大量举措加剧了这个问题。但是,从技术上讲,没有副本。具体做法是:

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

这会生成

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

基本上,出现了与上述相同的问题:如果它们消失,我们会获得移动分配。

编辑2

Per @ MooingDuck的建议,实际上可以从函数返回一个rref。一般来说,这是一个非常糟糕的主意,但由于内存是在函数之外分配的,因此它变得没有问题。然后,移动次数显着减少。不幸的是,如果有人试图将结果分配给rref,这将导致未定义的行为。所有代码和结果都在下面。

对于单个参数案例:

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

这会生成

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

在多参数案例中

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

这会生成

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

3 个答案:

答案 0 :(得分:3)

在我看来,你需要的不是通过函数调用来回移动值的机制,因为引用可以充分地做到这一点,而是一个用这种方式组合函数的设备。

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

当然,您可以在参数类型上使其更通用。

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 &){
}

示例:

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

请注意,您甚至可以在过程原型上参数化模板,但此时在我的机器上,只有clang ++(v3.5)似乎接受它,g ++(4.9.1)不喜欢它。

答案 1 :(得分:2)

如果您使用一点间接和编译器优化,则可以在不移动的情况下执行此操作:

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 :(得分:0)

Per @ MooingDuck的建议,它实际上可以从函数中返回一个rref。一般来说,这是一个非常糟糕的主意,但由于内存是在函数之外分配的,因此它变得没有问题。然后,移动次数显着减少。不幸的是,如果有人试图将结果分配给rref,这将导致未定义的行为。所有代码和结果都在下面。

对于单个参数案例:

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

这会生成

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

在多参数案例中

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

这会生成

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