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

wye*_*r33 6 c++ c++11 c++14

我有一种情况,我想用一个参数调用一个函数,并将结果返回到同一个参数

foo = f(foo);
Run Code Online (Sandbox Code Playgroud)

另外,我假设参数x非常大,所以我不想调用它的复制构造函数,而是调用它的移动构造函数.最后,我不想通过引用传递参数,因为我想f用另一个函数组合函数g.因此,这样的事情就好

foo = g(f(foo));
Run Code Online (Sandbox Code Playgroud)

是可能的.现在,使用移动语义,这几乎是可能的,如以下程序所示

#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;
}
Run Code Online (Sandbox Code Playgroud)

该程序的输出是:

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
Run Code Online (Sandbox Code Playgroud)

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

当满足某些条件时,允许实现省略类对象的复制/移动构造,即使为复制/移动操作选择的构造函数和/或对象的析构函数具有副作用.在这种情况下,实现将省略的复制/移动操作的源和目标视为仅仅两种不同的引用同一对象的方式,并且该对象的销毁发生在两个对象的后期时间.没有优化就被破坏了.复制/移动操作的省略,称为复制省略,在以下情况下允许(可以合并以消除多个副本):

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

由于我们返回一个函数参数,我们不会得到复制elison.此外:

当满足或将满足复制操作的省略标准时,除了源对象是函数参数这一事实,并且要复制的对象由左值指定,重载决策选择复制的构造函数是首先执行,好像对象是由右值指定的.如果重载决策失败,或者所选构造函数的第一个参数的类型不是对象类型的rvalue引用(可能是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;
}
Run Code Online (Sandbox Code Playgroud)

这会产生

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
Run Code Online (Sandbox Code Playgroud)

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

编辑2

根据@ 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;
}
Run Code Online (Sandbox Code Playgroud)

这会产生

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
Run Code Online (Sandbox Code Playgroud)

在多参数的情况下

#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;
}
Run Code Online (Sandbox Code Playgroud)

这会产生

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
Run Code Online (Sandbox Code Playgroud)

wye*_*r33 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;
}
Run Code Online (Sandbox Code Playgroud)

这会生成

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
Run Code Online (Sandbox Code Playgroud)

在多参数情况下

#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;
}
Run Code Online (Sandbox Code Playgroud)

这会生成

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
Run Code Online (Sandbox Code Playgroud)