Cpp_模板_Traits_Policy

模板是为了什么?

利用了模板的元编程,可以像面向对象那样复用、组合可用的代码(即也可以把一段代码作为组件使用),但是这不是Cpp的本意,实际上是Cpp的“副作用”,只是貌似是一种优势。
利用了模板的元编程的更有价值的作用在于:可以编译期执行、计算。

模板是为了编译期运算,编译期运算就可以达到零开销的效果。

传统的运行时运算的程序的性能主要依赖于客户端的计算机性能。而编译期运算的性能则主要依赖开发侧的计算机性能。C++的设计目标就在于Zero Overhead,即零开销,意思是尽量把工作都在编译期间完成。

主要体现在模板、元编程上,指导编译器生成代码

比如,现在有一个需求,是编写add函数,返回两个操作数的加和值。

刚开始可能只想到了两个整型。

1
2
3
4
5
6
7
8
9
#include<iostream>
int add(int a, int b)
{
return a + b;
}
int main()
{
int c = add(1, 2);
}

但是问题在于,后期可能要加上float型的情况。那我们可以通过Cpp的函数重载功能进行解决。

1
2
3
4
float add(float a, float b)
{
return a + b;
}

由此可以看出,需要模板的地方在于:

  1. 需要函数重载
  2. 虽然是函数重载,但是这些函数的结构相似,具体的操作、行为都一样。
  3. 区别仅在于操作的对象的数据类型不同。

在这种情况下,可以不用反复重载,而是利用模板,让编译器为我们自动生成。当然,编译器不会一下子全部把所有情况都生成,而是我们当时的代码具体用哪个,就在编译期特别地生成哪个。这就叫做模板编程。

模板编程

以下叫做函数模板。不能叫做函数。这个函数模板也不是编译单元,即不是可编译的代码。

1
2
3
4
5
template<typename T>
T add(T a, T b)
{
return a + b;
}

而是从add<float>开始到之后才是编译单元。总之:模板不会生成代码,模板不能编译,而是在使用模板的时候,指导编译器生成相应的函数代码,才有了可编译的代码。

1
2
3
4
int main()
{
float c = add<float>(1.0f, 2.0f);
}

模板特化

Specialization

全特化

Full Specialization

如果模板中的所有类型T都被具体类型替代了,那么< >中就不用写typename T了,空着。叫做全特化。

1
2
3
4
5
template < >
int add(int a, int b)
{
return a + b;
}

灵活利用特化

1
2
3
4
5
template<typename T, typename R>
R add(T a, T b)
{
return static_cast<R>(a + b);
}

以下语句是无法编译通过的,因为调用语句只说明了函数参数的类型,而函数返回类型系统是无法推断的。

1
2
3
4
int main()
{
float f = add(1, 2);
}

如果以下这样写呢?也不行,因为模板中类型的顺序问题,导致float对应的是第一个类型T,因此系统还是未知函数返回类型。

1
2
3
4
int main()
{
float f = add<float>(1, 2);
}

只能全部写出:add<int, float>(1, 2);

或者:使用类似于后位缺省的写法习惯,来解决这个问题。即把T、R换个顺序,那么在调用add时,就可以略去T了。

1
2
3
4
5
template<typename R, typename T>
R add(T a, T b)
{
return static_cast<R>(a + b);
}

以下调用,走的是template<typename R, typename T>的add。会生成:float add(int, int)

1
2
3
4
int main()
{
float f = add<float>(1, 2);
}

如果:配合上全特化模板函数。如以下main函数调用add<int>,则系统就判断出我们调用的是int add(int, int)了。总之,只要R和T全都对应上了特化的模板函数的所有类型,就会走模板特化函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<typename R, typename T>
R add(T a, T b)
{
return static_cast<R>(a + b);
}
template < >
int add(int a, int b)
{
return a + b;
}
int main()
{
int c = add<int>(1, 2); //int add(int a, int b)
}

简便的写法

模板类型的后面的参数是可以有默认类型的。
如果还是T、R的顺序,如下写:就是在说:如果不指定模板第二个类型参数,就会默认返回类型为int。

1
2
3
4
5
6
7
8
9
10
template<typename T, typename R = int>
R add(T a, T b)
{
return static_cast<R>(a + b);
}
template < >
int add(int a, int b)
{
return a + b;
}

而我们模板第一个类型参数T又是可以通过函数参数推断的,那么就可以全部省略:

1
2
3
4
int main()
{
int c = add(1, 2); //int add(int a, int b)
}

来看看以下的情况:此时走的是R add(T a, T b),是int add(double, double)

1
2
3
4
int main()
{
int c = add(1.0, 2.0);
}

