Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is it not efficient to use a single assignment operator handling both copy and move assignment?

Here is an exercise from C++ Primer 5th Edition:

Exercise 13.53: As a matter of low-level efficiency, the HasPtr assignment operator is not ideal. Explain why. Implement a copy-assignment and move-assignment operator for HasPtr and compare the operations executed in your new move-assignment operator versus the copy-and-swap version.(P.544)

File hasptr.h:

//! a class holding a std::string*
class HasPtr
{
    friend void swap(HasPtr&, HasPtr&);
    friend bool operator <(const HasPtr& lhs, const HasPtr& rhs);
public:
    //! default constructor.
    HasPtr(const std::string &s = std::string()):
        ps(new std::string(s)), i(0)
    { }

    //! copy constructor.
    HasPtr(const HasPtr& hp) :
        ps(new std::string(*hp.ps)), i(hp.i)
    { }

    //! move constructor.
    HasPtr(HasPtr&& hp) noexcept :
        ps(hp.ps), i(hp.i)
    { hp.ps = nullptr; }

    //! assignment operator
    HasPtr&
    operator = (HasPtr rhs);

    //! destructor.
    ~HasPtr()
    {
        delete ps;
    }

private:
    std::string *ps;
    int    i;
};

A part of the file hasptr.cpp:

//! specific swap.
inline void
swap(HasPtr &lhs, HasPtr &rhs)
{
    using std::swap;
    swap(lhs.ps, rhs.ps); // swap the pointers, not the string data
    swap(lhs.i, rhs.i);   // swap the int members

    std::cout <<"swapping!\n";
}

//! operator = using specific swap
HasPtr&
HasPtr::operator = (HasPtr rhs)
{
    swap(*this,rhs);
    return *this;
} 

My question is why it is not efficient to do so?

like image 445
Yue Wang Avatar asked Jan 09 '14 02:01

Yue Wang


2 Answers

Step 1

Set up a performance test which exercises the move assignment operator.

Set up another performance test which exercises the copy assignment operator.

Step 2

Set up the assignment operator both ways as instructed in the problem statement.

Step 3

Iterate on Steps 1 and 2 until you have confidence that you did them correctly.

Step 3 should help educate you as to what is going on, most likely by telling you where the performance is changing and where it is not changing.

Guessing is not an option for Steps 1-3. You actually have to do them. Otherwise you will (rightly) have no confidence that your guesses are correct.

Step 4

Now you can start guessing. Some people will call this "forming a hypothesis." Fancy way of saying "guessing." But at least now it is educated guessing.

I ran through this exercise while answering this question and noted no significant performance difference on one test, and a 6X performance difference on the other. This further led me to an hypothesis. After you do this work, if you are unsure of your hypothesis, update your question with your code, results, and subsequent questions.

Clarification

There are two special member assignment operators which typically have the signatures:

HasPtr& operator=(const HasPtr& rhs);  // copy assignment operator
HasPtr& operator=(HasPtr&& rhs);       // move assignment operator

It is possible to implement both move assignment and copy assignment with a single assignment operator with what is called the copy/swap idiom:

HasPtr& operator=(HasPtr rhs);

This single assignment operator can not be overloaded with the first set.

Is it better to implement two assignment operators (copy and move), or just one, using the copy/swap idiom? This is what Exercise 13.53 is asking. To answer, you must try both ways, and measure both copy assignment and move assignment. And smart, well meaning people get this wrong by guessing, instead of testing/measuring. You have picked a good exercise to study.

like image 64
Howard Hinnant Avatar answered Sep 20 '22 13:09

Howard Hinnant


As the problem suggests, it's "a matter of low-level efficiency". When you use HasPtr& operator=(HasPtr rhs) and you write something like hp = std::move(hp2);, ps member is copied twice (the pointer itself not the object to which it points): Once from hp2 to rhs as a result of calling move constructor, and once from rhs to *this as a result of calling swap. But when you use HasPtr& operator=(HasPtr&& rhs), ps is copied just once from rhs to *this.

like image 44
MrDetective Avatar answered Sep 22 '22 13:09

MrDetective