4

我正在尝试在大型对象之间进行操作,并尝试使用 r 值引用来避免临时对象创建。实验是下面的代码,但结果不是我预期的。

编码:

#include <iostream>
using namespace std;

struct A
{
    A() = default;
    A(const A& a) { cout << "copy ctor" << endl; }
    A(A&& a) { cout << "move ctor" << endl; }
    A &operator=(const A& a) { cout << "copy assign" << endl; return *this; }
    A &operator=(A&& a) { cout << "move assign" << endl; return *this; }
    A &operator*=(double s) { cout << "this = this *= s" << endl; return *this; }
    A operator*(double s) const { cout << "A = const this * s" << endl; return *this; }
    A &operator+=(const A &b) { cout << "this = this + const A&" << endl; return *this; }
    A operator+(const A &b) const { cout << "A = const this + const A&" << endl; return *this; }
    A &operator+(A &&b) const { cout << "A&& = const this + A&& --> "; return b += *this; }
};
A &operator+(A &&a, const A &b) { cout << "A&& = A&& + const A& --> "; return a += b; }
A &operator*(A &&a, double s) { cout << "A&& = A&& * s --> "; return a *= s; }

int main()
{
    A a,b,c,d;
    a = b + a * 4 + /*operator*(static_cast<A&&>(d), 2)*/ d * 2 + (A() + c) * 5;

    return 0;
}

输出:

A&& = A&& + const A& --> this = this + const A&     // A() + c
A = const this * s                  // (...) * 5
copy ctor                       // ???
A = const this * s                  // d * 2
copy ctor                       // ???
A = const this * s                  // a * 4
copy ctor                       // ???
A&& = const this + A&& --> this = this + const A&   // (d*2) + (...)
A&& = const this + A&& --> this = this + const A&   // (a*4) + (...)
A&& = const this + A&& --> this = this + const A&   // b + (...)
copy assign                     // a = (...)

我的期望:

A&& = A&& + const A& --> this = this + const A&     // A() + c
A&& = A&& * s --> this = this *= s          // (...) * 5
A&& = A&& * s --> this = this *= s          // (...) * 2    d is not used anymore, so I want to move semantics
A = const this * s      // a * 4    a is not used anymore, but I want to keep semantics
A&& = A&& + const A& --> this = this + const A& // (d*2) + (...)
A&& = A&& + const A& --> this = this + const A& // (a*4) + (...)
A&& = A&& + const A& --> this = this + const A& // b + (...)
move assign     // a = (...)
4

4 回答 4

4

这是一个更正确的版本,副本更少:

#include <iostream>
#include <utility>
using namespace std;

struct A
{
  A() = default;
  A(const A& a) { cout << "copy ctor" << endl; }
  A(A&& a) { cout << "move ctor" << endl; }
  A &operator=(const A& a) { cout << "copy assign" << endl; return *this; }
  A &operator=(A&& a) { cout << "move assign" << endl; return *this; }
  A &operator*=(double s) { cout << "this *= s" << endl; return *this; }
  A &operator+=(const A &b) { cout << "this += const A&" << endl; return *this; }
};

A&& operator+(A &&a, const A &b)
{ cout << "A&& + const A&" << endl; a+=b; return std::move(a); }

A&& operator+(A &&a, A &&b)
{ cout << "A&& + A&&" << endl; a+=b; return std::move(a); }

// I assume commutativity
A&& operator+(const A &a, A &&b)
{ cout << "const A& + A&&" << endl; b+=a; return std::move(b); }

A operator+(const A &a, const A &b)
{ cout << "const A& + const A&" << endl; A r(a); r+=b; return r; }

A&& operator*(A &&a, double s)
{ cout << "A&& * s" << endl; a*=s; return std::move(a); }

A operator*(const A& a, double s)
{ cout << "const A& * s" << endl; A r(a); r*=s; return r; }

int main()
{
  A a,b,c,d;
  a = b + a * 4 + d * 2 + (A() + c) * 5;

  return 0;
}

这是(带注释的)输出,其中ts 是创建的临时对象:

                       expression level    actual operations
                       ----------------    -----------------