而如果这样:就会编译不通过,因为无法推断T的类型。

1
2
3
4
int main()
{
int c = add(1.0, 2.0f);
}

那么就需要明确在add后加< >指出,T是什么。如果加的是double,那么2.0f会转为double型进行计算。

1
2
3
4
int main()
{
int c = add<double>(1.0, 2.0f);
}

部分特化(实际上是函数模板的重载)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template<typename T, typename R = int>
R add(T a, T b)
{
return static_cast<R>(a + b);
}
template<typename R = float>
R add(long long a, long long b)
{
return a + b;
}
template < >
int add(int a, int b)
{
return a + b;
}
int main()
{
auto c = add(1ll, 2ll);
}

以上,add函数优先和特化的模板函数匹配,而不是和R add(T a, T b)匹配,因为,参数1ll2ll与特化模板函数中的long long对应上了,即R add(long long a, long long b),所以最后生成的函数是float add(long long a, long long b)

实际上,template<typename R = float>,这个形式,本质上是一种函数模板的重载。因为,给出了参数在某些特别类型下,函数的重定义,体现了多态。总之:同一个名字,不同的形式,都叫overload。

再举一个例子_1

1
2
3
4
5
template<typename R = float>
R add(long long a, long long* pb)
{
return a + *pb;
}
1
2
3
4
5
int main()
{
auto b = 2ll;
auto c = add(1ll, &b);
}

走的是R add(long long a, long long* pb)。这也是一个function template overload。

再举一个例子_2

1
2
3
4
5
6
7
8
9
10
11
template<typename R = float>
R add(long long a, long long b)
{
return a + b;
}

template<typename R = float>
R add(long long a, long long& b)
{
return a + b;
}
1
2
3
4
5
int main()
{
auto b = 2ll;
auto c = add(1ll, &b);
}

此时,如果没有用到add(long long, long long&),是可以正常编译的。
但是,如果一旦用到了:

1
2
3
4
5
int main()
{
auto b = 2ll;
auto c = add(1ll, b);
}

就会报错:

1
2
3
4
5
'add': ambiguous call to overloaded function

more than one instance of overloaded function "add" matches the argument list:
function template "R add(long long a, long long b)"
function template "R add(long long a, long long &b)"

类模板

1
2
3
4
5
6
7
8
9
template <typename R, typename T>
class Addition
{
public:
R add(T a, T b) const noexcept
{
return a + b;
}
}
1
2
3
4
5
int main()
{
Addition<int, int> addition;
auto c = addition.add(1, 2);
}
  1. Addition<int, int>是对类模板的实例化,产生了类。
  2. Addition<int, int> addition;是对类的实例化,产生了对象。
  3. addition.add(1, 2);调用类模板函数。生成了代码。

简便的写法

能不能省一个模板参数,写出构造函数?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template<typename R, typename T = int>
class Addition
{
public:
Addition(void)
{
}
Addition(T a, T b) : _a{ a }, _b{ b }
{

}
R add(T a, T b) const noexcept
{
return a + b;
}
R add(void) const noexcept
{
return _a + _b;
}
private:
T _a;
T _b;
};
1
2
3
4
5
int main()
{
Addition addition(1, 2); // C++14标准无法编译通过
auto c = addition.add(1, 2);
}

类的部分特化(偏特化)

Class Template Partial Specialization

1
2
3
4
5
6
7
8
9
template<typename R, typename T>
class Addition
{
public:
R add(T a, T b) const noexcept
{
return a + b;
}
};

类的部分特化,定义时,要在类名后写尖括号,写入模板参数。

1
2
3
4
5
6
7
8
9
template <typename T>
class Addition<T, int>
{
public:
int add(T a, T b) const noexcept
{
return a + b;
}
};
1
2
3
4
5
int main()
{
Addition<int, int> addition;
auto c = addition.add(1, 2);
}

类的实例化走的是class Addition<T, int>

类的全特化

Class Template Full Specialization

1
2
3
4
5
6
7
8
9
template < >
class Addition<int, int>
{
public:
int add(int a, int b) const noexcept
{
return a + b;
}
};
1
2
3
4
5
int main()
{
Addition<int, int> addition;
auto c = addition.add(1, 2);
}

类的实例化走的是class Addition<int, int>

再来个例子

1
2
3
4
5
6
7
8
9
template < >
class Addition<int, int*>
{
public:
int add(int a, int* b) const noexcept
{
return a + *b;
}
};
1
2
3
4
5
6
int main()
{
auto b = 2;
Addition<int, int*> addition;
auto c = addition.add(1, &b);
}

类的实例化走的是class Addition<int, int*>

Traits

