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, class
template <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) {//值传递,不是引用,这里会发生copy
swap(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文件。