const A& * s           t1 = a * 4
copy ctor                                  create t1 = copy a
this *= s                                  t1 *= 4
const A& + A&&         b + t1
this += const A&                           t1 += b
const A& * s           t2 = d * 2
copy ctor                                  create t2 = copy d
this *= s                                  t2 *= 2
A&& + A&&              t1 + t2
this += const A&                           t1 += t2
A&& + const A&         A() + c (note: A() is already a temporary)
this += const A&                           A() += c
A&& * s                A'() * 5
this *= s                                  A'() *= 5
A&& + A&&              t1 + A''()
this += const A&                           t1 += A''()
move assign            a = t1              a = t1

我认为你不能期望它比整个表达式只有两个临时性更好。

关于您注释掉的代码:尝试std::move(d)而不是普通代码d,您将保护上述输出中的副本并将d临时对象的数量减少到一个。如果您还添加std::move(a),则整个表达式的计算没有一个临时的

另请注意,如果没有std::move(d)and std::move(a),编译器不知道它应该/可以移动这些对象,因此任何最终移动它们的代码都是危险且完全错误的。


更新:我把我的想法变成了一个库,在GitHub 上找到它。有了这个,你的代码变得很简单:

#include <iostream>
using namespace std;

#include <df/operators.hpp>

struct A : df::commutative_addable< A >, df::multipliable< A, double >
{
  A() = default;
  A(const A& a) { cout << "copy ctor" << endl; }
  A(A&& a) { cout << "move ctor" << endl; }
  A &operator=(const A& a) { cout << "copy assign" << endl; return *this; }
  A &operator=(A&& a) { cout << "move assign" << endl; return *this; }
  A &operator*=(double s) { cout << "this *= s" << endl; return *this; }
  A &operator+=(const A &b) { cout << "this += const A&" << endl; return *this; }
};

同时仍然高效并避免任何不必要的临时工。享受!

于 2013-03-24T11:52:02.580 回答
2

首先A() + c通过左值引用返回。这使得表达式本身成为左值。

如果结果类型是左值引用类型或对函数类型的右值引用,则函数调用是左值,如果结果类型是对对象类型的右值引用,则为 xvalue,否则为纯右值。

左值不能绑定到右值引用,所以operator*选择成员版本。您的非成员函数可能应该按值返回:

A operator+(A &&a, const A &b) { cout << "A&& = A&& + const A& --> "; return a += b; }
A operator*(A &&a, double s) { cout << "A&& = A&& * s --> "; return a *= s; }

这会导致结果继续是引用临时对象的纯右值表达式。

其次,复制构造函数调用是由成员operators按值返回引起的。这将导致对象的副本。例如,当(...) * 5返回时,它将复制*this出函数的值:

A operator*(double s) const { cout << "A = const this * s" << endl; return *this; }
于 2013-03-24T11:24:25.550 回答
1

您的运算符被实现为按值/按左值引用返回。这导致链式操作接受对象副本(因此是复制 ctor)或左值引用。

例如b + a * 4等于b.operator+(a.operator*(4))。的输入operator+将是对象的副本。

于 2013-03-24T11:23:48.840 回答
0

这是您的方法的签名:

struct A
{
    A() = default;
    A(const A& a);
    A(A&& a);
    A &operator=(const A& a);
    A &operator=(A&& a);
    A &operator*=(double s);
    A operator*(double s) const;
    A &operator+=(const A &b);
    A operator+(const A &b) const;
    A &operator+(A &&b) const;
};
A &operator+(A &&a, const A &b);
A &operator*(A &&a, double s);

问题就出现在这里。首先,freeoperator+应该返回一个A&&它传入的值,以避免将右值引用更改为左值。也是如此A &A::operator+(A &&b) const;——它应该返回一个A&&.

接下来,您的自由运算符链接到+=运算符。这是一个可爱的技术:

template<typename T>
A&&operator+(A &&a, T&&b){ return std::move(a+=std::forward<T>(b)); }
template<typename T>
A&&operator*(A &&a, T&&b){ return std::move(a*=std::forward<T>(b)); }

我们将我们的论点蒙蔽到+=操作中。

auto使用返回值技术可以使这更健壮,更容易出错:

template<typename T>
auto operator+(A &&a, T&&b)->declval(std::move(a+=std::forward<T>(b)))
{ return std::move(a+=std::forward<T>(b)); }
template<typename T>
auto operator*(A &&a, T&&b)->declval(std::move(a*=std::forward<T>(b)))
{ return std::move(a*=std::forward<T>(b)); }

使用 SFINAE 在解析堆栈中将错误提高 1 步。(注意&&inT&&A&&有完全不同的含义 -- T&&'s&&用于类型推导上下文中,因此T可以绑定到任何引用类型,而A&&'s&&没有用于类型推导上下文中,因此它意味着A&&绑定到右值。)。