Traits是特质、特性的意思,主要用来区分不同类型。在Modern Cpp中,有<traits>库,可以用来分析各种类型。
如果要对某种类型单独做特别的处理时,就会用到Traits,这在meta progrmming中是一种设计模式。

AddTraits

比如拿Addition加和函数举例:整型有整型的策略,浮点型有浮点型的策略。更具体地,整型中也有不同的整型:int有int的策略,long有long的策略……

  1. int数和另一个int数相加,可能会溢出,这时就需要转为long数加和并返回。
  2. long数和另一个long数相加,可能会溢出,这时就需要转为long long数加和并返回。

在没有用到Traits时的解决方案

如果只是通过两个模板参数来解决这个问题,可以采用如下方案:

1
2
3
4
5
template<typename R, typename T>
R add(T a, T b)
{
return a + b;
}

但是,这个方案不够自动化。每当处理不同的类型,都需要时刻调整模板参数:

1
2
3
4
5
6
7
int main()
{
int a = 10, b = 20;
long c = add<long, int>(a, b);
long d = 2000;
long long e = add<long long, long>(c, d);
}

用到Traits,让调用更爽

Traits就是为了解决这个问题。通过提前约束不同类型的行为,从而让调用更简便。这让模板函数在使用上更自动化了。

要写这样的Addition模板群,就要先声明一个主模板:
T代表操作数类型。而操作数的返回值类型通过T对应的具体的class得出(本例中为R)。
这个主模板可以不实现,因为这个主模板是一个抽象的定义。后期才会定义具体的、特化的类模板。

1
2
template <typename T>
class AddTraits;

通过类模板的特化实现AddTraits

Traits是利用特化来实现的。通过类模板的特化,来区分不同类型的加和。

拿unsigned short的"加和"类模板举例:

1
2
3
4
5
6
template < >
class AddTraits<unsigned short>
{
public:
typedef unsigned int R;
};

以上,在unsigned short类型的加和下,规定了目标类型R(即加和的返回类型)为unsigned int。
这个R,要在模板类外部得到其实例可以如下操作:

1
2
3
4
5
int main()
{
// AddTraits<unsigned short>::R 前面需要加一个typename,不然R可能会被认作是静态变量名字。
typename AddTraits<unsigned short>::R r = 9;
}

更多地:
unsigned int的加和类模板,规定了目标类型R(即加和的返回类型)为unsigned long long。

1
2
3
4
5
6
template < >
class AddTraits<unsigned int>
{
public:
typedef unsigned long long R;
};

更多地:
unsigned long的加和类模板,规定了目标类型R(即加和的返回类型)为unsigned long long。

1
2
3
4
5
6
template < >
class AddTraits<unsigned long>
{
public:
typedef unsigned long long R;
};

用AddTraits类模板编写Add函数模板

我们的需求、目标就是,已知两个T类型的数,加和,返回T加和后特定、自定、规定的更大包容的类型。那么这个更大包容的类型就是每一个特化类模板中的R,现在可以统一写为:typename AddTraits<T>::R

为了便于书写,可以重命名AddTraits<T>::R为R。

1
2
3
4
5
6
template<typename T>
typename AddTraits<T>::R add(T a, T b)
{
typedef AddTraits<T>::R R;
return static_cast<R>(a) + static_cast<R>(b);
}

以上函数形式也可以如下写。即在模板参数中就通过缺省值的形式指明R是什么的别名,就可以用在返回值类型的简化了。

1
2
3
4
5
template <typename T, typename R = AddTraits<T>::R>
R add(T a, T b)
{
return static_cast<R>(a) + static_cast<R>(b);
}

测试:

1
2
3
4
5
6
7
8
9
10
11
int main()
{
unsigned short a = 1u;
unsigned short b = 2u;
// 调用的是unsigned int add<unsigned short>(unsigned short a, unsigned short b)
auto c1 = add(a, b);
// 调用的是unsigned long long add<unsigned int>(unsigned int a, unsigned int b)
auto c2 = add(1u, 2u);
// 调用的是unsigned long long add<unsigned long>(unsigned long a, unsigned long b)
auto c3 = add(1ul, 2ul);
}

Policy

上面谈到的Traits是关于类型的封装。

而Policy——策略,是关于行为的封装。比如把加法、减法、乘法、除法都封装成一样的行为,就是Policy。再如,日志系统,有的要写到文件中,有的则要写到服务器中,或者直接控制台输出。

封装AddPolicy

比如要把加法封装为Policy,就是要封装上面的R add(T a, T b)

AddPolicy即是一个具体的OperatePolicy,那么,OperatePolicy都将有一个calculate方法。

