有关移动赋值运算符的问题 [英] Questions about the move assignment operator

查看:171
本文介绍了有关移动赋值运算符的问题的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

想象以下管理资源的类(我的问题只是关于移动赋值运算符):

  struct A 
{
std :: size_t s;
int * p;
A(std :: size_t s):s(s),p(new int [s]){}
〜A(){delete [] p;}
A const& other):s(other.s),p(new int [other.s])
{std :: copy(other.p,other.p + s,this-> p);}
A(A&&其他):s(other.s),p(other.p)
{other.s = 0; other.p = nullptr;}
A& operator =(A const& other)
{A temp = other; std :: swap(* this,temp); return * this;}
//移动赋值运算符#1
A& operator =(A&& other)
{
std :: swap(this-> s,other.s);
std :: swap(this-> p,other.p);
return * this;
}
//移动赋值运算符#2
A& operator =(A&& other)
{
delete [] p;
s = other.s;
p = other.p;
other.s = 0;
other.p = nullptr;
return * this;
}
};

问题:



和上面的两个移动赋值运算符#1和#2的缺点?我相信我可以看到的唯一区别是, std :: swap 保存lhs的存储,但是,我不知道如何将有用作为右值反正毁了。也许唯一的时间将是像 a1 = std :: move(a2); ,但即使在这种情况下,我没有看到任何理由使用#1。

解决方案

这是一个你应该测量的情况。



我正在查看OP的复制赋值运算符,并看到效率低下:

  operator =(A const& other)
{A temp = other; std :: swap(* this,temp); return * this;}

如果 * this 其他有相同的 s



似乎我更聪明的副本分配可以避免旅行到堆如果 s == other.s 。所有它需要做的是副本:

  A& operator =(A const& other)
{
if(this!=& other)
{
if(s!= other.s)
{
delete [] p;
p = nullptr;
s = 0;
p = new int [other.s];
s = other.s;
}
std :: copy(other.p,other.p + s,this-> p);
}
return * this;
}

如果您不需要 ,只有拷贝分配上的基本异常安全(如 std :: string std :: vector 等)那么存在上述潜在的性能改进。多少?

设计1:



使用上述副本赋值运算符和OP的移动赋值运算符#1。



2:



使用上述副本分配运算符和OP的移动分配运算符#2。



设计3:



DeadMG的副本和移动分配的副本分配操作符。



这里是我用来测试的代码:

  #include< cstddef> 
#include< algorithm>
#include< chrono>
#include< iostream>

struct A
{
std :: size_t s;
int * p;
A(std :: size_t s):s(s),p(new int [s]){}
〜A(){delete [] p;}
A const& other):s(other.s),p(new int [other.s])
{std :: copy(other.p,other.p + s,this-> p);}
A(A&&其他):s(other.s),p(other.p)
{other.s = 0; other.p = nullptr;}
void swap(A& other)
{std :: swap(s,other.s); std :: swap(p,other.p);}
#if DESIGN!= 3
A& operator =(A const& other)
{
if(this!=& other)
{
if(s!= other.s)
{
delete [] p;
p = nullptr;
s = 0;
p = new int [other.s];
s = other.s;
}
std :: copy(other.p,other.p + s,this-> p);
}
return * this;
}
#endif
#if DESIGN == 1
//移动赋值运算符#1
A& operator =(A&& other)
{
swap(other);
return * this;
}
#elif DESIGN == 2
//移动赋值运算符#2
A& operator =(A&& other)
{
delete [] p;
s = other.s;
p = other.p;
other.s = 0;
other.p = nullptr;
return * this;
}
#elif DESIGN == 3
A& operator =(A other)
{
swap(other);
return * this;
}
#endif
};

