C++11一些新特性

C++11一些新特性

一.可变参数模板(Variadic Templates)

新特性允许模板定义中含有任意多个的模板参数,这种新特性的写法与普通模板是差不多的,只是加上了省略号...
省略号的作用:
1. 声明一个参数包,这个参数包可以包含一个或者任意个模板参数。
2. 在模板定义的右边,可以将参数包展开成一个一个独立的参数。
举个简单的例子:

void print() { }
template<class T, class ...Types>
void print(const T& firstArg, const Types&... args) {
    cout << head << endl;
    print(args...);
}

第一个省略号代表模板参数包,第二个省略号代表函数参数类型包,第三个省略号代表函数参数包。给一个调用:

string s("string");
int i = 0;
double d = 1.0;
print(s, i, d);

这个函数的调用过程如下:

print(s, i, d);
print(i, d);
print(d);
print();

输出的结果显然就是:

string
0
1.0

可以使用sizeof...()来查询参数的个数。
实例:典型应用tuple

template<typename... Elements> class tuple;
template<typename Head, typename... Tail>
class tuple : private tuple<Tail...> { 
    Head head; 
};
template<> class type<> {};

第一个tuple,变模板参数,只含有一个模板参数包。
第二个tuple是一个偏特化版本,包含两个参数,一个Headl类型,另一个是模板参数包Tail。
第三个tuple也是一个偏特化版本,不含任何参数,也是边界条件。
例如:

tuple<double, int, char, float> a = make_tuple(0.0, 1, 'a', 1.0);

二.使用智能指针解决内存泄漏

C++资源管理之智能指针

三.lambda表达式

lambda表达式定义了一个匿名函数,语法如下:

[capture] {params} opt -> ret { body; }

capture是捕获列表,允许在lambda表达式直接使用这些值,主要是lambda表达式所在作用域中能访问的值,包括这个作用域里面的临时变量,类的可访问成员,全局变量。捕获方式有两种:按值捕获和按引用捕获。
- []:不捕获任何
- [&]:作用域的所有变量全部按照引用捕获
- [=]:作用域的所有变量全部按照值捕获
- [=, &foo]:foo按照引用捕获,其它全部按照值捕获
- [foo]:只按值捕获foo
- [this]:捕获类中的this指针,在lambda表达式中可以使用类的成员函数和成员变量

params是参数列表,opt是函数选项,ret是返回值类型,body是匿名函数体。

auto f = [](int a) -> int { return a + 1; };
//f -> int (*)(int);

lambda表达式在C++11中被称为“闭包类型(Closure Type)”,可以认为是带有operator()的类,即仿函数。
举个算法的例子:
leetcode 179. Largest Number,用lambda表达式就非常简单:

class Solution {
public:
    string largestNumber(vector<int>& nums) {
        string re = "";
        std::sort(nums.begin(), nums.end(), [](int i, int j)
            { return std::to_string(i) + std::to_string(j) >= std::to_string(j) + std::to_string(i); });
        if(nums[0] == 0) return "0";
        for(auto num : nums) {
            re += std::to_string(num);
        }
        return re;
    }
};

四.右值引用与移动语意

右值引用(R-value reference),记为T &&。我们先来学习一下什么是左值,什么是右值。
- 左值:拥有明确内存范围的值,可以取地址
- 右值:需要储存到对象的值,无法取地址。右值又包括将亡值和纯右值,纯右值包括临时对象,原始字面值和lambda表达式,将亡值包括被移动的对象,T&&返回值,std::move()返回值等。

不管是右值引用还是左值引用,都必须初始化。
右值引用不能绑定到任何左值,同样地,左值引用也不能绑定到任何右值引用,特别地,常量左值引用能够绑定到任何值。

const bool &judge1 = true;  //常量左值引用绑定到纯右值
const bool judge2 = true;   //赋值语句

两者的区别在于第二个赋值语句,表达式结束之后,纯右值就被销毁。

右值引用的作用: 减少临时变量拷贝的开销
1. std::move
显式将转化为右值引用,相当于执行static_cast<T&&>(lvalue),这个时候左值变量lvalue不会被立即析构。实际的实现如下:

template<typename T> 
typename remove_reference<T>::type &&move(T &&param) {
    return static_cast<typename remove_reference<T>::type&&>(param);
}

无条件将param转化为右值引用。
2. 折叠引用

X & &   => X &
X && &  => X &
X & &&  => X &
X && && => X &&

举个例子:

void foo(T &&val);
void f(const T &val);
int i = 0;
const int ci = i;
f(i);       //T => int & 
f(ci);      //T => const int &
f(ci*i);    //T => int && 因为ci*i返回的是右值
  1. std::forward
    利用引用折叠完成完美转发:
template<typename T> 
T &&forward(typename remove_reference<T>::type &&param) {
    return static_cast<T &&>(param);
}