以加法Policy来说,它的特征就是:

  1. 有两个操作数a、b,类型为T。
  2. 有一个计算的指令,指令名可以都叫做calculate,作为函数名。函数名中则是具体的计算行为,加法Policy中则是a + b
  3. 会返回一个值,类型为R。

我们只是利用类的外壳,实际有用的是静态方法。
再利用Traits,通过具体T指明R将返回什么。R = AddTraits<T>::R

1
2
3
4
5
6
7
8
9
10
// T是操作数类型,R是返回类型
template <typename T, typename R = AddTraits<T>::R>
class AddPolicy
{
public:
static R calculate(T a, T b)
{
return static_cast<R>(a) + static_cast<R>(b);
}
};

封装MultiplyPolicy

现在我们要编写第二个具体的Policy,即乘法Policy。因为加法和乘法最后都表现出同样的特质,所以乘法Traits可以复用AddTraits。

1
2
3
4
5
6
7
8
9
template <typename T, typename R = AddTraits<T>::R>
class MultiplyPolicy
{
public:
static R calculate(T a, T b)
{
return static_cast<R>(a) * static_cast<R>(b);
}
};

封装 Traits 和 Policy 为 Operate 函数模板

设计一个函数模板,把数据特性 Traits 和行为抽象 Policy 封装。

其中,T 是原始数据类型,U 是一个 Policy,如 AddPolicy。

封装AddOperate

首先可以尝试封装一个具体的 Policy 如 AddPolicy 。
这个函数返回 AddPolicy 的 calculate 的计算结果,即返回数据类型是AddTraits<T>::R

1
2
3
4
5
6
// T是操作数类型,U是Policy
template<typename T, typename U = AddPolicy<T> >
typename AddTraits<T>::R AddOperate(T a, T b)
{
return U::calculate(a, b);
}
1
2
3
4
int main()
{
std::cout << AddOperate(1, 2) << std::endl; // 3
}

优化

AddOperate 函数的返回类型书写太冗长,考虑可以用个简化的别名。可以利用AddTraits<T>::R在 AddPolicy 中存在、使用这个特点,则可以在 AddPolicy 中另起模板参数R的别名为RTNTYPE(除了R,其他名字都行)。
好处在于:类中另起的 RTNTYPE 和模板参数的 R 相比,前者可以在类外部直接使用,而后者不可以。

1
2
3
4
5
6
7
8
9
10
11
// T是操作数类型,R是返回类型
template <typename T, typename R = AddTraits<T>::R>
class AddPolicy
{
public:
using RTNTYPE = R;
static R calculate(T a, T b)
{
return static_cast<R>(a) + static_cast<R>(b);
}
};

T为操作数类型;U为Policy;R为返回值类型。

1
2
3
4
5
template <typename T, U = AddPolicy<T> >
U::RTNTYPE AddOperate(T a, T b)
{
return U::calculate(a, b);
}
1
2
3
4
5
6
7
int main()
{
unsigned short a = 7u;
unsigned short b = 3u;
auto c = AddOperate(a, b);
std::cout << c << std::endl;
}

封装抽象Operate

抽象的Operate是真正的可以传入任意的Policy参数的。

1
2
3
4
5
template <typename U, typename T>
U::RTNTYPE Operate(T a, T b)
{
return U::calculate(a, b);
}

但是,如果这样写的话,得给AddPolicy后面加具体的操作数类型才能编译通过。

1
2
3
4
5
6
7
int main()
{
unsigned short a = 3u;
unsigned short b = 7u;
auto c = Operate<AddPolicy<decltype(a)> >(a, b);
std::cout << c << std::endl;
}

有没有什么办法能不传入decltype(a)就能进行的呢?那样的话,就可以非常地简洁:Operate<AddPolicy>(a, b)
方法就是把Operate中的U参数指明为类模板。

1
2
3
4
5
template <typename T, template<typename, typename> class Policy>
Policy<T, Policy::RTNTYPE>::RTNTYPE Operate(T a, T b)
{
return Policy<T, Policy::RTNTYPE>::calculate(a, b);
}

但是以上代码肯定编译不过,因为出现了无限递归解析:Policy不是一个具体类,因此无法通过Policy指明具体RTNTYPE,于是就得加第三个模板参数Traits。

注意,Policy<T, typename Traits::R>中的Traits::R前面应该加typename。以明确区分传入的是类型而不是常量值(因为模板参数可以传入常量值)

1
2
3
4
5
template <template<typename, typename> class Policy, typename Traits, typename T>
Traits::R Operate(T a, T b)
{
return Policy<T, typename Traits::R>::calculate(a, b);
}
1
2
3
4
5
6
7
int main()
{
unsigned short a = 3u;
unsigned short b = 7u;
auto c = Operate<AddPolicy, AddTraits<decltype(a)>>(a, b);
std::cout << c << std::endl;
}

