C++ standard之演化
- C++ 98(1.0)
- C++ 03(TR1, technical Report 1) // 一个实验性的版本
- C++ 11(2.0)
- C++ 14
此次记录涵盖了C++ 11和C++ 14
C++ 2.0新特性包括了语言和标准库两个方面,标准库主要是以头文件的形式呈现
头文件不带 (.h), 例如 #include <vector>
新式的C 头文件也不带 (.h), 例如 #include<cstdio>
新式的C 头文件带有 (.h) 的头文件仍然可用, 例如 #include <stdio.h>
一些新增的头文件
#include <type_traits>
#include <unordered_set>
#include <forward_list>
#include <array>
#include <tuple>
#include <regex>
#include <thread>
语言
Variadic Templates
这个特性的典型应用就是提供泛型代码处理任意数量任意类型的参数。而具有这种特性的模板被称为可变参数模板。(注意"..."出现的地方,此为语法规定)
#include <bits/stdc++.h>
using namespace std;
template <typename T>
void print() { // one 当参数个数为0的时候调用
}
template <typename T, typename... Types>
void print(const T& firstArg, const Types&... args) { // two (注意添加const)
//cout << sizeof...(Types) << endl;
//cout << sizeof...(args) << endl; // 打印args参数的个数,上述两种方式结果相同
cout << firstArg << endl; // 打印第一个参数
print(args...); // 打印其余参数,
}
int main() {
print(7.5, "hello", bitset<16>(377), 42);
}
递归调用方法print函数,当刚开始传入的时候使用打印第一个参数,然后使用print(T& firstArg, Types&... args)将函数参数包中参数的内容一一打印,当函数参数包中的没有参数时候,调用print()结束递归。
可以这样理解:刚开始比如传入的是5个参数,则进入 two 的时候变成了(1,4),然后 one 打印第一个参数的内容,接着继续调用 two, 参数变成了(1,3),然后重复以上内容,直到进入 two 的时候变成了(1,0),这个时候参数为空的print(),结束递归。
问题:既然sizeof可以判断args的参数个数,那我们是不是可以把打印一个参数的函数略去,直接判断如果args为0的话,直接return呢?
答案是很遗憾,这样不行,就上面的问题写出代码:
void print(const T& firstArg, const Types&... args) { // two (注意添加const)
cout << firstArg << endl; // 打印第一个参数
if(sizeof...(args) > 0)
print(args...); // 打印其余参数
}
上述代码不能通过编译。被实例化的代码是否有效是运行时决定的, 然而函数调用时的实例化却是编译时的事情。也就是说,if 语句是否执行是运行时的事情,而编译的时候每一次函数调用都会被实例化。因此,当我们调用print()打印最后一个参数时,print(args...)仍然会实例化,而我们并没有提供不接受任何参数的print()函数,那么就产生了错误。
然而在C++ 17中却解决了这个问题:(介绍的是C++ 2.0的特性,这里不过多介绍)
template <typename T, typename... Types>
void print(const T& firstArg, const Types&... args) { // two (注意添加const)
cout << firstArg << endl; // 打印第一个参数
if constexpr(sizeof...(args) > 0)
print(args...); // 打印其余参数
}
递归继承
template <typename... Values> class tuple;
template <> class tuple<> { };
//private tuple <Tail...> 完成递归继承
template <typename Head, typename... Tail>
class tuple<Head, Tail...> : private tuple <Tail...> {
typedef tuple<Tail...> inherited;
public:
tuple() { }
tuple(Head v, Tail... vtail) : m_head(v), inherited(vtail...) { } // 调用base ctor并给予参数
...
protected:
Head m_head;
};
Spaces in Template Expression
(模板表达式中的空格)
在C++11之前,多层模板参数在嵌套时,最后的两个 >
之间要加一个空格,以避免和 >>
运算符混淆。
vector<list<int> >; // 所有版本均能通过编译
vector<list<int>>; // C++11之后能通过编译
nullptr 和 std::nullptr
C++ 11之前指针可以赋值为0和NULL,C++ 11之后,nullptr可以被自动转换为任意指针类型,但不会被转换为整型数。
// 函数两个重载版本
void f(int);
void f(void*);
// 调用函数
f(0); // 调用 f(int)
f(NULL); // 若NULL被定义为0,则会调用 f(int),产生二义性
f(nullptr); // 调用 f(void*)
Automatic Type Deduction with auto (使用auto自动推断类型)
使用auto定义变量不用显示指定类型,auto会自动做实参推导, 前提是必须有东西可以推,也就是必须 = 存在;
auto s = 10; // auto 为 int类型
double f(....){};
auto s = f(); // auto 为 int类型
list<string> c;
....
list<string>::iterator it;
it = find(c.begin(), c.end(), target);
使用auto
list<string> c;
....
auto it = find(c.begin(), c.end(), target);
可以看出使用auto可以使冗余的代码简单化。
Uniform Initialization(一致性的初始化)
在C++11之前,变量的初始化方式有括号()
,花括号{}
和赋值运算符=
.因此C++ 11引入了一种uniform initialization机制,上述用到的初始化方式都可以用一个{}
代替.
int values[]{1, 2, 3};
vector<int> v{2, 3, 5, 7, 11, 13, 17};
vector<string> cities{"Berlin", "New York", "London", "Braunschweig" "Cairo", "Cologne"};
complex<double> c{4.0, 3.0}; // 等价于 c(4.0, 3.0)
※ 其实是利用了一个事实:编译器看到{t1,t2,...}便做出一个initializer_list
例如:
class P {
public:
// 有两个重载版本的构造函数,uniform initialization时优先调用接收initializer list的重载版本
P(int a, int b) {
cout << "P(int, int), a=" << a << ", b=" << b << endl;
}
P(initializer_list<int> initlist) {
cout << "P(initializer list<int>), values= ";
for (auto i : initlist)
cout << i << ' ';
cout << endl;
}
};
P p(77, 5); // P(int, int), a=77, b=5
P q{77, 5}; // P(initializer list<int>), values= 77 5
P r{77, 5, 42}; // P(initializer list<int>), values= 77 5 42
P s = {77, 5}; // P(initializer list<int>), values= 77 5
上面的案例可以看出刚才的事实的正确性,如果没有P(initializer_list
int i; // i 未定义初值
int j{}; // j 初值为0
int* p; // P 未定义初值
int* q{}; // q 初值为nullptr
uniform initialization可以防止窄化的功能,当然在不同的编译器上提示信息是不一样的,有的会报错,而有的只会警告。
uniform initialization底层依赖于模板类initializer_list
void print(std::initializer_list<int> vals) {
for (auto p = vals.begin(); p != vals.end(); ++p) { //a list of values
std::cout << *p << endl;
}
}
print({12, 3, 5, 7, 11, 13, 17});
测试样例:
#include <bits/stdc++.h>
using namespace std;
int main() {
auto s = {1,2,3};
cout << typeid(s).name() << endl;
}
输出结果:
STL中的很多容器和算法相关函数均有接收initializer list的重载版本,以vector、min和max为例:
#include <initializer_list>
vector(initializer_list<value_type> __l,
const allocator_type &__a = allocator_type())
: _Base(a)
{ _M_range_initalize(__l.begin(), __l.end(), random_access_iterator_tag()); }
vector &operator=(initalizer_list <value_type> __l) {
this->assign(__l.begin(), __l.end());
return *this;
}
void insert(iterator __position, initializer_list<value_type> __l) {
this->insert(__position, __l.begin(), __l.end());
}
void assign(initializer_list<value_type> __l) {
this->assign(__l.begin(), __l.end());
}
vector<int> v1{2, 5, 7, 13, 69, 83, 50};
vector<int> v2({2, 5, 7513, 69, 83, 50});
vector<int> v3;
v3 = {2, 5, 7, 13, 69, 83, 50};
v3.insert(v3.begin() + 2, {0, 1, 2, 3, 4});
for (auto i : v3)
cout << i << ' ';
cout << endl; // 2 5 0 1 2 3 4 7 13 69 83 50
cout << max({string("Ace"), string("Stacy"), string("Sabrina"), string("Bark1ey")}); //Stacy,可以输入任意个数的参数,但必须保证参数类型已知
cout << min({string("Ace"), string("Stacy"), string("Sabrina"), string("Sarkley")}); //Ace
cout << max({54, 16, 48, 5}); //54
cout << min({54, 16, 48, 5}); //5
expecilit
- expecilit极大多数情况下是只用于构造函数前面
expecilit for ctors taking more than one argument //C++ 2.0 之前
expecilit for ctors taking more than
one argument //c++ 2.0之后
当为expecilit for ctors taking more than one argument的时候
struct Complex {
int real, imag;
Complex(int re, int im = 0): real(re), imag(im){} //单一实参
Complex operator+(const Complex& x) {
return Complex(real + x.real, imag + x.imag);
}
};
int main() {
Complex c1(12,5);
Complex c2 = c1 + 5; //这个地方,5隐式转换为Complex对象
}
加上expecilit
struct Complex {
int real, imag;
expecilit Complex(int re, int im = 0): real(re), imag(im){} //加上expecilit的目的是:只可以显示的调用,不能隐式的调用
Complex operator+(const Complex& x) {
return Complex(real + x.real, imag + x.imag);
}
};
int main() {
Complex c1(12,5);
Complex c2 = c1 + 5; // 此时会报错
}
只有no expecilit one argument,这个时候才可以做隐式转换。
当为expecilit for ctors taking more than
one argument的时候,则可以用于任意多的参数的情况。
range-based for statement(基于范围的声明)
for( decl : coll) {
statement
}
//coll 是容器, 把coll中的元素一个个赋值给左边的decl
= default 和 = delete
=default
默认的函数声明式是一种新的函数声明方式,C++ 11允许添加= default
关键字添加到默认函数声明的末尾,这个默认函数有限制:默认构造函数,移动构造函数,赋值构造函数,移动赋值函数,析构函数,以将该函数声明为显示默认函数。这就使得编译器为显示默认函数生成了默认实现,它比手动编程函数更加有效。
假如我们实现一个有参构造函数,或者自己实现一个默认构造函数,那编译器就不会创建默认的构造函数。
问题:那我什么我们不自己实现一个默认构造函数呢?不是应该和编译器为我们的默认构造函数一样吗?
尽管两者在我们看来没什么不同,但使用=default实现的默认构造函数仍然有一定的好处。以下几点做了一定的解释:
- 如果您希望您的类是聚合类型或普通类型(或通过传递性,POD类型),那么需要使用’= default’,如果声明一个子类对象,那子类的构造函数会调用父类的构造函数,但是自己定义的默认构造函数只是一个空的函数,而编译器为我们生成的默认构造函数内部是会自动为我们调用父类的默认构造函数的。
- 使用’= default’也可以与复制构造函数和析构函数一起使用。例如,空拷贝构造函数与默认拷贝构造函数(将执行其成员的复制副本)不同。对每个特殊成员函数统一使用’= default’语法使代码更容易阅读。
class A {
public:
A() {} // 手动添加的空参构造函数
A(int mem) : member(mem) {}
private:
int member;
};
class B {
public:
B() = default; // 使用编译器生成的空参构造函数
B(int mem) : member(mem) {}
private:
int member;
};
int main() {
cout << std::is_pod<A>::value << endl; // false
cout << std::is_pod<B>::value << endl; // true
return 0;
}
=delete
在C++ 11之前,操作符delete 只有一个目的,即释放已动态分配的内存。而C ++ 11标准引入了此操作符的另一种用法,即:禁用成员函数的使用。这是通过附加= delete来完成的; 说明符到该函数声明的结尾。
- 禁用拷贝构造函数
class A {
public:
A(int x): m(x) { }
A(const A&) = delete;
A& operator=(const A&) = delete;
int m;
};
int main() {
A a1(1), a2(2), a3(3);
a1 = a2; // Error:the usage of the copy assignment operator is disabled
a3 = A(a2); // Error:the usage of the copy constructor is disabled
return 0;
}
- 禁用析构函数(慎用)
class A {
public:
A() = default;
~A() = delete;
};
int main() {
A a; //error: A destructor is deleted
A *a = new A(); //ok
delete a; //error: A destructor is deleted
}
- 禁用参数转换
class A {
public:
A(int) {}
A(double) = delete;
};
int main() {
A A1(10);
// Error, conversion from double to class A is disabled.
A A2(10.1);
return 0;
}
※ 删除的函数定义必须是函数的第一个声明, 不可以再类内声明完,到了类外在添加‘=delete’。
删除函数的优点:
- 可以防止编译器生成那些我们不想要的函数
- 可以防止那些不必要的类型转换。
在C++11之前的做法通常是将这些函数声明为private
函数,这样外界就不能调用这些函数了.但是这种做法对友元的支持不好。
拿禁用拷贝复制
class PrivateCopy {
private:
// C++11之前的做法,拷贝赋值函数仅能被内部和友元调用
PrivateCopy(const PrivateCopy &);
PrivateCopy &operator=(const PrivateCopy &);
// other members
public:
PrivateCopy() = default; // use the synthesized default constructor
~PrivateCopy(); // users can define objects of this type but not copy them
};
alias template(别名模板)
alias template使用关键字using
template<typename T>
using Vec = std::vector<T, MyAlloc<T>>; // 使用alias template语法定义含有自定义分配器的vector
Vec<int> container; // 使用Vec类型
上述功能使用宏定义或typedef
都不能实现
- 要想使用宏定义实现该功能,从语义上来说,应该这样实现:
#define Vec<T> std::vector<T, MyAlloc<T>> // 理想情况下应该这样写,但不能通过编译
Vec<int> container;
但是define不支持以小括号定义参数,要想符合语法,需要这样写
#define Vec(T) std::vector<T, MyAlloc<T>> // 能通过编译,但是使用小括号失去了泛型的语义
Vec(int) container;
这样可以通过编译,但是Vec(int)这种指定泛型的方式与原生指定泛型的方式不一致.
-
typedef
根本不接受参数,因此也不能实现上述功能.
这个时候就用到了模板模板参数
模板模板参数
template<typename T, template<typename T> class Container>
class XCls {
private:
Container<T> c;
public:
// ...
};
// 错误写法:
XCls<string, list> mylst2; // 错误:虽然list的第二模板参数有默认值,但是其作模板模板参数时不能自动推导
// 正确写法: 使用alias template指定第二模板参数
template<typename T>
using LST = list<T, allocator<T>>
XCls<string, list> mylst2; // 正确:模板LST只需要一个模板参数9
- 可以看到使用化名模板不单单只是少写几个单词这么简单.
type alias
其用法类似于typedef.
noexcept
当只要函数不发出异常,就为它加上noexcept声明
int f(int x) throw(); // f 不会抛出异常, C++ 98风格
int f(int x) noexcept; // f 不会抛出异常, C++ 11风格
// noexcept也可以加条件比如上面的 noexcept = noexcept(true)
template<typename T, size_t N>
void swap(T (&a)[N], T(&b)[N]) noexcept(noexcept(swap(*a, *b))); // 当swap(*a, *b) 不抛出异常的时候,才能保证函数不丢异常
- 在vector中的移动构造函数必须加上noexcept,因为在vector成长的时候如果移动构造函数没有加noexcept的话就没法使用
override
override:复写,改写,用于虚函数
基类中定义了一些虚函数,派生类中对于虚函数的继承可能出现漏掉一些条件的情况,然而编译器有的是不会报错的,但是如果我们在派生类继承而来的虚函数后面加上override
,编译器会严格检测派生类,如果你没有按照基类中的形式写的话,会报错.
class Base {
public:
virtual void mf1() const;
virtual void mf2(int x);
virtual void mf3() &;
void mf4() const;
};
class Derived: pulic Base {
public:
virtual void mf1(); //缺少const
virtual void mf2(unsigned int x); // 参数类型不正确
virtual void mf3() &&; //左值变右值调用
void mf4() const;
}
// 上面的派生类继承自基类的方法因缺少一些修饰而没有达到我们的目的,但是编译器不会报错
// 变成下面这样编译器会严格检查:
class Derived: pulic Base {
public:
virtual void mf1() override; //缺少const
virtual void mf2(unsigned int x) override; // 参数类型不正确
virtual void mf3() && override; //左值变右值调用
virtual void mf4() const override;
}
final
修饰类的时候
struct Base1 final{...}; //这个地方是告诉编译器Base1不可以被继承,它是体系当中的最后一个
struct Deviced : Base1{...}; //报错:Base1不可被继承
修饰类中的方法
struct Base1 {
virtual f() final;
};
struct Deviced : Base1{
void f(); //报错:f()不可被重写
};
decltype
decltype主要用于推导表达式的类型,作用类似于C++ 11之前的typeof
下面用来描述decltype的三大应用
- 用于描述返回类型
下面程序使用decltype声明函数add的返回值类型:
template <typename T1, typename T2>
decltype(x+y) add(Tl x, T2 y); // error: 'x' and 'y' was not declared in this scope
从语法上来说,上述程序是错误的,因为变量x和y在函数外访问不到,因此需要使用C++11声明返回值类型的新语法:
template<typename T1, typename T2>
auto add(T1 x, T2 y) ->decltype(x+y); //前提是x和y可以相加
- 用于元编程(其实就是模板中的运用)
template <typename T>
void test_decltype(T obj) {
map<string, float>::value_type elem1;
typedef typename decltype(0bj)::iterator iType;
typedef typename T::iterator iType;
decltype(obj) anotherObj(obj);
}
这个地方需要再介绍一下typename
的两个作用:第一个作用是和class一样声明模板的模板参数,定义template ::
操作符的地方。
class MyArray
{
public:
typedef int LengthType;
.....
}
template<class T>
void MyMethod( T myarr )
{
typedef typename T::LengthType LengthType;
LengthType length = myarr.GetLength;
}
- 代指lambda函数的类型
面对lambda表达式,一般我们手上只有object
,没有type
,要获得type
就要借助decltype
// 定义lambda函数,lambda函数作为变量的变量类型较复杂,因此使用auto进行推断
auto cmp = [](const Person &p1, const Person &p2) {
return p1.lastname() < p2.lastname() ||
(p1.lastname() == p2.lastname() && p1.firstname() < p2.firstname());
};
// 使用decltype语法推断lambda函数cmp的类型
std::set<Person, decltype(cmp)> coll(cmp);
lambda函数
lambda函数既可以用作变量,也可以立即执行:
[] {
std::cout << "hello lambda" << std::endl;
};
// 用作变量
auto l = [] {
std::cout << "hello lambda" << std::endl;
};
l();
// 直接执行
[] {
std::cout << "hello lambda" << std::endl;
}();
lambda函数的完整语法如下:
- [...] (...) mutableopt throwSpeCopt -> retTypeopt {...}
其中mutableopt,throwSpeCopt, retTypeopt都是可选的.
[...]部分指定可以在函数体内访问的外部非static对象,可以通过这部分访问函数作用域外的变量.
- [=]表示使用值传递变量.
- [&]表示使用引用传递变量.
int id = 0;
auto f = [id]() mutable {
std::cout << "id:" << id << std::endl;
++id;
};
id = 42;
f(); // id:0
f(); // id:1
f(); // id:2
std::cout << id << std::endl; // 42
lambda函数使用时相当于仿函数(functor),[...]中传入的对象相当于为仿函数的成员变量.
class Functor {
private:
int id; // copy of outside id
public:
void operator()() {
std::cout << "id: " << id << std::endl;
++id; // OK
}
};
Functor f;
与STL结合时,相比于仿函数,lambda函数通常更优雅:
// lambda函数充当predict谓词
vector<int> vi{5, 28, 50, 83, 70, 590, 245, 59, 24};
int x = 30;
int y = 100;
remove_if(vi.begin(), vi.end(),
[x, y](int n) { return x < n && n < y; });
// 仿函数充当predict谓词
class LambdaFunctor {
public:
LambdaFunctor(int a, int b) : m_a(a), m_b(b) {}
bool operator()(int n) const {
return m_a < n && n < m_b;
}
private:
int m_a;
int m_b;
};
remove_if(vi.begin(), vi.end(),
LambdaFunctor(x, y));
继续介绍variadic template
void PrintX(){}
template<typename T, typename... Types>
void Printx(const T& firstArg, const Types&... args) {
cout << firstArg << endl;
PrintX(args...);
}
int main() {
PrintX(77, "string", bitset<30>(377) , 100);
}
有6个案例演示variadic template的强大之处
1. 定义printX()
函数输出任意数目任意类型的变量
// 重载版本1,用于结束递归
void printX() {
}
// 重载版本2,先输出第一个参数,再递归调用自己处理余下的参数
template<typename T, typename... Types>
void printX(const T &firstArg, const Types &... args) {
cout << firstArg << endl;
printX(args...);
}
// 重载版本3,可以与重载版本2并存么?
template<typename... Types>
void printX(const Types &... args) {
}
对于PrintX(77, "string", bitset<30>(377) , 100);的执行顺序,我在上面已经介绍。
对于上面的问题是重载版本2和重载版本3可以共存,但是重载版本3不会被调用,因为重载版本2比重载版本3更特化
2. 重写printf()函数
void printf(const char *s) {
while (*s) {
if (*s == '%' && *(++s) != '%')
throw std::runtime_error("invalid format string: missing arguments");
std::cout << *s++;
}
}
template<typename T, typename... Args>
void printf(const char *s, T value, Args... args) {
while (*s) {
if (*s == '%' && *(++s) != '%') {
std::cout << value;
printf(++s, args...); // call even when *s = 0 to detect extra arguments
return;
}
std::cout << *s++;
}
throw std::logic_error("extra arguments provided to printf");
}
int main() {
int* pi = new int;
printf("params:%d %s %p %f \n", 15, "This is Ace.", pi, 3.141592653);
}
3. 重写max()
函数接收任意参数
若max()
函数的所有参数的类型相同的话,直接使用initializer_list
传递参数即可.
std::max({10.0, 20.0, 4.5, 8.1});
- 使用initializer_list的一个限制就是参数类型必须相同,否则会报错
然而使用variadic template重写max
函数使之接受任意类型参数:
int maximum(int n) {
return n;
}
template<typename... Args>
int maximum(int n, Args... args) {
return std::max(n, maximum(args...));
}
4. 重载tuple的<<运算符,以异于一般的方式处理头尾元素
// helper: print element with index IDX of the tuple with MAX elements
template<int IDX, int MAX, typename... Args>
struct PRINT_TUPLE {
static void print(ostream &os, const tuple<Args...> &t) {
os << get<IDX>(t) << (IDX + 1 == MAX ? "" : ",");
PRINT_TUPLE<IDX + 1, MAX, Args...>::print(os, t);
}
};
// partial specialization to end the recursion
template<int MAX, typename... Args>
struct PRINT_TUPLE<MAX, MAX, Args...> {
static void print(std::ostream &os, const tuple<Args...> &t) {
}
};
// output operator for tuples
template<typename... Args>
ostream &operator<<(ostream &os, const tuple<Args...> &t) {
os << "[";
PRINT_TUPLE<0, sizeof...(Args), Args...>::print(os, t);
return os << "]";
}
5. 递归继承实现tuple容器
// 定义 tuple类
template<typename... Values>
class tuple;
// 特化模板参数: 空参
template<>
class tuple<> {};
// 特化模板参数
template<typename Head, typename... Tail>
class tuple<Head, Tail...> :
private tuple<Tail...> // tuple类继承自tuple类,父类比子类少了一个模板参数
{
typedef tuple<Tail...> inherited; // 父类类型
protected:
Head m_head; // 保存第一个元素的值
public:
tuple() {}
tuple(Head v, Tail... vtail) // 构造函数: 将第一个元素赋值给m_head,使用其他元素构建父类tuple
: m_head(v), inherited(vtail...) {}
Head head() { return m_head; } // 返回第一个元素值
inherited &tail() { return *this; } // 返回剩余元素组成的tuple(将当前元素强制转换为父类类型)
};
6. 递归复合实现tuple容器
template<typename... Values>
class tup;
template<>
class tup<> {};
template<typename Head, typename... Tail>
class tup<Head, Tail...> {
typedef tup<Tail...> composited;
protected:
composited m_tail;
Head m_head;
public:
tup() {}
tup(Head v, Tail... vtail) : m_tail(vtail...), m_head(v) {}
Head head() { return m_head; }
composited &tail() { return m_tail; }
};
标准库
move
可以理解为完成容器的一些加速操作
平时实现对象的复制的时候,用赋值构造函数(copy)申请空间,然后指针指向新分配的空间,然后完成赋值,而移动构造函数(move)却是用一个新的指针指向原本需要赋值的对象,略去了一些行为,完成对对象的加速,但是因为新的指针指向了需要复制的对象,所以原本指向该对象的指针就不能使用。
- 右值引用:只能出现的
=
的右边 - 左值引用:可以放在
=
两侧
如果对象是一个左值,可以用std::move(对象),来强行把它变成一个右值
int foo() { return 5; }
int x = foo();
int *p = &foo(); // lvalue required as unary '&' operand
foo() = 7; // lvalue required as left operand of assignment
临时对象是一个右值,不可以取地址。
假设对于自定义的一个类,放到容器当中,这里拿vector举例,当我们调用insert方法的时候,运用的是vector中的insert(...&&自定义类) 方法,然后这个方法内中调用的应该是自定义类中的移动构造函数。
调用中间函数(这个地方可以看做上面的insert方法)会改变变量的可变性和左值右值等性质,导致参数的非完美转交(unperfect forwarding),下面程序中的中间转交函数
perfect forwarding
// 函数process的两个重载版本,分别处理参数是左值和右值的情况
void process(int &i) {
cout << "process(int&):" << i << endl;
}
void process(int &&i) {
cout << "process(int&&):" << i << endl;
}
// 中间转交函数forward接收一个右值,但函数内将其作为左值传递给函数process了
void forward(int &&i) {
cout << "forward(int&&):" << i << ", ";
process(i);
}
上面的forward(..)
方法破坏了参数本身是一个右值的性质
int a = 0;
process(a); // process(int&):0 (变量作左值)
process(1); // process(int&&):1 (临时变量作右值)
process(std::move(a)); // process(int&&):0 (使用std::move将左值改为右值)
forward(2); // forward(int&&):2, process(int&):2 (临时变量作左值传给forward函数,forward函数体内将变量作为右值传给process函数)
forward(std::move(a)); // forward(int&&):0, process(int&):0 (临时变量作左值传给forward函数,forward函数体内将变量作为右值传给process函数)
forward(a); // ERROR: cannot bind rvalue reference of type 'int&&' to lvalue of type 'int'
const int &b = 1;
process(b); // ERROR: binding reference of type 'int&' to 'const int' discards qualifiers
process(move(b)); // ERROR: binding reference of type 'int&&' to 'std::remove_reference<const int&>::type' {aka 'const int'} discards qualifiers
使用std::forward()
函数可以完美转交变量,不改变其可变性和左值右值等性质:
// 函数process的两个重载版本,分别处理参数是左值和右值的情况
void process(int &i) {
cout << "process(int&):" << i << endl;
}
void process(int &&i) {
cout << "process(int&&):" << i << endl;
}
// 中间转交函数forward使用std::forward()转交变量
void forward(int &&i) {
cout << "forward(int&&):" << i << ", ";
process(std::forward<int>(i));
}
forward(2); // forward(int&&):2, process(int&&):2 (临时变量作左值传给forward函数,forward函数体内使用std::forward函数包装变量,保留其作为右值的性质)
forward(std::move(a)); // forward(int&&):0, process(int&&):0 (临时变量作左值传给forward函数,forward函数体内使用std::forward函数包装变量,保留其作为右值的性质)
move-aware class
编写一个支持move语义的类MyString以演示移动构造函数和移动赋值函数的写法.
#include <cstring>
class MyString {
public:
static size_t DCtor; // 累计默认构造函数调用次数
static size_t Ctor; // 累计构造函数调用次数
static size_t CCtor; // 累计拷贝构造函数调用次数
static size_t CAsgn; // 累计拷贝赋值函数调用次数
static size_t MCtor; // 累计移动构造函数调用次数
static size_t MAsgn; // 累计移动赋值函数调用次数
static size_t Dtor; // 累计析构函数调用次数
private:
char *_data;
size_t _len;
void _init_data(const char *s) {
_data = new char[_len + 1];
memcpy(_data, s, _len);
_data[_len] = '\0';
}
public:
// 默认构造函数
MyString() : _data(nullptr), _len(0) { ++DCtor; }
// 构造函数
MyString(const char *p) : _len(strlen(p)) {
++Ctor;
_init_data(p);
}
// 拷贝构造函数
MyString(const MyString &str) : _len(str._len) {
++CCtor;
_init_data(str._data);
}
// 拷贝赋值函数
MyString &operator=(const MyString &str) {
++CAsgn;
if (this != &str) {
if (_data) delete _data;
_len = str._len;
_init_data(str._data); //COPY!
}
return *this;
}
// 移动构造函数
MyString(MyString &&str) noexcept : _data(str._data), _len(str._len) {
++MCtor;
str._len = 0;
str._data = nullptr; // 将传入对象的_data指针设为nullptr,防止析构函数多次delete同一根指针
}
// 移动赋值函数
MyString &operator=(MyString &&str) noexcept {
++MAsgn;
if (this != &str) {
if (_data) delete _data;
_len = str._len;
_data = str._data; //MOVE!
str._len = 0;
str._data = nullptr; // 将传入对象的_data指针设为nullptr,防止析构函数多次delete同一根指针
}
return *this;
}
//dtor
virtual ~MyString() {
++Dtor;
if (_data)
delete _data;
}
};
size_t MyString::DCtor = 0;
size_t MyString::Ctor = 0;
size_t MyString::CCtor = 0;
size_t MyString::CAsgn = 0;
size_t MyString::MCtor = 0;
size_t MyString::MAsgn = 0;
size_t MyString::Dtor = 0;
值得注意的有两点:
- 移动构造函数和移动赋值函数通常不涉及内存操作,不会抛出异常,因此应加以noexcept修饰.
- 在移动构造函数和移动赋值函数中,移动了原对象的数据后,要把原对象的数据指针置空,防止析构函数多次delete同一指针
测试move语义对容器的作用
move语义可以减少深拷贝,可以加速容器操作,编写下述测试函数进行测试:
template<typename M, typename NM>
void test_moveable(M c1, NM c2, long &value) {
char buf[10];
// 测试保存moveable对象的容器
typedef typename iterator_traits<typename M::iterator>::value_type V1type;
clock_t timeStart = clock();
for (long i = 0; i < value; ++i) {
snprintf(buf, 10, "%d", rand()); // 向容器内放入随机字符串
auto ite = c1.end(); // 定位尾端
c1.insert(ite, V1type(buf)); // 安插於尾端 (對RB-tree和HT這只是hint)
}
cout << "construction, milli-seconds: " << (clock() - timeStart) << endl;
cout << "size()= " << c1.size() << endl;
output_static_data(*(c1.begin()));
// 测试容器的std::move()语义
M c11(c1);
M c12(std::move(c1));
c11.swap(c12);
// 对保存non-movable对象的容器进行上述测试
// ...
}
template<typename T>
void output_static_data(const T &myStr) {
cout << typeid(myStr).name() << "-- " << endl;
cout << "CCtor=" << T::CCtor
<< " MCtor=" << T::MCtor
<< "Asgn=" << T::CAsgn
<< "MAsgn=" << T::MAsgn
<< "Dtor=" << T::Dtor
<< "Ctor=" << T::Ctor
<< "DCtor=" << T::DCtor
<< endl;
}
long value = 3000000L;
test_moveable(vector<MyString>(), vector<MyStringNonMovable>(), value);
test_moveable(list<MyString>(), list<MyStringNonMovable>(), value);
test_moveable(deque<MyString>(), deque<MyStringNonMovable>(), value);
test_moveable(multiset<MyString>(), multiset<MyStringNonMovable>(), value);
test_moveable(unordered_multiset<MyString>(), unordered_multiset<MyStringNonMovable>(), value);
测试结果:
- 在插入元素部分,只有vector容器的速度受元素是否movable影响大,这是因为只有容器vector在增长过程中会发生复制.
- 对于所有容器,其移动构造函数都远快于其拷贝构造函数,容器vector的移动复制函数仅仅发生了指针的交换,未发生元素的复制.
新的容器特性
容器array
内部其实就是一个数组,初始化的是不必须指明类型和数组个数:array<int,10>, 10个int类型的数据
- array容器没有构造函数和析构函数,因为array就是为了表现纯粹的数组。
容器hashtable
- vector里面存放的是单向链表
hashtable
最开始只有53个桶,当元素个数大于桶的个数时,桶的数目扩大为最接近当前桶数两倍的质数,实际上,桶数目的增长顺序被写死在代码里:
static const unsigned long __stl_prime_list[__stl_num_primes] = {
53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593,
49157, 98317, 196613, 393241, 786433, 1572869, 3145739,
6291469, 12582917, 25165843, 50331653, 100663319,
201326611, 402653189, 805306457, 1610612741,
3221225473ul, 4294967291ul};
hash function
hash function
的目的就是希望把元素值算出一个hash code(一个可运行modulus运算的值), 使得元素经hash code 映射之后能够(够乱够随意)地被至于hashtable内,越乱,与不容易发生碰撞。
以G2.9版关于C的char类型举例
tuple
// 创建tuple
tuple<string, int, int, complex<double> > t;
tuple<int, float, string> t1(41, 6.3, "nico"); // 指定初值
auto t2 = make_tuple(22, 44, "stacy"); // 使用make_tuple函数创建tuple
// 使用get<>()函数获取tuple内的元素
cout << "t1:" << get<0>(t1) << "<< get<1>(t1)<<" << get<2>(t1) << endl;
get<1>(t1) = get<1>(t2); // 获取的元素是左值,可以对其赋值
// tuple可以直接进行比较
if (t1 < t2) {
cout << "t1 < t2" << endl;
} else {
cout << "t1 >= t2" << endl;
}
// 可以直接拷贝构造
t1 = t2;
// 使用tie函数将tuple的元素绑定到变量上
tuple<int, float, string> t3(77, 1.1, "more light");
int i1, float f1; string s1;
tie(i1, f1, s1) = t3;
// 推断 tuple 类型
typedef decltype(t3) TupleType; // 推断出 t3 的类型为 tuple<int, float, string>
// 使用 tuple_size 获取元素个数
cout << tuple_size<TupleType>::value << endl; // 3
// 使用 tuple_element 获取元素类型
tuple_element<1, TupleType>::type fl = 1.0; // float
tuple
类源码分析
容器tuple
的源码使用可变模板参数,递归调用不同模板参数的tuple构造函数,以处理任意多的元素类型.
// 定义 tuple类
template<typename... Values>
class tuple;
// 特化模板参数: 空参
template<>
class tuple<> {};
// 特化模板参数
template<typename Head, typename... Tail>
class tuple<Head, Tail...> :
private tuple<Tail...> // tuple类继承自tuple类,父类比子类少了一个模板参数
{
typedef tuple<Tail...> inherited; // 父类类型
protected:
Head m_head; // 保存第一个元素的值
public:
tuple() {}
tuple(Head v, Tail... vtail) // 构造函数: 将第一个元素赋值给m_head,使用其他元素构建父类tuple
: m_head(v), inherited(vtail...) {}
Head head() { return m_head; } // 返回第一个元素值
inherited &tail() { return *this; } // 返回剩余元素组成的tuple(将当前元素强制转换为父类类型)
};
调用head
函数返回的是元素m_head的值.
调用tail
函数返回父类成分的起点,通过强制转换将当前tuple
转换为父类tuple
,丢弃了元素m_head所占内存.
上述总结的是部分C++ 2.0特性,关于博文内容的视频可以去看侯捷老师的C++新标准 11/14