当复制省略失败时,有没有一种方法可以防止移动构造函数后跟移动赋值运算符?

6

我有一个情况,需要调用一个带参数的函数,并将结果返回到同一个参数中。

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

这很有道理。现在困扰我的是,在第一次调用或组合时,移动构造函数后跟随移动赋值运算符。理想情况下,我想使用复制省略来防止调用任何这些构造函数,但我不确定如何做。具体而言,函数和

1
所以,在标准下,赋值操作不能省略。通过将中间值存储到Foo f2Foo f3而不是返回到foo中,我们将总移动操作次数从5降低到3。@Agnew的隐含建议是使用Foo而不是Foo&&参数,实际上会增加2个move计数,因为函数参数在任何情况下都不能省略为输出。 - Yakk - Adam Nevraumont
1
如果在 f() 和 g() 中使用return foo;而非return std::move(foo);会发生什么?这将使NRVO发生。它无法解决foo=f(std::move(foo));,但应该避免在foo=g(f(std::move(foo)));中进行一次移动赋值操作。 - Frederic Lachasse
4
如果这些函数返回的是输入,那么它们是否应该通过rref返回而不是值返回?这样可以省去移动操作,但也会增加意外UB的可能性。 - Mooing Duck
@MooingDuck 很有趣,这似乎有效。一般来说,我不想通过rref返回,因为内存在函数退出时被释放。然而,由于内存是在函数调用外分配的,所以它似乎正在做正确的事情。尽管如此,我不理解你评论中的一部分。什么是“意外UB”? - wyer33
@MooingDuck 听起来不错。我已经更新了问题,并附上了演示所有内容的代码。 - wyer33
显示剩余2条评论
3个回答

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
从技术上讲,是的。我真的希望这也可以处理多个参数,这有点像改变目标。我会在问题的更新中详细解释。 - wyer33
2
不需要将fg作为模板参数。只需使compose接受一个std::initializer_list<void (*)(T &)>,并调用compose<Foo>({ f, g, f, g }, v)即可。通过将T作为模板参数包,可以轻松扩展以处理多个参数。(但我可能会错过什么。如果这种形式的组合是可行的,那么为什么不直接编写f(v); g(v); f(v); g(v);呢?) - user743382
@hvd 楼主想要能够组合这些过程,也许是为了将组合传递给其他代码的某个部分? - didierc
1
在这种情况下,我会使用lambda表达式:您可以将[](Foo &v) { f(v); g(v); f(v); g(v); }传递给任何接受void(*)(Foo &) 的函数。我想这也是对OP的一些评论,不仅仅是对你的评论。我不确定您的答案是否是解决OP问题的最佳方法,但部分原因是因为我不清楚提出问题的原因。我会避免编辑,因为我可能完全误解了OP提出问题的原因。 - user743382
@hvd 嗯,这确实是一个更简单的方法!有时候事情是如此显而易见,以至于我看不到它们。 - didierc
显示剩余2条评论

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
}

由于内联或NRVO?我认为NRVO将优化掉返回值,而内联将用do_f(foo)的调用替换对f(foo)的调用,是吗? - cdmh
嗯,其中一个取决于你的看法。如果你先看内联,它会用Foo temp_foo = foo; do_f(temp_foo); foo = temp_foo;替换调用,编译器会将其优化为do_f(foo);。但你也可以从另一个角度来看待它,即首先通过NRVO优化掉返回值,然后再进行内联。在两种情况下,生成的代码都是相同的。 - StenSoft
NRVO 不会应用于函数参数。也就是说,对象 foo 将被“移动”到返回值中,并且该移动可能无法省略。 - dyp

0

根据 @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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接