这样的话,还是得在AddTraits后加一个decltype(a)才行,能不能彻底消灭呢?类比指明Policy是个类模板的经验,把Traits也指明为一个类模板,即可:

1
2
3
4
5
6
7
template <template<typename, typename> class Policy,
template<typename> class Traits,
typename T>
Traits<T>::R Operate(T a, T b)
{
return Policy<T, typename Traits<T>::R>::calculate(a, b);
}
1
2
3
4
5
6
7
8
9
int main()
{
unsigned short a = 3u;
unsigned short b = 7u;
auto c = Operate<AddPolicy, AddTraits>(a, b);
std::cout << c << std::endl; // 10
c = Operate<MultiplyPolicy, AddTraits>(a, b);
std::cout << c << std::endl; // 21
}

如此,终于把Operate的调用变得简洁、美观了!这个调用形式也体现了Policy和Traits合二为一、相辅相成的美感。

最终代码

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
54
55
56
57
58
59
60
61
62
63
#include<iostream>

template <typename T>
class AddTraits;

template < >
class AddTraits<unsigned short>
{
public:
typedef unsigned int R;
};

template < >
class AddTraits<unsigned int>
{
public:
typedef unsigned long long R;
};

template < >
class AddTraits<unsigned long>
{
public:
typedef unsigned long long R;
};

template <typename T, typename R = typename AddTraits<T>::R>
class AddPolicy
{
public:
using RTNTYPE = R;
static R calculate(T a, T b)
{
return static_cast<R>(a) + static_cast<R>(b);
}
};

template <typename T, typename R = typename AddTraits<T>::R>
class MultiplyPolicy
{
public:
using RTNTYPE = R;
static R calculate(T a, T b)
{
return static_cast<R>(a) * static_cast<R>(b);
}
};

template <template<typename, typename> class Policy, template<typename> class Traits, typename T>
typename Traits<T>::R Operate(T a, T b)
{
return Policy<T, typename Traits<T>::R>::calculate(a, b);
}

int main()
{
unsigned short a = 3u;
unsigned short b = 7u;
auto c = Operate<AddPolicy, AddTraits>(a, b);
std::cout << c << std::endl;
c = Operate<MultiplyPolicy, AddTraits>(a, b);
std::cout << c << std::endl;
}

更加简化

升级AddTraits为OperationTraits

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
54
55
56
57
58
59
60
61
62
63
#include<iostream>

template <typename T>
class OperationTraits;

template < >
class OperationTraits<unsigned short>
{
public:
using R = unsigned int;
};

template < >
class OperationTraits<unsigned int>
{
public:
using R = unsigned long;
};

template < >
class OperationTraits<unsigned long>
{
public:
using R = unsigned long long;
};

template <typename T>
class AddPolicy
{
public:
using RTNTYPE = typename OperationTraits<T>::R;
static RTNTYPE calculate(T a, T b)
{
return static_cast<RTNTYPE>(a) + static_cast<RTNTYPE>(b);
}
};

template <typename T>
class MultiplyPolicy
{
public:
using RTNTYPE = typename OperationTraits<T>::R;
static RTNTYPE calculate(T a, T b)
{
return static_cast<RTNTYPE>(a) * static_cast<RTNTYPE>(b);
}
};

template <template<typename> class Policy, typename T>
typename Policy<T>::RTNTYPE Operate(T a, T b)
{
return Policy<T>::calculate(a, b);
}

int main()
{
unsigned short a = 3u;
unsigned short b = 7u;
auto c = Operate<AddPolicy>(a, b);
std::cout << c << std::endl;
c = Operate<MultiplyPolicy>(a, b);
std::cout << c << std::endl;
}

Cpp_string仿写

内容

  1. 分析string的设计、实现
  2. 由at方法引出的C++异常机制(有单独详细的文章)
  3. 由字符串对象的比较引出的<compare>库、比较机制(三路比较)
  4. cout输出流机制,以及引出的友元
  5. cin输入流机制
  6. 引入右值引用拷贝、赋值

String的历史

C++手册中的<string>header中,Class instantiations(类实例)有:string、u16string、u32string、wstring(宽字符)。
string 是 ANSI 规范的普通字符。
wstring(宽字符)是 Unicode 字符集,开发中推荐使用。

String方法

  1. length():返回字符串有效字符长度,即不包括\0
  2. size():也是返回有效字符长度。与length()等价。
    1. 但在 vector 中,二者有区别。
    2. Cpp字符串的长度函数是以上两个,调用形式是:str.size() str.length()。而C语言的<cstring>中的调用形式是strlen(str)

