1 不可变的类

1.1 不可变的好处

  • 不可变对象默认是线程安全的,不会遇到同步问题。不变性使得设计实现并行软件更加容易。
  • 不变性使编写、使用和理解代码更加容易。一组必须始终为真的约束,在对象创建的时候就可以建立,并且在对象整个生命周期不会改变。

    1.2 如何创建不变类

    要创建C++的不变类,需要满足以下条件:
  1. 类的成员变量都必须使不可变的,const变量,只能在构造函数中初始化一次。
  2. 操作方法不能修改调用者的状态,而是返回状态改变后的新对象实例(回想一下string的成员函数返回值)
  3. 类定义必须是final的,不允许被继承。一位内继承可能改变基类的不可变性。

代码示例:

  1. #include "Identifier.h"
  2. #include "Money.h"
  3. #include <string>
  4. #include <string_view>
  5. class Employee final
  6. {
  7. public:
  8. Employee(std::string_view forename,
  9. std::string_view surname,
  10. const Identifier& staffNumber,
  11. const Money& salary) noexcept :
  12. forename{ forename },
  13. surname{ surname },
  14. staffNumber{ staffNumber },
  15. salary{ salary } { }
  16. Identifier getStaffNumber() const noexcept {
  17. return staffNumber;
  18. }
  19. Money getSalary() const noexcept {
  20. return salary;
  21. }
  22. Employee changeSalary(const Money& newSalary) const noexcept {
  23. return Employee(forename, surname, staffNumber, newSalary);
  24. }
  25. private:
  26. const std::string forename;
  27. const std::string surname;
  28. const Identifier staffNumber;
  29. const Money salary;
  30. };

2 SFINAE原则

在C++模板实例化错误的情况下(比如使用错误的模板参数),错误信息可能非常冗长并且含糊不清。
SFINAE是一种编程技术(Substitution Failure Is Not An Error),在模板参数匹配失败时不会产生烦人的编译错误,而是继续搜索合适的模板。

看下面的例子:
因为在编译时,编译器无法知道T的具体类型,更不会知道T::multiplication_result,所以编译器会根据SDINAE原则,自动选择了long multiply函数。

  1. #include <iostream>
  2. long multiply(int i, int j)
  3. {
  4. std::cout << "long multiply" << std::endl;
  5. return i * j;
  6. }
  7. template <class T>
  8. typename T::multiplication_result multiply(T t1, T t2)
  9. {
  10. std::cout << "template multiply" << std::endl;
  11. return t1 * t2;
  12. }
  13. int main(void)
  14. {
  15. multiply(4, 5);//-------------最终选择的是long multiply函数
  16. return 0;
  17. }

我们利用C++11 Type Traits库的std::enable_if()函数,可以方便的根据T的类型,从候选的模板函数中有条件的筛选函数。
举例如下:

  1. #include <iostream>
  2. #include <type_traits>
  3. //使用enable_if分别实现四个模板函数,对应输入类型enum, int, float, class
  4. template <typename T>
  5. void print(T var, typename std::enable_if<std::is_enum<T>::value, T>::type* = 0) {
  6. std::cout << "Calling overloaded print() for enumerations." << std::endl;
  7. }
  8. template <typename T>
  9. void print(T var, typename std::enable_if<std::is_integral<T>::value, T>::type = 0) {
  10. std::cout << "Calling overloaded print() for integral types." << std::endl;
  11. }
  12. template <typename T>
  13. void print(T var, typename std::enable_if<std::is_floating_point<T>::value, T>::type = 0) {
  14. std::cout << "Calling overloaded print() for floating point types." << std::endl;
  15. }
  16. template <typename T>
  17. void print(const T& var, typename std::enable_if<std::is_class<T>::value, T>::type* = 0) {
  18. std::cout << "Calling overloaded print() for classes." << std::endl;
  19. }
  20. //定义一些类型
  21. enum Enumeration1 {
  22. Literal1,
  23. Literal2
  24. };
  25. enum class Enumeration2 : int {
  26. Literal1,
  27. Literal2
  28. };
  29. class Clazz { };
  30. int main() {
  31. Enumeration1 enumVar1{ };
  32. print(enumVar1);//---------------调用enum模板
  33. Enumeration2 enumVar2{ };
  34. print(enumVar2);//---------------调用enum模板
  35. print(42);//---------------调用int模板
  36. Clazz instance{ };
  37. print(instance);//---------------调用class模板
  38. print(42.0f);//---------------调用float模板
  39. return 0;
  40. }

3 Copy/Swap实现类赋值运算符