int main()
{
typedef std :: chrono :: high_resolution_clock时钟;
typedef std :: chrono :: duration< float,std :: nano> NS;
A a1(10);
A a2(10);
auto t0 = Clock :: now();
a2 = a1;
auto t1 = Clock :: now();
std :: cout<< copy takes<< NS(t1-t0).count() ns\\\
;
t0 = Clock :: now();
a2 = std :: move(a1);
t1 = Clock :: now();
std :: cout<< move takes<< NS(t1-t0).count() ns\\\
;
}

这是我得到的输出:

  $ clang ++ -std = c ++ 11 -stdlib = libc ++ -O3 -DDESIGN = 1 test.cpp 
$ a.out
copy需要55ns
移动需要44ns
$ a.out
复制需要56ns
移动需要24ns
$ a.out
复制需要53ns
move takes 25ns
$ clang ++ -std = c ++ 11 -stdlib = libc ++ -O3 -DDESIGN = 2 test.cpp
$ a.out
复制需要74ns
move take 538ns
$ a.out
复制需要59ns
移动需要491ns
$ a.out
复制需要61ns
移动需要510ns
$ clang ++ -std = c ++ 11 -stdlib = libc ++ -O3 -DDESIGN = 3 test.cpp
$ a.out
复制需要666ns
移动需要304ns
$ a。 out
复制需要603ns
移动需要446ns
$ a.out
复制需要619ns
移动需要317ns

DESIGN 1 看起来不错。



Caveat:如果类具有需要快速释放的资源,例如互斥锁所有权或文件开放状态所有权,则从正确性的角度来看,设计-2移动赋值运算符可能更好。但是当资源只是内存时,通常有利的是尽可能地延迟释放它(如在OP的用例中)。



注意:如果你有其他使用案例,你知道是重要的,测量他们。您可能会得到与我在这里不同的结论。



注意:我的性能优于DRY。这里的所有代码将被封装在一个类( struct A )中。使 struct A 尽可能好。如果你做一个足够高质量的工作,那么你的客户 struct A (可能是你自己)不会被诱惑RIA(再次发明)。我更喜欢在一个类中重复一个小代码,而不是一遍又一遍重复整个类的实现。


Imagine the following class that manages a resource (my question is only about the move assignment operator):

struct A
{
    std::size_t s;
    int* p;
    A(std::size_t s) : s(s), p(new int[s]){}
    ~A(){delete [] p;}
    A(A const& other) : s(other.s), p(new int[other.s])
    {std::copy(other.p, other.p + s, this->p);}
    A(A&& other) : s(other.s), p(other.p)
    {other.s = 0; other.p = nullptr;}
    A& operator=(A const& other)
    {A temp = other; std::swap(*this, temp); return *this;}
    // Move assignment operator #1
    A& operator=(A&& other)
    {
        std::swap(this->s, other.s);
        std::swap(this->p, other.p);
        return *this;
    }
    // Move assignment operator #2
    A& operator=(A&& other)
    {
        delete [] p;
        s = other.s;
        p = other.p;
        other.s = 0;
        other.p = nullptr;
        return *this;
     } 
};

Question:

What are the advantages and disadvantages of the two move assignment operators #1 and #2 above? I believe the only difference I can see is that std::swap preserves the storage of the lhs, however, I don't see how that would be useful as rvalues would be destroyed anyways. Maybe the only time would be with something like a1 = std::move(a2);, but even in this case I don't see any reason to use #1.

解决方案

This is a case where you should really measure.

And I'm looking at the OP's copy assignment operator and seeing inefficiency:

A& operator=(A const& other)
    {A temp = other; std::swap(*this, temp); return *this;}

What if *this and other have the same s?

It seems to me that a smarter copy assignment could avoid making a trip to the heap if s == other.s. All it would have to do is the copy:

A& operator=(A const& other)
{
    if (this != &other)
    {
        if (s != other.s)
        {
            delete [] p;
            p = nullptr;
            s = 0;
            p = new int[other.s];
            s = other.s;
        }
        std::copy(other.p, other.p + s, this->p);
    }
    return *this;
}

If you don't need strong exception safety, only basic exception safety on copy assignment (like std::string, std::vector, etc.), then there is a potential performance improvement with the above. How much? Measure.

