C++ 11 常用新特性
C++11 常用新特性(一)
最近工作中,遇到一些问题,使用
nullptr
((void *)0)
,有些则会直接将其定义为
void *
隐式转换到其他类型,但如果char *ch = NULL;
时,
而这依然会产生问题,将导致了
void foo(char *);
void foo(int);
对于这两个函数来说,如果
当需要使用
类型推导
auto
使用
for(vector<int>::const_iterator itr = vec.cbegin(); itr != vec.cend(); ++itr)
而有了
// 由于 cbegin() 将返回 vector<int>::const_iterator
// 所以 itr 也应该是 vector<int>::const_iterator 类型
for(auto itr = vec.cbegin(); itr != vec.cend(); ++itr);
一些其他的常见用法:
auto i = 5; // i 被推导为 int
auto arr = new auto(10) // arr 被推导为 int *
注意:
int add(auto x, auto y);
此外,
#include <iostream>
int main() {
auto i = 5;
int arr[10] = {0};
auto auto_arr = arr;
auto auto_arr2[10] = arr;
return 0;
}
decltype
decltype(表达式)
在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。
有时候,我们可能需要计算某个表达式的类型,例如:
auto x = 1;
auto y = 2;
decltype(x+y) z;
拖尾返回类型、auto 与decltype 配合
你可能会思考,
template<typename R, typename T, typename U>
R add(T x, U y) {
return x+y
}
这样的代码其实变得很丑陋,因为程序员在使用这个模板函数的时候,必须明确指出返回类型。但事实上我们并不知道
在
decltype(x+y) add(T x, U y);
但事实上这样的写法并不能通过编译。这是因为在编译器读到
template<typename T, typename U>
auto add(T x, U y) -> decltype(x+y) {
return x+y;
}
从
template<typename T, typename U>
auto add(T x, U y) {
return x+y;
}
区间迭代
基于范围的for 循环
std::vector<int> arr(5, 100);
for(std::vector<int>::iterator i = arr.begin(); i != arr.end(); ++i) {
std::cout << *i << std::endl;
}
变得非常的简单:
// & 启用了引用
for(auto &i : arr) {
std::cout << i << std::endl;
}
初始化列表
struct A {
int a;
float b;
};
struct B {
B(int _a, float _b): a(_a), b(_b) {}
private:
int a;
float b;
};
A a {1, 1.1}; // 统一的初始化语法
B b {2, 2.2};
std::initializer_list
,允许构造函数或其他函数像参数一样使用初始化列表,这就为类对象的初始化与普通数组和
#include <initializer_list>
class Magic {
public:
Magic(std::initializer_list<int> list) {}
};
Magic magic = {1,2,3,4,5};
std::vector<int> v = {1, 2, 3, 4};
模板增强
外部模板
传统
template class std::vector<bool>; // 强行实例化
extern template class std::vector<double>; // 不在该编译文件中实例化模板
尖括号 “>”
在传统>>
一律被当做右移运算符来进行处理。但实际上我们很容易就写出了嵌套模板的代码:
std::vector<std::vector<int>> wow;
这在传统
类型别名模板
在传统
template< typename T, typename U, int value>
class SuckType {
public:
T a;
U b;
SuckType():a(value),b(value){}
};
template< typename U>
typedef SuckType<std::vector<int>, U, 1> NewType; // 不合法
template <typename T>
using NewType = SuckType<int, T, 1>; // 合法
默认模板参数
我们可能定义了一个加法函数:
template<typename T, typename U>
auto add(T x, U y) -> decltype(x+y) {
return x+y
}
但在使用时发现,要使用
在
template<typename T = int, typename U = int>
auto add(T x, U y) -> decltype(x+y) {
return x+y;
}
构造函数
委托构造
class Base {
public:
int value1;
int value2;
Base() {
value1 = 1;
}
Base(int value) : Base() { // 委托 Base() 构造函数
value2 = 2;
}
};
继承构造
在继承体系中,如果派生类想要使用基类的构造函数,需要在构造函数中显式声明。
假若基类拥有为数众多的不同版本的构造函数,这样,在派生类中得写很多对应的“透传”构造函数。如下:
struct A
{
A(int i) {}
A(double d,int i){}
A(float f,int i,const char* c){}
//...等等系列的构造函数版本
};
struct B:A
{
B(int i):A(i){}
B(double d,int i):A(d,i){}
B(folat f,int i,const char* c):A(f,i,e){}
//......等等好多个和基类构造函数对应的构造函数
};
struct A
{
A(int i) {}
A(double d,int i){}
A(float f,int i,const char* c){}
//...等等系列的构造函数版本
};
struct B:A
{
using A::A;
//关于基类各构造函数的继承一句话搞定
//......
};
如果一个继承构造函数不被相关的代码使用,编译器不会为之产生真正的函数代码,这样比透传基类各种构造函数更加节省目标代码空间。
新增容器
std::array
std::array<int, 4> arr= {1,2,3,4};
int len = 4;
std::array<int, len> arr = {1,2,3,4}; // 非法, 数组大小参数必须是常量表达式
当我们开始用上了
void foo(int *p, int len) {
return;
}
std::array<int 4> arr = {1,2,3,4};
// C 风格接口传参
// foo(arr, arr.size()); // 非法, 无法隐式转换
foo(&arr[0], arr.size());
foo(arr.data(), arr.size());
// 使用 `std::sort`
std::sort(arr.begin(), arr.end());
std::forward_list
和
无序容器
std::unordered_map/std::unordered_multimap
和 std::unordered_set/std::unordered_multiset
。
无序容器中的元素是不进行排序的,内部通过
元组std::tuple
元组的使用有三个核心的函数:
std::make_tuple
std::get
std::tie
#include <tuple>
#include <iostream>
auto get_student(int id)
{
// 返回类型被推断为 std::tuple<double, char, std::string>
if (id == 0)
return std::make_tuple(3.8, 'A', "张三");
if (id == 1)
return std::make_tuple(2.9, 'C', "李四");
if (id == 2)
return std::make_tuple(1.7, 'D', "王五");
return std::make_tuple(0.0, 'D', "null");
// 如果只写 0 会出现推断错误, 编译失败
}
int main()
{
auto student = get_student(0);
std::cout << "ID: 0, "
<< "GPA: " << std::get<0>(student) << ", "
<< "成绩: " << std::get<1>(student) << ", "
<< "姓名: " << std::get<2>(student) << '\n';
double gpa;
char grade;
std::string name;
// 元组进行拆包
std::tie(gpa, grade, name) = get_student(1);
std::cout << "ID: 1, "
<< "GPA: " << gpa << ", "
<< "成绩: " << grade << ", "
<< "姓名: " << name << '\n';
}
合并两个元组,可以通过
auto new_tuple = std::tuple_cat(get_student(1), std::move(t));
正则表达式
正则表达式描述了一种字符串匹配的模式。一般使用正则表达式主要是实现下面三个需求:
-
检查一个串是否包含某种形式的子串;
-
将匹配的子串替换;
-
从某个串中取出符合条件的子串。
我们通过一个简单的例子来简单介绍这个库的使用。考虑下面的正则表达式:
#include <iostream>
#include <string>
#include <regex>
int main() {
std::string fnames[] = {"foo.txt", "bar.txt", "test", "a0.txt", "AAA.txt"};
// 在 C++ 中 `\` 会被作为字符串内的转义符,为使 `\.` 作为正则表达式传递进去生效,需要对 `\` 进行二次转义,从而有 `\\.`
std::regex txt_regex("[a-z]+\\.txt");
for (const auto &fname: fnames)
std::cout << fname << ": " << std::regex_match(fname, txt_regex) << std::endl;
}
另一种常用的形式就是依次传入
std::regex base_regex("([a-z]+)\\.txt");
std::smatch base_match;
for(const auto &fname: fnames) {
if (std::regex_match(fname, base_match, base_regex)) {
// sub_match 的第一个元素匹配整个字符串
// sub_match 的第二个元素匹配了第一个括号表达式
if (base_match.size() == 2) {
std::string base = base_match[1].str();
std::cout << "sub-match[0]: " << base_match[0].str() << std::endl;
std::cout << fname << " sub-match[1]: " << base << std::endl;
}
}
}
以上两个代码段的输出结果为:
foo.txt: 1
bar.txt: 1
test: 0
a0.txt: 0
AAA.txt: 0
sub-match[0]: foo.txt
foo.txt sub-match[1]: foo
sub-match[0]: bar.txt
bar.txt sub-match[1]: bar
语言级线程支持
std::thread
std::mutex/std::unique_lock
std::future/std::packaged_task
std::condition_variable
C++11 常用新特性( 二)
Lambda 表达式
Lambda 表达式的基本语法如下:
[ caputrue ] ( params ) opt -> ret { body; };
-
capture 是捕获列表; -
params 是参数表;( 选填) -
opt 是函数选项;可以填mutable
, exception
, attribute
(选填)
-
mutable 说明lambda 表达式体内的代码可以修改被捕获的变量,并且可以访问被捕获的对象的non-const 方法。 -
exception 说明lambda 表达式是否抛出异常以及何种异常。 -
attribute 用来声明属性。
ret 是返回值类型(拖尾返回类型) 。( 选填) body 是函数体。
捕获列表:
-
[] 不捕获任何变量。 -
[&] 捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获) 。 -
[=] 捕获外部作用域中所有变量,并作为副本在函数体中使用( 按值捕获) 。注意值捕获的前提是变量可以拷贝,且被捕获的量在lambda 表达式被创建时拷贝,而非调用时才拷贝。如果希望lambda 表达式在调用时能即时访问外部变量,我们应当用引用方式捕获。
int a = 0;
auto f = [=] { return a; };
a+=1;
cout << f() << endl; //输出0
int a = 0;
auto f = [&a] { return a; };
a+=1;
cout << f() <<endl; //输出1
-
[=,&foo] 按值捕获外部作用域中所有变量,并按引用捕获foo 变量。 -
[bar] 按值捕获bar 变量,同时不捕获其他变量。 -
[this] 捕获当前类中的this 指针,让lambda 表达式拥有和当前类成员函数同样的访问权限。如果已经使用了& 或者= ,就默认添加此选项。捕获this 的目的是可以在lamda 中使用当前类的成员函数和成员变量。
class A
{
public:
int i_ = 0;
void func(int x,int y){
auto x1 = [] { return i_; }; //error,没有捕获外部变量
auto x2 = [=] { return i_ + x + y; }; //OK
auto x3 = [&] { return i_ + x + y; }; //OK
auto x4 = [this] { return i_; }; //OK
auto x5 = [this] { return i_ + x + y; }; //error,没有捕获x,y
auto x6 = [this, x, y] { return i_ + x + y; }; //OK
auto x7 = [this] { return i_++; }; //OK
};
int a=0 , b=1;
auto f1 = [] { return a; }; //error,没有捕获外部变量
auto f2 = [&] { return a++ }; //OK
auto f3 = [=] { return a; }; //OK
auto f4 = [=] {return a++; }; //error,a是以复制方式捕获的,无法修改
auto f5 = [a] { return a+b; }; //error,没有捕获变量b
auto f6 = [a, &b] { return a + (b++); }; //OK
auto f7 = [=, &b] { return a + (b++); }; //OK
注意
int a = 0;
auto f1 = [=] { return a++; }; //error
auto f2 = [=] () mutable { return a++; }; //OK
原因:
lambda 表达式的大致原理:
每当你定义一个
lambda 表达式是不能被赋值的:
auto a = [] { cout << "A" << endl; };
auto b = [] { cout << "B" << endl; };
a = b; // 非法,lambda无法赋值
auto c = a; // 合法,生成一个副本
闭包类型禁用了赋值操作符,但是没有禁用复制构造函数,所以你仍然可以用一个
在多种捕获方式中,最好不要使用
默认引用捕获所有变量,你有很大可能会出现悬挂引用(Dangling references
std::function<int(int)> add_x(int x)
{
return [&](int a) { return x + a; };
}
上面函数返回了一个
但是采用默认值捕获所有变量仍然有风险,看下面的例子:
class Filter
{
public:
Filter(int divisorVal):
divisor{divisorVal}
{}
std::function<bool(int)> getFilter()
{
return [=](int value) {return value % divisor == 0; };
}
private:
int divisor;
};
这个类中有一个成员方法,可以返回一个
// 类的方法,下面无法编译,因为divisor并不在lambda捕捉的范围
std::function<bool(int)> getFilter()
{
return [divisor](int value) {return value % divisor == 0; };
}
原代码中,
std::function<bool(int)> getFilter() { return [this](int value) {return value % this->divisor == 0; };}
尽管还是以值方式捕获,但是捕获的是指针,其实相当于以引用的方式捕获了当前类对象,所以
std::function<bool(int, int)> wrapper = [](int x, int y) { return x < y; };
最常用的是在
int value = 3;
vector<int> v {1, 3, 5, 2, 6, 10};
int count = std::count_if(v.beigin(), v.end(), [value](int x) { return x > value; });
再比如你想生成斐波那契数列,然后保存在数组中,此时你可以使用
vector<int> v(10);
int a = 0;
int b = 1;
std::generate(v.begin(), v.end(),
[&a, &b] { int value = b; b = b + a; a = value; return value; });
// 此时v {1, 1, 2, 3, 5, 8, 13, 21, 34, 55}
当需要遍历容器并对每个元素进行操作时:
std::vector<int> v = { 1, 2, 3, 4, 5, 6 };
int even_count = 0;
for_each(v.begin(), v.end(), [&even_count](int val){
if(!(val & 1)){
++ even_count;
}
});
std::cout << "The number of even is " << even_count << std::endl;
大部分
C++ std::function
-
函数。
-
lamada 表达式。 -
绑定表达式或其他函数对象。
-
指向成员函数和指向数据成员的指针。
当std::function
对象没有初始化任何实际的可调用元素,调用std::function
对象将抛出std::bad_function_call
异常。
std::function 简介
类模版
通常
这里我们大概总结一下。
Member types
成员类型 | 说明 |
---|---|
result_type | 返回类型 |
argument_type | 如果函数对象只有一个参数,那么这个代表参数类型。 |
first_argument_type | 如果函数对象有两个个参数,那么这个代表第一个参数类型。 |
second_argument_type | 如果函数对象有两个个参数,那么这个代表第二个参数类型。 |
Member functions
成员函数声明 | 说明 |
---|---|
constructor | 构造函数:constructs a new std::function instance |
destructor | 析构函数: destroys a std::function instance |
operator= | 给定义的 |
operator bool | 检查定义的 |
operator() | 调用一个对象 |
std::function 使用
封装普通函数例子:
#include <iostream>#include <vector>#include <list>#include <map>#include <set>#include <string>#include <algorithm>#include <functional>#include <memory>using namespace std;typedef std::function<int(int)> Functional;int TestFunc(int a) { return a; }int main(){ Functional obj = TestFunc; int res = obj(1); std::cout << res << std::endl; while(1); return 0;}
封装
#include <iostream>#include <vector>#include <list>#include <map>#include <set>#include <string>#include <algorithm>#include <functional>#include <memory>using namespace std;typedef std::function<int(int)> Functional;auto lambda = [](int a)->int{return a;};int main(){ Functional obj = lambda; res = obj(2); std::cout << res << std::endl; while(1); return 0;}
封装仿函数:
#include <iostream>#include <vector>#include <list>#include <map>#include <set>#include <string>#include <algorithm>#include <functional>#include <memory>using namespace std;typedef std::function<int(int)> Functional;class Functor{public: int operator()(int a) { return a; }};int main(){ Functor func; Functional obj = func; res = obj(3); std::cout << res << std::endl; while(1); return 0;}
封装类的成员函数和
#include <iostream>
#include <vector>
#include <list>
#include <map>
#include <set>
#include <string>
#include <algorithm>
#include <functional>
#include <memory>
using namespace std;
typedef std::function<int(int)> Functional;
class CTest
{
public:
int Func(int a)
{
return a;
}
static int SFunc(int a)
{
return a;
}
};
int main()
{
CTest t;
obj = std::bind(&CTest::Func, &t, std::placeholders::_1);
res = obj(3);
cout << "member function : " << res << endl;
obj = CTest::SFunc;
res = obj(4);
cout << "static member function : " << res << endl;
while(1);
return 0;
}
关于可调用实体转换为
- 转换后的
std::function 对象的参数能转换为可调用实体的参数; - 可调用实体的返回值能转换为
std::function 对象的返回值。
为什么要用
好用并实用的东西才会加入标准的。因为好用,实用,我们才在项目中使用它。
参考文档:
右值引用和move 语义
先看一个简单的例子直观感受下:
string a(x); // line 1
string b(x + y); // line 2
string c(some_function_returning_a_string()); // line 3
如果使用以下拷贝构造函数:
string(const string& that)
{
size_t size = strlen(that.data) + 1;
data = new char[size];
memcpy(data, that.data, size);
}
以上
第二行和第三行的参数则是右值,因为表达式产生的
string(string&& that) // string&& is an rvalue reference to a string
{
data = that.data;
that.data = 0;
}
我们没有深度拷贝堆内存中的数据,而是仅仅复制了指针,并把源对象的指针置空。事实上,我们“偷取”了属于源对象的内存数据。由于源对象是一个右值,不会再被使用,因此客户并不会觉察到源对象被改变了。在这里,我们并没有真正的复制,所以我们把这个构造函数叫做“转移构造函数”(move constructor
有了右值引用,再来看看赋值操作符:
string& operator=(string that)
{
std::swap(data, that.data);
return *this;
}
注意到我们是直接对参数
如果是
如果是
总结一下:复制构造函数执行的是深度拷贝,因为源对象本身必须不能被改变。而转移构造函数却可以复制指针,把源对象的指针置空,这种形式下,这是安全的,因为用户不可能再使用这个对象了。
下面我们进一步讨论右值引用和
std::auto_ptr
,该类型在
auto_ptr<Shape> a(new Triangle);auto_ptr<Shape> b(a);
注意
auto_ptr(auto_ptr& source) // note the missing const
{
p = source.p;
source.p = 0; // now the source no longer owns the object
}
auto_ptr<Shape> make_triangle()
{
return auto_ptr<Shape>(new Triangle);
}
auto_ptr<Shape> c(make_triangle()); // move temporary into c
double area = make_triangle()->area(); // perfectly safe
auto_ptr<Shape> a(new Triangle); // create triangle
auto_ptr<Shape> b(a); // move a into b
double area = a->area(); // undefined behavior
显然,在持有
转移像
我们现在知道转移左值是十分危险的,但是转移右值却是很安全的。如果
使用右值引用
其转移构造函数:
unique_ptr(unique_ptr&& source) // note the rvalue reference
{
ptr = source.ptr;
source.ptr = nullptr;
}
这个转移构造函数跟
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // error
unique_ptr<Shape> c(make_triangle()); // okay
第二行不能编译通过,因为
转移左值
有时候,我们可能想转移左值,也就是说,有时候我们想让编译器把左值当作右值对待,以便能使用转移构造函数,即便这有点不安全。出于这个目的,
以下是如何正确的转移左值:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // still an error
unique_ptr<Shape> c(std::move(a)); // okay
请注意,第三行之后,
当然,如果你在使用了
总之,
一个例子:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(parameter) // error
{}
};
上面的
因此以上对std::move
来显示转换成右值。