看下面的例子,一个最简单的类的拷贝构造函数和赋值运算符实现:

  1. #pragma once
  2. #include <cstddef>
  3. #include <algorithm>
  4. class Clazz final {
  5. public:
  6. Clazz(const std::size_t size) : resourceToManage{ new char[size] }, size{ size } { }
  7. ~Clazz() {
  8. delete[] resourceToManage;
  9. }
  10. //拷贝构造函数
  11. Clazz(const Clazz& other) : Clazz{ other.size } {
  12. std::copy(other.resourceToManage, other.resourceToManage + other.size, resourceToManage);
  13. }
  14. //赋值运算符
  15. Clazz& operator=(const Clazz& other) {
  16. if (&other == this)
  17. {
  18. return *this;
  19. }
  20. delete[] resourceToManage;
  21. resourceToManage = new char[other.size];
  22. std::copy(other.resourceToManage, other.resourceToManage + other.size, resourceToManage);
  23. size = other.size;
  24. return *this;
  25. }
  26. private:
  27. char* resourceToManage;
  28. std::size_t size;
  29. };

上面的赋值运算符是我们一般学到的标准写法,但是这样存在一些问题:

  • 开头有一个自我分配的check
  • new和delete的代码与构造函数和析构函数重复了
  • new语句可能出现异常,会导致对象处于不可预知的状态

所以我们可以用Copy/Swap改善上面的问题,修改如下:

  1. class Clazz final {
  2. public:
  3. //......
  4. Clazz& operator=(Clazz other) {//值传递,不是引用,这里会发生copy
  5. swap(other); //自定义的swap函数
  6. return *this;
  7. }
  8. private:
  9. void swap(Clazz& other) noexcept {
  10. using std::swap;
  11. //直接交换变量值
  12. swap(resourceToManage, other.resourceToManage);
  13. swap(size, other.size);
  14. }
  15. char* resourceToManage;
  16. std::size_t size;
  17. };

修改的诀窍是:

  • operator=的参数从引用变为值传递,这样当调用赋值符号时,会先调用拷贝构造函数,重新new一个other副本(正好把之前代码中的new操作替换)。
  • 在私有函数中调用std::swap交换other副本的内容到this对象中。

从这两步改变我们可以看到,无论是复制创建类对象还是调用std::swap不会触发异常,比之前代码更加安全。

4 指向实现的的类内指针

看下面示例,假设customer.h头文件被许多其他的类使用(通过include),那么当该头文件更改(即使时重命名一个变量这样的修改)时,所有include的文件都需要重新编译。

  1. #ifndef CUSTOMER_H_
  2. #define CUSTOMER_H_
  3. #include "Address.h"
  4. #include "Identifier.h"
  5. #include <string>
  6. class Customer {
  7. public:
  8. Customer();
  9. virtual ~Customer() = default;
  10. std::string getFullName() const;
  11. void setShippingAddress(const Address& address);
  12. // ...
  13. private:
  14. Identifier customerId;
  15. std::string forename;
  16. std::string surname;
  17. Address shippingAddress;
  18. };

为了减少重新编译的文件个数,可以使用PIMPL用法,即指向实现的指针(Pointer to Implementation)。
我们可以把上面代码修改成:

  1. #ifndef CUSTOMER_H_
  2. #define CUSTOMER_H_
  3. #include <memory>
  4. #include <string>
  5. class Address;
  6. class Customer {
  7. public:
  8. Customer();
  9. virtual ~Customer();
  10. std::string getFullName() const;
  11. void setShippingAddress(const Address& address);
  12. // ...
  13. private:
  14. class Impl; //内部类,在h文件中并没有定义
  15. std::unique_ptr<Impl> impl; //指向内部类的指针,即PIMPL
  16. };

我们定义了一个内部类Impl,而且在头文件中没有给出定义,我们在cpp文件中定义它,并且将所有具体操作都放在Impl中,Cunstomer类的成员函数都只是调用Impl的方法:

  1. #include "Customer.h"
  2. #include "Address.h"
  3. #include "Identifier.h"
  4. class Customer::Impl final { //Impl类的定义
  5. public:
  6. std::string getFullName() const;
  7. void setShippingAddress(const Address& address);
  8. private:
  9. Identifier customerId;
  10. std::string forename;
  11. std::string surname;
  12. Address shippingAddress;
  13. };
  14. std::string Customer::Impl::getFullName() const {
  15. return forename + " " + surname;
  16. }
  17. void Customer::Impl::setShippingAddress(const Address& address) {
  18. shippingAddress = address;
  19. }
  20. //构造函数,利用上面刚定义的Impl类初始化impl成员变量
  21. Customer::Customer() : impl{ std::make_unique<Customer::Impl>() } { }
  22. Customer::~Customer() = default;
  23. std::string Customer::getFullName() const {
  24. return impl->getFullName();//调用Impl的成员函数
  25. }
  26. void Customer::setShippingAddress(const Address & address) {
  27. impl->setShippingAddress(address);//调用Impl的成员函数
  28. }

这样设计后,如果我们要修改Customer的实现,我们只需要修改cpp文件中的Impl内即可,编译时只需要重新编译cpp文件。