仿写成员方法

基本思想1:体现面向对象中封装的特性:构造

基本思想2:RAII

RAII 全称 Resource Acquisition is Initialization. 这是C++的设计哲学。
意为:C++的对象在创建或构造时意味着初始化的开始,比如char * _str成员在string构造时自动地指向一块新申请的区域。那么,相对应的过程是析构就意味着要释放资源。这个过程就是推荐的C++对象的自然设计、自然行为。
而Java的设计理念是类启动时并没有进行初始化,还要进行init()

常量字符串构造

std::string str("Hello")

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyString
{
public:
MyString(const char * str)
{
if(!str) return;
size_t len = strlen(str);
_str = new char[len + 1];
strcpy(_str, str);
}

const char * c_str(void) const
{
return _str;
}
private:
char * _str{ nullptr };
}

拷贝构造

1
2
3
4
5
6
7
8
9
10
11
class MyString
{
public:
MyString(MyString const & str)
{
if(!str._str) return;
size_t len = strlen(str._str);
_str = new char[len + 1];
strcpy(_str, str._str);
}
}

赋值重载

把旧的内容清空,赋予新的内容。

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
class MyString
{
public:
MyString& operator = (const char * str)
{
if(_str)
{
delete[] _str;
_str = nullptr;
}
if(str != nullptr)
{
size_t len = strlen(str);
_str = new char[len + 1];
strcpy(_str, str);
}
return *this;
}
MyString& operator = (MyString const & str)
{
if(&str == this) return *this;
if(_str)
{
delete[] _str;
_str = nullptr;
}
if(str._str != nullptr)
{
size_t len = strlen(str._str);
_str = new char[len + 1];
strcpy(_str, str._str);
}
return *this;
}
}

+=

两种情况:

  1. 类本身是空的,则直接new,复制
  2. 类有东西,则想办法搞合适的空间,追加
    1. 可能要扩容原空间
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
class MyString
{
public:
MyString& operator += (const char * str)
{
// 要追加的str为空 退出
if(!str) return *this;

size_t oldlen = strlen(_str);
size_t newlen = strlen(str);
// 要追加的str内容为空串 退出
if(newlen == 0) return *this;

// 重新分配更大的空间
// 需要先备份到newstr临时空间
char * newstr = new char[oldlen + newlen + 1];
strcpy(newstr, _str);
strcat(newstr, str);
//如果本身串不为空 则需要销毁旧空间
if(_str != nullptr)
{
delete[] _str;
_str = nullptr;
}
_str = newstr;
return *this;
}
MyString& operator += (MyString const & str)
{
//
}
}

析构

1
2
3
4
5
6
7
8
~MyString()
{
if (_str)
{
delete[] _str;
_str = nullptr;
}
}

Boolean Operator

为了便于运算if(str)这类语句。C++11版本允许有bool运算符的重载。

bool运算符的重载的特点:

  1. 因为布尔运算符的返回值必须为Boolean,锁定了返回值类型,所以可以省略不写。
  2. 和小括号运算符不一样,小括号运算符可以返回任何值类型,但bool运算符必须返回boolean类型。
1
2
3
4
5
6
7
8
9
class MyString
{
public:
// 不用写返回值,默认就一定是bool类型
operator bool() const
{
return _str;
}
}

at(抛异常,检查边界)

有两种:

  1. at位置的字符不能修改

  2. at位置的字符可以修改

  3. 可能会out of range。

  4. 返回的都是引用类型,引用不能为空,所以不能通过返回值是否为空来判断是否错误了,所以得用异常机制。

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
#include<exception>
class OutOfRange : public std::exception
{
public:
OutOfRange() : std::exception{"MyString: out of range!"}
{
}
}
class MyString
{
public:
char & at(const size_t off)
{
size_t len = strlen(_str);
if(off > strlen - 1)
throw OutOfRange{};
return _str[off];
}
char const & at(const size_t off) const
{
size_t len = strlen(_str);
if(off > strlen - 1)
throw OutOfRange{};
return _str[off];
}
}
int main()
{
if(str)
{
try
{
std::cout << str.at(11) << std::endl;
}
catch (const std::exception &e)
{
std::cout << e.what() << std::endl;
}
}
}

[](不抛异常,不检查边界)

与at函数的不同在于,[]是不抛出异常的,因此不检查边界。如果越界,则行为未定义。

1
2
3
4
5
6
7
8
9
10
11
12
class MyString
{
public:
char& operator[](const size_t off) noexcept
{
return _str[off];
}
char const & operator[](const size_t off) const noexcept
{
return _str[off];
}
}

<=>三路比较运算符