接下来是一个标记更重的版本,为了正确性和效率,做了一些基本的修改。我跟踪该字段中每个实例的历史name——对该字段的操作不是“真实的”,它的值表示创建给定实例所需的“计算”。

我假设移动操作会移动这种状态。

#include <iostream>
#include <utility>

struct A;
A &operator+=(A& a, std::string op);
A&&operator+=(A&& a, std::string op);

struct recurse_nl {
   int& count() {
      static int v = 0;
      return v;
   }
   recurse_nl(){if (++count()>1) std::cout << " --> "; else if (count()>2) std::cout << " --> [";}
   ~recurse_nl(){if (--count() == 0) std::cout <<"\n"; else if (count()>1) std::cout << "]"; }
};
struct A
{
    std::string name;
    A() = delete;
    A(std::string n):name(n) { recurse_nl _; std::cout << "AUTO ctor{"<<name<<"}";};
    A(const A& o):name(o.name+"_c&") { recurse_nl _; std::cout << "COPY ctor{"<<name<<"}(const&)"; }
    A(A&& o):name(std::move(o.name)) { recurse_nl _; std::cout << "ctor{"<<name<<"}(&&)"; }
    A(A& o):name(o.name+"_&") { recurse_nl _; std::cout << "COPY ctor{"<<name<<"}(&)"; }
    A &operator=(const A& rhs) { recurse_nl _; std::cout << "COPY assign{"<<name<<"}={"<<rhs.name<<"}"; this->name = rhs.name; return *this; }
    A &operator=(A&& rhs) { recurse_nl _; std::cout << "move assign{"<<name<<"}={"<<rhs.name<<"}"; this->name = std::move(rhs.name); return *this; }
    A &operator*=(double d) { recurse_nl _; std::cout << "this{"<<name<<"} *= s{"<<d<<"}"; return (*this) += "(*#)"; }
    A operator*(double d) const { recurse_nl _; std::cout << "A = const this{"<<name<<"} * s{"<<d<<"}"; A tmp(*this); return std::move(tmp*=d); }
    A &operator+=(const A &rhs) { recurse_nl _; std::cout << "this{"<<name<<"} += const A&{"<<rhs.name<<"}"; return ((*this)+="(+=")+=rhs.name+")"; }
    A operator+(const A &rhs) const { recurse_nl _; std::cout << "A = const this{"<<name<<"} + const A&{"<<rhs.name<<"}"; return std::move(A(*this)+="(+)"); }
    A&& operator+(A &&rhs) const { recurse_nl _; std::cout << "A&& = const this{"<<name<<"} + A&&{"<<rhs.name<<"}"; return std::move(rhs += *this); }
    ~A() { recurse_nl _; std::cout << "dtor{"<<name<<"}"; }
};

A &operator+=(A& a, std::string op)
{ a.name+=op; return a; }
A&&operator+=(A&& a, std::string op)
{ a.name+=op; return std::move(a); }

template<typename T>
struct ref_type_of {
   std::string value() const { return "value"; }
};
template<typename T>
struct ref_type_of<T&> {
   std::string value() const { return "&"; }
};
template<typename T>
struct ref_type_of<T&&> {
   std::string value() const { return "&&"; }
};
template<typename T>
struct ref_type_of<T const&&> {
   std::string value() const { return " const&&"; }
};
template<typename T>
struct ref_type_of<T const&> {
   std::string value() const { return " const&"; }
};
template<typename T>
std::string ref_type() { return ref_type_of<T>().value(); }

template<typename T>
A&& operator+(A &&a, T&& b) { recurse_nl _; std::cout << "A&&{"<<a.name<<"} = A&&{"<<a.name<<"} + T" << ref_type<T>(); return std::move(a += std::forward<T>(b)); }
template<typename T>
A&& operator*(A &&a, T&& b) { recurse_nl _; std::cout << "A&&{"<<a.name<<"} = A&&{"<<a.name<<"} * T" << ref_type<T>(); return std::move(a *= std::forward<T>(b)); }

void test1()
{
    A a("a"),b("b"),c("c"),d("d");
    a = b + a * 4 + d * 2 + (A("tmp") + c) * 5;
}
int main()
{
    std::cout << "test1\n";
    test1();
    return 0;
}

我在现场工作空间玩过这个,这是输出:

stdout:
test1
AUTO ctor{a}
AUTO ctor{b}
AUTO ctor{c}
AUTO ctor{d}
AUTO ctor{tmp}
A&&{tmp} = A&&{tmp} + T& --> this{tmp} += const A&{c}
A&&{tmp(+=c)} = A&&{tmp(+=c)} * Tvalue --> this{tmp(+=c)} *= s{5}
A = const this{d} * s{2} --> COPY ctor{d_c&}(const&) --> this{d_c&} *= s{2} --> ctor{d_c&(*#)}(&&) --> dtor{}
A = const this{a} * s{4} --> COPY ctor{a_c&}(const&) --> this{a_c&} *= s{4} --> ctor{a_c&(*#)}(&&) --> dtor{}
A&& = const this{b} + A&&{a_c&(*#)} --> this{a_c&(*#)} += const A&{b}
A&&{a_c&(*#)(+=b)} = A&&{a_c&(*#)(+=b)} + Tvalue --> this{a_c&(*#)(+=b)} += const A&{d_c&(*#)}
A&&{a_c&(*#)(+=b)(+=d_c&(*#))} = A&&{a_c&(*#)(+=b)(+=d_c&(*#))} + Tvalue --> this{a_c&(*#)(+=b)(+=d_c&(*#))} += const A&{tmp(+=c)(*#)}
move assign{a}={a_c&(*#)(+=b)(+=d_c&(*#))(+=tmp(+=c)(*#))}
dtor{a}
dtor{d_c&(*#)}
dtor{tmp(+=c)(*#)}
dtor{d}
dtor{c}
dtor{b}
dtor{a_c&(*#)(+=b)(+=d_c&(*#))(+=tmp(+=c)(*#))}

这非常冗长,但几乎演示了每个操作。

我修改了您的代码,以便在需要operator+operator*实际创建一个新对象。代价高昂的操作(创建新对象和复制)通过使用AUTO和突出显示COPY——如您所见,有最初的 4 个字母对象、tmp表达式中的对象和由 . 创建的两个副本operator*(double)

我们可以用这个摆脱一些副本:

a = b + std::move(a) * 4 + std::move(d) * 2 + (A("tmp") + c) * 5;

但是,我们最终还是要销毁 3 个具有非平凡状态的对象,因为我们这样做了两次operator+(A&&, A&&),而且我并不认为这个操作会特别有效。

如果是,我们可以添加这个运算符:

A &operator+=(A &&rhs) { recurse_nl _; std::cout << "this{"<<name<<"} += A&&{"<<rhs.name<<"}"; return ((*this)+="(+=")+=std::move(rhs.name)+")"; }

结果输出表明只有一个具有非平凡状态的对象被销毁。

现场工作区的最终版本在这里

(该recurse_nl对象用于递归跟踪。在基本级别,它在函数末尾打印一个换行符。在更深的递归中,它进行-->打印,理论上如果递归足够深,它会打印[括号以提供帮助)。

最终输出:

test1
AUTO ctor{a}
AUTO ctor{b}
AUTO ctor{c}
AUTO ctor{d}
AUTO ctor{tmp}
A&&{tmp} = A&&{tmp} + T& --> this{tmp} += const A&{c}
A&&{tmp(+=c)} = A&&{tmp(+=c)} * Tvalue --> this{tmp(+=c)} *= s{5}
A&&{d} = A&&{d} * Tvalue --> this{d} *= s{2}
A&&{a} = A&&{a} * Tvalue --> this{a} *= s{4}
A&& = const this{b} + A&&{a(*#)} --> this{a(*#)} += const A&{b}
A&&{a(*#)(+=b)} = A&&{a(*#)(+=b)} + Tvalue --> this{a(*#)(+=b)} += A&&{d(*#)}
A&&{a(*#)(+=b)(+=d(*#))} = A&&{a(*#)(+=b)(+=d(*#))} + Tvalue --> this{a(*#)(+=b)(+=d(*#))} += A&&{tmp(+=c)(*#)}
move assign{a(*#)(+=b)(+=d(*#))(+=tmp(+=c)(*#))}={a(*#)(+=b)(+=d(*#))(+=tmp(+=c)(*#))}
dtor{}
dtor{}
dtor{c}
dtor{b}
dtor{a(*#)(+=b)(+=d(*#))(+=tmp(+=c)(*#))}

您可以在其中看到最后被破坏的单个“复杂对象”(连同其整个历史)。

于 2013-03-24T12:54:48.487 回答