C++ Primer 5th中练习13.53

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.

.h文件:

#ifndef _HASPTR_HH
#define _HASPTR_HH

#include <string>
#include <iostream>

//! 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
    // copy-and-swap
    HasPtr&
    operator= (HasPtr rhs);

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

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

#endif // _HASPTR_HH
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.

.cpp文件

#include "HasPtr.hh"

#include <iostream>

//! 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
}

//! operator = using specific swap
// copy-and-swap
HasPtr&
HasPtr::operator= (HasPtr rhs) {
    swap(*this,rhs);
    return *this;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

测试代码

#include <iostream>
#include <memory>
#include <vector>
#include <string>
#include <algorithm>

#include <set>
#include <chrono>
#include <utility>
#include "String.hh"
#include "HasPtr.hh"

using namespace std;
int main() {
    HasPtr hp1 {"111111111111111234567876548765787656787654567854566543451"}, hp2;

    // 记录开始时间
    auto start = std::chrono::high_resolution_clock::now();
    for(int i = 0; i < 1000000; ++i)
        // copy-and-swap
        // copy constructor + assignment
        // hp2 = hp1; // 执行时间: 0.133841

        // move constructor + assignment
        hp2 = std::move(hp1); // 执行时间: 0.0202316

     // 记录结束时间
    auto end = std::chrono::high_resolution_clock::now();

    // 计算耗时
    std::chrono::duration<double> elapsed = end - start;
    std::cout << "执行时间: " << elapsed.count() << std::endl;
    /*
    g++ -fno-elide-constructors -std=c++11 -Wall -o main main.cc HasPtr.cc
    */

    return 0;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.

实现copy assignmentmove assignment
.h文件:

#ifndef _HASPTR_HH
#define _HASPTR_HH

#include <string>
#include <iostream>

//! 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
    // copy-and-swap
    // HasPtr&
    // operator= (HasPtr rhs);

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

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


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

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

#endif // _HASPTR_HH
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.

.cpp文件

#include "HasPtr.hh"

#include <iostream>

//! 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
}

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

// copy assignment
HasPtr&
HasPtr::operator= (const HasPtr &rhs) {
    std::string *newData = new std::string(*rhs.ps);

    delete ps;
    ps = newData;
    i = rhs.i;
    return *this;
}

// move assignment
HasPtr&
HasPtr::operator= (HasPtr &&rhs) {
    if(this != &rhs) {
        ps = rhs.ps;
        i = rhs.i;

        rhs.ps = nullptr;
    }
    return *this;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.

测试代码:

#include <iostream>
#include <memory>
#include <vector>
#include <string>
#include <algorithm>

#include <set>
#include <chrono>
#include <utility>
#include "String.hh"
#include "HasPtr.hh"

using namespace std;
int main() {
    HasPtr hp1 {"111111111111111234567876548765787656787654567854566543451"}, hp2;

    // 记录开始时间
    auto start = std::chrono::high_resolution_clock::now();
    for(int i = 0; i < 1000000; ++i)
        // copy-and-swap
        // copy constructor + assignment
        // hp2 = hp1;           // 执行时间: 0.133841

        // move constructor + assignment
        // hp2 = std::move(hp1); // 执行时间: 0.0202316

        // copy assignment
        // hp2 = hp1;              // 执行时间: 0.124041
        // move assignment
        hp2 = std::move(hp1); // 执行时间: 0.00637096
     // 记录结束时间
    auto end = std::chrono::high_resolution_clock::now();

    // 计算耗时
    std::chrono::duration<double> elapsed = end - start;
    std::cout << "执行时间: " << elapsed.count() << std::endl;
    /*
    g++ -fno-elide-constructors -std=c++11 -Wall -o main main.cc HasPtr.cc
    */

    return 0;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.

所以总结一下:

copy-and-swap和copy assignment差不多

  1. copy-and-swap和copy assignment差不多,其实copy-and-swap多跑几次的结果是:

[Chap1] : ) ./main
执行时间: 0.140264
[Chap1] : ) ./main
执行时间: 0.141963
[Chap1] : ) ./main
执行时间: 0.130669
[Chap1] : ) ./main
执行时间: 0.142128
[Chap1] : ) ./main
执行时间: 0.133721
[Chap1] : ) ./main
执行时间: 0.13198
[Chap1] : ) ./main
执行时间: 0.136442
[Chap1] : ) ./main
执行时间: 0.13367

  1. 而copy assignment多跑几次的结果是:

[Chap1] : ) ./main
执行时间: 0.130349
[Chap1] : ) ./main
执行时间: 0.120378
[Chap1] : ) ./main
执行时间: 0.122632
[Chap1] : ) ./main
执行时间: 0.121985
[Chap1] : ) ./main
执行时间: 0.124312
[Chap1] : ) ./main
执行时间: 0.122294
[Chap1] : ) ./main
执行时间: 0.123275
[Chap1] : ) ./main
执行时间: 0.123618
[Chap1] : ) ./main
执行时间: 0.123131

  1. 所以copy-assignment比copy-and-swap是客观上的快(如果算法精度有这差距不得水篇文章?)
    根本原因目前我不知,但是我们可以插入一些输出语句来分析一下。首先copy-and-swap它需要调用
    copy constructor来用hp1初始化rhs,然后才执行copy-and-swap的函数体。而copy- assignment直接 执行其函数体。客观上两者都进行了一次copy,前者发生在copy constructor的初始化列表里(ps(new std::string(*hp.ps))),后者发生在copy- assignment的函数体内(std::string *newData = new std::string(*rhs.ps);),然后前者还要在copy-and-swap函数体内执行swap操作(swap(*this,rhs);),但是后者只进行了赋值操作( ps = newData; i = rhs.i;)。难道是因为swap比赋值开销大一些?不过总体性能是差不多的。

copy-and-swap比move assignment肉眼可见的慢

  1. copy-and-swap比move assignment肉眼可见的慢,如果前者多跑几次的话:

[Chap1] : ) ./main
执行时间: 0.0214209
[Chap1] : ) ./main
执行时间: 0.0156422
[Chap1] : ) ./main
执行时间: 0.0196867
[Chap1] : ) ./main
执行时间: 0.0143008
[Chap1] : ) ./main
执行时间: 0.0143276
[Chap1] : ) ./main
执行时间: 0.0190589
[Chap1] : ) ./main
执行时间: 0.014424
[Chap1] : ) ./main
执行时间: 0.0197936
[Chap1] : ) ./main
执行时间: 0.0188477

  1. ,而move assignment多跑几次的话:

[Chap1] : ) ./main
执行时间: 0.00741979
[Chap1] : ) ./main
执行时间: 0.00479204
[Chap1] : ) ./main
执行时间: 0.00680325
[Chap1] : ) ./main
执行时间: 0.00676896
[Chap1] : ) ./main
执行时间: 0.00675975
[Chap1] : ) ./main
执行时间: 0.00691304
[Chap1] : ) ./main
执行时间: 0.00686121
[Chap1] : ) ./main
执行时间: 0.00430117

  1. 所以这里的差距是哪里造成的呢?首先copy-and-swap需要调用move constructor,然后才会执行其函数体内的swap(*this,rhs);。有趣的是, move assignment做的就是move constructor的事情,所以前者硬生生比后者多执行了一个swap(*this,rhs);~~。

以上都是根据结果进行的猜测,真实原因不知。

参考

 https://stackoverflow.com/questions/21010371/why-is-it-not-efficient-to-use-a-single-assignment-operator-handling-both-copy-a