1 不可变的类
1.1 不可变的好处
- 不可变对象默认是线程安全的,不会遇到同步问题。不变性使得设计实现并行软件更加容易。
- 不变性使编写、使用和理解代码更加容易。一组必须始终为真的约束,在对象创建的时候就可以建立,并且在对象整个生命周期不会改变。
1.2 如何创建不变类
要创建C++的不变类,需要满足以下条件:
- 类的成员变量都必须使不可变的,const变量,只能在构造函数中初始化一次。
- 操作方法不能修改调用者的状态,而是返回状态改变后的新对象实例(回想一下string的成员函数返回值)
- 类定义必须是final的,不允许被继承。一位内继承可能改变基类的不可变性。
代码示例:
#include "Identifier.h"#include "Money.h"#include <string>#include <string_view>class Employee final{public:Employee(std::string_view forename,std::string_view surname,const Identifier& staffNumber,const Money& salary) noexcept :forename{ forename },surname{ surname },staffNumber{ staffNumber },salary{ salary } { }Identifier getStaffNumber() const noexcept {return staffNumber;}Money getSalary() const noexcept {return salary;}Employee changeSalary(const Money& newSalary) const noexcept {return Employee(forename, surname, staffNumber, newSalary);}private:const std::string forename;const std::string surname;const Identifier staffNumber;const Money salary;};
2 SFINAE原则
在C++模板实例化错误的情况下(比如使用错误的模板参数),错误信息可能非常冗长并且含糊不清。
SFINAE是一种编程技术(Substitution Failure Is Not An Error),在模板参数匹配失败时不会产生烦人的编译错误,而是继续搜索合适的模板。
看下面的例子:
因为在编译时,编译器无法知道T的具体类型,更不会知道T::multiplication_result,所以编译器会根据SDINAE原则,自动选择了long multiply函数。
#include <iostream>long multiply(int i, int j){std::cout << "long multiply" << std::endl;return i * j;}template <class T>typename T::multiplication_result multiply(T t1, T t2){std::cout << "template multiply" << std::endl;return t1 * t2;}int main(void){multiply(4, 5);//-------------最终选择的是long multiply函数return 0;}
我们利用C++11 Type Traits库的std::enable_if()函数,可以方便的根据T的类型,从候选的模板函数中有条件的筛选函数。
举例如下:
#include <iostream>#include <type_traits>//使用enable_if分别实现四个模板函数,对应输入类型enum, int, float, classtemplate <typename T>void print(T var, typename std::enable_if<std::is_enum<T>::value, T>::type* = 0) {std::cout << "Calling overloaded print() for enumerations." << std::endl;}template <typename T>void print(T var, typename std::enable_if<std::is_integral<T>::value, T>::type = 0) {std::cout << "Calling overloaded print() for integral types." << std::endl;}template <typename T>void print(T var, typename std::enable_if<std::is_floating_point<T>::value, T>::type = 0) {std::cout << "Calling overloaded print() for floating point types." << std::endl;}template <typename T>void print(const T& var, typename std::enable_if<std::is_class<T>::value, T>::type* = 0) {std::cout << "Calling overloaded print() for classes." << std::endl;}//定义一些类型enum Enumeration1 {Literal1,Literal2};enum class Enumeration2 : int {Literal1,Literal2};class Clazz { };int main() {Enumeration1 enumVar1{ };print(enumVar1);//---------------调用enum模板Enumeration2 enumVar2{ };print(enumVar2);//---------------调用enum模板print(42);//---------------调用int模板Clazz instance{ };print(instance);//---------------调用class模板print(42.0f);//---------------调用float模板return 0;}
3 Copy/Swap实现类赋值运算符
看下面的例子,一个最简单的类的拷贝构造函数和赋值运算符实现:
#pragma once#include <cstddef>#include <algorithm>class Clazz final {public:Clazz(const std::size_t size) : resourceToManage{ new char[size] }, size{ size } { }~Clazz() {delete[] resourceToManage;}//拷贝构造函数Clazz(const Clazz& other) : Clazz{ other.size } {std::copy(other.resourceToManage, other.resourceToManage + other.size, resourceToManage);}//赋值运算符Clazz& operator=(const Clazz& other) {if (&other == this){return *this;}delete[] resourceToManage;resourceToManage = new char[other.size];std::copy(other.resourceToManage, other.resourceToManage + other.size, resourceToManage);size = other.size;return *this;}private:char* resourceToManage;std::size_t size;};
上面的赋值运算符是我们一般学到的标准写法,但是这样存在一些问题:
- 开头有一个自我分配的check
- new和delete的代码与构造函数和析构函数重复了
- new语句可能出现异常,会导致对象处于不可预知的状态
所以我们可以用Copy/Swap改善上面的问题,修改如下:
class Clazz final {public://......Clazz& operator=(Clazz other) {//值传递,不是引用,这里会发生copyswap(other); //自定义的swap函数return *this;}private:void swap(Clazz& other) noexcept {using std::swap;//直接交换变量值swap(resourceToManage, other.resourceToManage);swap(size, other.size);}char* resourceToManage;std::size_t size;};
修改的诀窍是:
- 将
operator=的参数从引用变为值传递,这样当调用赋值符号时,会先调用拷贝构造函数,重新new一个other副本(正好把之前代码中的new操作替换)。 - 在私有函数中调用
std::swap交换other副本的内容到this对象中。
从这两步改变我们可以看到,无论是复制创建类对象还是调用std::swap都不会触发异常,比之前代码更加安全。
4 指向实现的的类内指针
看下面示例,假设customer.h头文件被许多其他的类使用(通过include),那么当该头文件更改(即使时重命名一个变量这样的修改)时,所有include的文件都需要重新编译。
#ifndef CUSTOMER_H_#define CUSTOMER_H_#include "Address.h"#include "Identifier.h"#include <string>class Customer {public:Customer();virtual ~Customer() = default;std::string getFullName() const;void setShippingAddress(const Address& address);// ...private:Identifier customerId;std::string forename;std::string surname;Address shippingAddress;};
为了减少重新编译的文件个数,可以使用PIMPL用法,即指向实现的指针(Pointer to Implementation)。
我们可以把上面代码修改成:
#ifndef CUSTOMER_H_#define CUSTOMER_H_#include <memory>#include <string>class Address;class Customer {public:Customer();virtual ~Customer();std::string getFullName() const;void setShippingAddress(const Address& address);// ...private:class Impl; //内部类,在h文件中并没有定义std::unique_ptr<Impl> impl; //指向内部类的指针,即PIMPL};
我们定义了一个内部类Impl,而且在头文件中没有给出定义,我们在cpp文件中定义它,并且将所有具体操作都放在Impl中,Cunstomer类的成员函数都只是调用Impl的方法:
#include "Customer.h"#include "Address.h"#include "Identifier.h"class Customer::Impl final { //Impl类的定义public:std::string getFullName() const;void setShippingAddress(const Address& address);private:Identifier customerId;std::string forename;std::string surname;Address shippingAddress;};std::string Customer::Impl::getFullName() const {return forename + " " + surname;}void Customer::Impl::setShippingAddress(const Address& address) {shippingAddress = address;}//构造函数,利用上面刚定义的Impl类初始化impl成员变量Customer::Customer() : impl{ std::make_unique<Customer::Impl>() } { }Customer::~Customer() = default;std::string Customer::getFullName() const {return impl->getFullName();//调用Impl的成员函数}void Customer::setShippingAddress(const Address & address) {impl->setShippingAddress(address);//调用Impl的成员函数}
这样设计后,如果我们要修改Customer的实现,我们只需要修改cpp文件中的Impl内即可,编译时只需要重新编译cpp文件。
