Item17 Understand special member function generation

   Effective C++中曾经提到了Rule of Three 意思就是如果你需要声明拷贝构造函数,拷贝赋值操作符和析构函数三者中的任何一个(原因参见Rule of three),那么你应该三者都声明,Inside the C++ Object Model中提到了编译器会在什么情况下帮我们生成一些默认的函数。学习这些都是为了更好的掌握C++,清楚的了解C++在什么情况下会帮我们做什么样的事情。本文在C++11的基础上讨论编译器何时会帮我们生成一些特殊的成员函数。

​   在进入正文之前先来谈论下,编译器都会帮我们生成哪些特殊的成员函数,在C++98的时候会帮我们生成默认构造函数,拷贝构造函数,拷贝赋值操作符,还有析构函数,并且都是隐式的public,和inline。那么这些函数在什么情况下会生成呢?,有的书和资料中说默认都会生成,其实这种说法是不严谨的,Inside the C++ Object Model中说到,这些特殊的成员函数只有在需要的时候才会生成,那么什么才算是需要呢?,又是谁需要呢? 准确来说应该是当编译器需要的时候才会帮我们生成这些特殊的成员函数,下面四种情况下编译器才需要生成默认的构造函数:

  • class没有任何的constructor,但它内含member object,而后者有默认的构造函数。
  • class没有任何的constructor,但是它派生自一个带有默认构造函数的基类。
  • 带有虚函数的类
  • 继承自带有虚函数的基类

   只有在满足上述情况下,编译期才会帮我们生成默认的构造函数,帮我们调用成员变量的构造函数进行初始化,或者是创建虚函数表,调用基类的构造函数,初始化基类等工作。

更多内容可以看Inside the C++ Object Model

class simple {
  private:
    int data;
};

​   上面这个simple类编译器就不会帮其创建默认的构造函数。当做基本类型来看待,赋值拷贝的时候直接是bitwise copy(位拷贝,当只有基本数据类型的时候bitwise copymember copy的效果是一样的)。到了C++11又额外的添加了两个特殊的成员函数,一个是移动构造函数,另外一个则是移动赋值操作符。其函数声明如下:

bitwise copy 是将类所在内存处,进行整个拷贝

memberwise copy 则是对类的每一个成员逐个调用拷贝构造函数进行拷贝

在网络上经常会看到有人将上面两个词的含义理解成深拷贝和浅拷贝,这是不严谨的。

class Widget {
  public:
    .....
    Widget(Widget&& rhs);
    Widget& operator=(widget&& rhs);
};

​   移动构造函数的生成规则类似于拷贝构造函数,仅仅当编译器需要的时候才会生成,要求其每一个非static的成员都具有 移动语义。实际上当我们对一个类施行移动构造的时候,它并不保证一定是移动构造,因为这个类并不一定具备移动语义,那么这个时候会使用拷贝构造代替,如下

class test {
 public:
  test() {
    p = new char('a');
  }

  test(const test& other) {
    std::cout << "copy construct" << std::endl;
  }

 private:
  char *p = nullptr;
};

int main() {

  test t;
  test c(std::move(t));
  return 0;
}

​   上面的代码会输出copy construct,因为没有生成默认的移动构造函数,那么究竟是什么原因导致没有生成默认的移动构造函数呢?,上文中说到当类的非static成员都具备移动语义的时候才会生成默认移动构造函数,如下:

class test {
 public:
  test() {
    str = "test";
  }
  std::string str;
};

int main() {

  test t;
  test c(std::move(t));
  std::cout << t.str << std::endl;
  std::cout << c.str << std::endl;

  return 0;
}

​   上面的代码中会输出一行空,还有一行test,这是因为c使用了默认的移动构造进行构造的(因为类的成员是string类型,这是一个具备移动语义的类,所以会生成默认的移动构造函数),这导致t的内容被移动到了c,所以t的内容就是空的了。如果给上面的代码加上一个拷贝构造函数,那么结果又会怎样呢?

  test(const test& other) {
    std::cout << "copy construct" << std::endl;
  }

输出结果如下:
copy construct
dewd
    (空白)

​   很奇怪居然会调用自定义的拷贝构造函数,这说明此时并没有生成默认的移动构造函数,这就是两者之间产生了相互影响,至于具体是如何影响的,以及这两个构造函数的之间的关系如何,见下文。

​   通过上文可知,当移动构造函数和拷贝构造函数在一起的时候,它们之间会产生相互影响,这个话题就是我们最后要来谈论。

​   六个特殊的成员函数之间的关系到底是如何的呢?,对于拷贝构造函数和赋值操作符来说,这两者相互独立不会产生影响。对于移动构造函数和移动赋值操作符来说,这两者也是相互独立的,而当用户显示的声明了拷贝操作(无论是拷贝构造函数和拷贝赋值操作符),这会导致编译期不会生成默认的移动操作,因为如果这个类不适合memberwise copy(自定义了拷贝操作是因为默认的浅拷贝不适合,所以通常来说自定义的操作拷贝操作时深拷贝)操作的话,那么同样默认移动操作可能也不适合。还有用户自定义析构操作的话也会导致默认的移动操作不会生成。同理当用户自定义了移动操作,那么默认的拷贝操作也不会生成。总的来说,只有当满足下面几个条件的时候才会生成默认的移动操作:

  • 没有用户自定义的拷贝操作
  • 没有用户自定义的移动操作
  • 没有用户自定义的析构操作

   这些特殊的成员函数之间的关系,让类的含义变的模糊不清,当你自定义了一个拷贝构造函数,你却丢失了默认的移动操作,借助于C++11default可以让类的含义变的更清楚。

class test {
 public:
  test() {
    str = "test";
  }
  test(const test& other) {
    std::cout << "copy construct" << std::endl;
  }
  test(test&&) = default; //显示的声明默认的移动构造函数
  test& operator=(test&&)  = default;
  std::string str;
};

   通过显示的声明这些特殊的默认函数,这样让类的含义更加明确。最后总结下上面提到的六个特殊的成员函数其生成规则:

  • 默认构造函数,生成规则和C++98一样,在用户没有声明自定义的构造函数的时候并且编译期需要的时候生成。
  • 析构函数,生成规则和C++98一样,在C++11中有点不同的是,析构函数默认是noexcept
  • 拷贝构造函数,用户自定义了移动操作会导致不生成默认的拷贝构造函数,其它和C++98的行为一致。
  • 拷贝赋值操作符,用户自定义了移动操作会导致不生成默认的拷贝赋值操作,其它和C++98的行为一致。
  • 移动构造函数和移动赋值操作符,仅仅在没有用户自定义的拷贝操作,移动操作和析构操作的时候才会生成。

注意,上面这些规则并不适用于通过模版生成构造函数的场景,如下:

class Widget {
  template<typename T>
  Widget(const T& rhs);

  template<typename T>
  Widget& operator=(const T& rhs);
}

上面这种情况下,编译期仍然会生成默认的移动操作,也就是说模版成员函数,并不会抑制特殊成员函数的生成。

©️2020 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值