C++20标准中,有<compare>库。

  1. strong_ordering
    1. less
    2. greater
    3. equal:相等
    4. equivalent:等价
  2. weak_ordering
    1. less
    2. greater
    3. equivalent:等价,或者说模糊的相等
  3. partial_ordering
    1. less
    2. greater
    3. equivalent
    4. unordered

strong_ordering测试如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<compare>
int main()
{
int a = 3;
int b = 4;
auto c = a <=> b;//c的类型:std::strong_ordering
if(c < 0)
std::cout << "less" << std::endl;
else if(c > 0)
std::cout << "greater" << std::endl;
else if(c == 0)
std::cout << "equal" << std::endl;
return 0;
}

partial_ordering测试如下,主要测试unordered的情况。用一个NaN浮点数比较时,就会出现。

NaN定义于limits库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<limits>
int main()
{
double a = 4.0;
double b = std::numeric_limits<double>::quiet_NaN(); //得出double类型的NaN
auto c = a <=> b; //c的类型:std::partial_ordering
if(c < 0)
std::cout << "less" << std::endl;
else if(c > 0)
std::cout << "greater" << std::endl;
else if(c == 0)
std::cout << "equivalent" << std::endl;
else
std::cout << "unordered" << std::endl;
return 0;
}

对于字符串来说,我们返回一个strong或weak都可以。strong要求肯定更严格一些,比如区分字母大小写等等,而weak要求则松一些,比如不管大小写,都算相等。

类中怎么使用<=>

  1. default的<=>是按照类中的成员顺序依次比较。
  2. 如果不是按照默认顺序依次比较,则需要自定义函数逻辑

以下是默认的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Point
{
public:
Point(int x, int y) : _x{ x }, _y{ y }
{
}
std::strong_ordering operator <=> (Point const & pt) const = default;

private:
int _x;
int _y;
};
int main()
{
Point pt{1, 2}, pt2{1, 2};
if(pt == pt2)
{
std::cout << "equal" << std::endl;
}
}

string中的实现

以下回到MyString中的<=>比较运算符。我们定义其返回weak_ordering。即不管大小写混合与否,都是等价关系。总的来说,我们在大小写不敏感的情况下返回weak_ordering

  1. 统一大小写,再比对,一样则返回等价,不一样则返回大、小
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<compare>
class MyString
{
public:
std::weak_ordering operator <=> (MyString const & other) const noexcept
{
// 全部转换为大写或小写。可以使用cctype中的toupper、tolower处理
/*for
if(!islower())
{
tolower()
}*/
int c = strcmp(_str, other.c_str());
if(c == 0)
return std::weak_ordering::equivalent;
else if(c > 0)
return std::weak_ordering::greater;
else
return std::weak_ordering::less;
}
}

类中只要定义了<=>这个运算符(参数是一个const 同类引用),那么,外部的比较运算符就转向运行该函数进行比较。

1
2
3
4
5
6
7
8
9
10
11
int main()
{
MyString str{ "Hello" };
MyString str2{ "Hello" };
if(str == str2)
std::cout << "equal" << std::endl;
else if(str < str2)
std::cout << "less" << std::endl;
else
std::cout << "greater" << std::endl;
}

cout识别本类(全局重载)

cout 是 ostream 的实例。如果我们要做到让 cout 认识自定义类 MyString 的话,就需要重载 cout 的<<运算符。注意,与 MyString 的成员方法没关系,因为cout的形式是:std::cout << str

1
2
3
4
5
std::ostream& operator <<(std::ostream &os, MyString const & str)
{
os << str.c_str();
return os;
}

友元

假如类外部需要访问类内私有成员,则需要再类内任意位置授权该类外函数。即在函数声明形式的语句之前,加friend。
这是遵循封装性的程序设计下的一种妥协的通路。在特殊情况下,可以破坏封装性。

1
2
3
4
5
6
7
8
9
10
class MyString
{
friend std::ostream& operator << (std::ostream& os, MyString const& str);
}
std::ostream& operator <<(std::ostream &os, MyString const & str)
{
// private
os << str._str;
return os;
}

cin

cin 的位置在<istream>中。内部有输入缓冲区。以回车标志着输入完毕,空格标志着数据的间隔。

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
int main()
{
std::string str;
std::string str2;
std::cin >> str;
std::cin >> str2;
}
// 输入:12 23 34 回车
// str: 12
// str2:23

cin的方法