通常,如果实参是一个引用参数,根据折叠引用的原理,参数会被转换为右值引用,这样就不会有拷贝的开销。
如果实参不是引用参数,还是有可能会有临时对象的产生。

五.其它

这一节主要参考自《深入理解C++ 11代码优化与工程级应用》,见其百度学术
1. 类型推导:auto与decltype
在介绍这个之前,介绍一下存储类标识符和类型限定符
存储类标识符包括:
- auto:自动存储期与无链接(C++ 11前)
- resigiter:自动存储期与无链接(C++ 17弃用)
- static:静态存储期与内部链接
- extern:静态存储期与外部链接

例如:

auto int i = 0;
static int j = 0;

存储期涉及到变量的生存周期(lifetime)
- 自动存储期:一般是局部变量,由对象所处的{}决定
- 静态存储期:一般是全局变量,程序开始前被创建和初始化,覆盖整个程序的执行过程。

标识符的链接:
- 无链接:作用域一般在函数内部,例如函数内部的auto变量和register变量,以及函数的参数。
- 内部链接:当前文件中相同的对象,其它文件无法对其访问,例如函数体外声明的static变量。
- 外部链接:整个程序(多个文件)相同的对象,例如函数体外声明的extern变量。

extern “C”的作用:实现C和C++混合编程。
extern “C”仅仅指定编译和链接,不改变语义,否则以C++的方式进行编译,例如没有声明extern “C”的函数按照C++的编译方式,进行name mangling和NRV优化。

类型限定符(cv):包括const和volatile。
const参见条款3.尽可能使用const
volatile:修饰的变量,防止编译器对访问该变量时进行优化,直接从原始内存中读取。

类型推导中的auto是C++ 11后才有的。
使用auto声明的变量必须立马初始化,以让编译器推断其实际类型,并且编译时auto占位符被替换成真正的类型。
主要的两个规则
- 当声明不是指针或者引用时,auto的推导结果和初始化表达式抛弃引用和cv限定符后的类型一致
- 当声明为指针或者引用时,auto的推导结果将保留初始化表达式的cv属性
例如:

const int& i = 0;
auto m = i; //m -> int
auto& n =  i; //n -> const int&

限制
- 不能用于函数参数
- 不能用于非静态成员变量
- 无法定义数组(数组退化成指针)
- 无法推导模板参数

例如:

void fun(auto a = 1) {} //错误,不能用于函数参数
class foo {
    auto v1 = 0; //错误,不能用于非静态成员变量
    static auto v2 = 0;
};
int arr[5] = {0};
auto a1 = arr;  //可以,只是数组退化成指针,a1 -> int*
auto a2[10] = arr;  //错误,无法定义数组
Bar<int> bar;   //Bar是一个模板类
Bar<auto> b = bar;  //错误,无法推导模板参数 

主要的使用场景
使用STL容器的迭代器时:

vector<string> vs;
auto iter1 = vs.begin();    //简单
vector<string>::iterator iter2 = vs.begin();

decltype关键字
用法:decltype(exp),在编译期完成。推导规则如下:
- exp是函数调用时,decltype(exp)与函数的返回值类型一致(当返回值是引用类型或者类类型时,才会保留cv限定符)。
- exp是标识符表达式或者类访问表达式时,decltype(exp)和exp的类型一致
- 其它,exp是一个左值,那么decltype(exp)就是exp类型的左值引用,否则和exp的类型一致
例如:

//情况一
Struct Foo {
    static const int Num = 0;
    int x;
}foo;
int n = 0;
volatile const int& x = n;
decltype(n) a = n;  //a -> int
decltype(x) b = n;  //b -> volatile const int&
decltype(foo.Num) c = 0;//c ->const int
decltype(foo.x) d = 0; //d -> int
//情况二
int func_int(void);
int&& func_int_rr(void);
const int func_cint(void);
const int&& func_cint_rr(void);//返回值是右值引用类型
const Foo func_cfoo(void);  //返回值是类类型

int x = 0;

decltype(func_int()) a1 = x;//a1 -> int
decltype(func_int_rr()) b1 = 0;//b1 -> int&&
decltype(func_cint()) a2 = x;//a2 -> int
decltype(func_cint_rr()) b2 = 0;//b2 -> const int&&
decltype(func_cfoo()) c = Foo();//c -> const Foo
//情况三:带括号的表达式和运算表达式
const Foo foo1 = Foo();
decltype(foo1.x) a = 0;//a -> int
decltype((foo1.x)) b = a;//b -> const int&

int n = 0, m = 0;
decltype(n + m) c = 0;//n+m返回右值 c -> int
decltype(n += m) d = c;//n+=m返回左值 d -> int&

引用场景:尾置返回语法

typename<typename T, typename U> 
auto add(T t, U u) -> decltype(t + u) {
    return t + u;
}
  1. 列表初始化
  2. 范围for循环

猜你喜欢

转载自blog.csdn.net/qq_25467397/article/details/80878232