I've coded this class three ways:

Design 1:

Use the above copy assignment operator and the OP's move assignment operator #1.

Design 2:

Use the above copy assignment operator and the OP's move assignment operator #2.

Design 3:

DeadMG's copy assignment operator for both copy and move assignment.

Here is the code I used to test:

#include <cstddef>
#include <algorithm>
#include <chrono>
#include <iostream>

struct A
{
    std::size_t s;
    int* p;
    A(std::size_t s) : s(s), p(new int[s]){}
    ~A(){delete [] p;}
    A(A const& other) : s(other.s), p(new int[other.s])
    {std::copy(other.p, other.p + s, this->p);}
    A(A&& other) : s(other.s), p(other.p)
    {other.s = 0; other.p = nullptr;}
    void swap(A& other)
    {std::swap(s, other.s); std::swap(p, other.p);}
#if DESIGN != 3
    A& operator=(A const& other)
    {
        if (this != &other)
        {
            if (s != other.s)
            {
                delete [] p;
                p = nullptr;
                s = 0;
                p = new int[other.s];
                s = other.s;
            }
            std::copy(other.p, other.p + s, this->p);
        }
        return *this;
    }
#endif
#if DESIGN == 1
    // Move assignment operator #1
    A& operator=(A&& other)
    {
        swap(other);
        return *this;
    }
#elif DESIGN == 2
    // Move assignment operator #2
    A& operator=(A&& other)
    {
        delete [] p;
        s = other.s;
        p = other.p;
        other.s = 0;
        other.p = nullptr;
        return *this;
     } 
#elif DESIGN == 3
    A& operator=(A other)
    {
        swap(other);
        return *this;
    }
#endif
};

int main()
{
    typedef std::chrono::high_resolution_clock Clock;
    typedef std::chrono::duration<float, std::nano> NS;
    A a1(10);
    A a2(10);
    auto t0 = Clock::now();
    a2 = a1;
    auto t1 = Clock::now();
    std::cout << "copy takes " << NS(t1-t0).count() << "ns\n";
    t0 = Clock::now();
    a2 = std::move(a1);
    t1 = Clock::now();
    std::cout << "move takes " << NS(t1-t0).count() << "ns\n";
}

Here is the output I got:

$ clang++ -std=c++11 -stdlib=libc++ -O3 -DDESIGN=1  test.cpp 
$ a.out
copy takes 55ns
move takes 44ns
$ a.out
copy takes 56ns
move takes 24ns
$ a.out
copy takes 53ns
move takes 25ns
$ clang++ -std=c++11 -stdlib=libc++ -O3 -DDESIGN=2  test.cpp 
$ a.out
copy takes 74ns
move takes 538ns
$ a.out
copy takes 59ns
move takes 491ns
$ a.out
copy takes 61ns
move takes 510ns
$ clang++ -std=c++11 -stdlib=libc++ -O3 -DDESIGN=3  test.cpp 
$ a.out
copy takes 666ns
move takes 304ns
$ a.out
copy takes 603ns
move takes 446ns
$ a.out
copy takes 619ns
move takes 317ns

DESIGN 1 looks pretty good to me.

Caveat: If the class has resources that need to be deallocated "quickly", such as mutex lock ownership or file open-state ownership, the design-2 move assignment operator could be better from a correctness point of view. But when the resource is simply memory, it is often advantageous to delay deallocating it as long as possible (as in the OP's use case).

Caveat 2: If you have other use cases you know to be important, measure them. You might come to different conclusions than I have here.

Note: I value performance over "DRY". All of the code here is going to be encapsulated within one class (struct A). Make struct A as good as you can. And if you do a sufficiently high quality job, then your clients of struct A (which may be yourself) won't be tempted to "RIA" (Reinvent It Again). I much prefer to repeat a little code within one class, rather than repeat the implementation of entire classes over and over again.

这篇关于有关移动赋值运算符的问题的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