cin.get()方法不带参数表示从缓冲区析出一个字符,并且不再返回缓冲区。
如果放在cin >> x的后面,前面>>已经把有效数据拿走,则此时恰好可以在间隔符(空格或回车)的位置上get,因此不会影响正常数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
int nums[100];
// 向nums输入数据,以空格为间隔,以回车为结束标志
int main()
{
int i = 0;
while (std::cin >> nums[i++])
{
if (std::cin.get() == '\n')
break;
}
for (int j = 0; j < i; ++j)
std::cout << nums[j] << std::endl;
}
// 输入:1 2 3 4 5 6 7 回车
// 输出:1 2 3 4 5 6 7

cin重载(全局)

首先调用cin的>>后用户要先输入一些东西,保存到了缓冲区。
需要用到cin.get(char* s, streamsize n)表示从缓冲区析出 n 个字符大小,写入 s 指向的区域中。

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
std::istream& operator >>(std::istream& is, MyString const& str)
{
char * in_c_str = new char[1024];
memset(in_c_str, 0, 1024); // <cstring>

is.get(in_c_str, 1024); // 1. 从缓冲区析出1024个字符,写入到in_c_str中

// 要追加的str为空 退出
if(!&str) return *this;

size_t inlen = strlen(in_c_str);
// 要追加的str内容为空串 退出
if(inlen == 0) return *this;

size_t oldlen = strlen(_str);
// 重新分配更大的空间
// 需要先备份到newstr临时空间
char * newstr = new char[oldlen + newlen + 1];
strcpy(newstr, _str);
strcat(newstr, in_c_str);
//如果本身串不为空 则需要销毁旧空间
if(_str != nullptr)
{
delete[] _str;
_str = nullptr;
}
_str = newstr;

return is; // 2. 返回is
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main()
{
MyString str{"Hello"};
std::cout << str.c_str() << std::endl;
MyString str2{str};
// MyString str2 = str; 等效于MyString str2{str}; 都是走拷贝构造
std::cout << str2.c_str() << std::endl;
str = "Test =";
std::cout << str.c_str() << std::endl;
if(str)
{
std::cout << str.c_str() << std::endl;
}
str += "Test +=";
std::cout << str.c_str() << std::endl;

std::cout << str.at(5) << std::endl;
}

问题代码

../../image-20211121150816082

问题:

  1. return String(newch)后,不能delete该函数体中的缓冲区newch,造成内存泄漏。
  2. 系统崩溃。因为返回的将亡值String(newch)s3 = s1 + s2语句结束前浅拷贝,把有值的str给了s3的str。语句结束后自动析构,释放了堆区空间(String(newch)).str,虽然str置空了,但s3的str是脏值。
    1. 首先,由于堆区释放了,s3用不了了
    2. 其次更严重的是,main函数结束时由于s3的str为脏值,不为空,因此会再次delete[]str,导致系统崩溃。

引入右值赋值函数

../../image-20211121153216410

此时,等号赋值调用右值移动函数,将临时构造的s1+s2对象的str的拥有权转移给了s3。

将亡值

某一句表达式产生了一个不具有名字的实体。
生存期只存在某一句表达式中。
此实体即为将亡值。

右值引用

1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
String fun()
{
String s2 = ("yhping");
return s2;
}
int main()
{
String s1;
s1 = fun();
return 0;
}
/*
此程序产生3个对象。
首先main函数产生s1; 1
其次,fun函数栈帧产生s2对象。 2
然后,拷贝s2构造一个无名对象到main函数栈帧。 3
s1再调用等号赋值,即无名对象的值赋给s1。
*/

2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
String & fun()
{
String s2 = ("yhping");
return s2;
}
int main()
{
String s1;
s1 = fun();
cout << s1 << endl;
return 0;
}
/*
不能这样做。
因为如果要以引用返回,则s1会从析构后的s2上取值,则会产生随机值
*/

3

右值引用只能引用字面常量或无名量。左值引用不能引用无名量。

1
2
3
4
5
6
7
8
int main()
{
String s1;

String & sx = s1;
String && sy = sx;//error
String && sz = String("hello");//ok
}
1
2
3
4
5
String && fun()//error,因为s2有名字,不能以右值引用方式返回。
{
String s2("yhping");
return s2;
}
1
2
3
4
5
int main()
{
int && a = 10;
int && b = a;//依然不可以,虽然a是一个右值引用,但是a有了名字,所以b不能右值引用a。
}

4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class String
{

public:
String(String && s)
{
cout << "move copy construct :" << this << endl;
str = s.str;
s.str = NULL;
}
String& operator=(String && s)
{

}
};

写时拷贝的String

1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
String s1("xcgong");
String s2(s1);
String s3(s2);
String s4(s3);
}
/*
有两种情况:
如果是浅拷贝,则会出现重复析构;
如果是深拷贝,则会出现多次重复的堆空间。
*/

写时拷贝的String是GCC里String的方案。