C++、auto, decltype, constexpr, lambda表达式

1. auto

auto 关键字可以根据初始化值自动推导所定义变量的数据类型,其作用于编译阶段。通常用于复杂类型如迭代器的定义,因为其具体数据类型由于模板的原因可能十分冗长,使用 auto 可以使得代码更加简洁。auto 可能造成一定的代码阅读理解困难,所以如果该变量的类型不是显而易见的最好不要使用。auto 不能作为类普通数据成员的数据类型,除了 (const static) 变量,并且必须在类内初始化。auto 也不能作为函数的参数类型。

但在 C++14 中 auto 可作为返回类型,通常会搭配返回类型后置使用,特别是以函数指针作为返回值时,可以避免手写过于复杂的函数指针类型。比如下面的例子,如果不使用 auto,则返回函数指针类型需要先通过 typedef 定义再使用,会额外增加一些代码量。而且函数指针随着嵌套层数的增加越来越不直观,手动定义又繁琐又容易出错,不如 auto 加上返回类型后置来得直观。

void f1() 
{
	static int cnt = 0;
	cnt++;
	cout << cnt << endl;
}

typedef void (*tf1)();
tf1 f2(char)
{
	return f1;
}

auto f2_1(char) -> void(*)()
{
	return f1;
}

typedef void (*(*tf2)(char))();
tf2 f3(int)
{
	return f2;
}

auto f3_1(int) -> auto (*)(char) -> void(*)() 
{
	return f2;
}

int main()
{
	void (*a)() = f1;
	void (*(*b)(char))() = f2;
	void (*(*(*c)(int))(char))() = f3;
	a();		// 1
	b(1)();		// 2
	c(1)(1)();	// 3

	auto a1 = f1;
	auto b1 = f2;
	auto c1 = f3;
	a1();		// 4
	b1(1)();	// 5
	c1(1)(1)();	// 6

	return 0;
}

注意,auto 搭配返回类型后置通常不是必须的,只用以增加代码的可读性。但在某些情况下,比如返回模板函数时,auto 可能无法推导返回类型,这时就必须通过返回类型后置来告诉编译器。除此以外,对于函数声明,由于编译器不可能获取返回值,所以如果使用 auto 推导则必须搭配返回类型后置,这时函数定义如果使用了 auto 推导则也应该搭配返回类型后置使用。

template <typename T>
T tmp1(T a)
{
	return a + 1;
}

auto tmp2() -> int(*)(int)	// 如果没有返回类型后置编译器不知道该返回什么类型
{
	return tmp1;
}

int x = tmp2()(1);	// 2

2. decltype

decltype 关键字可通过括号内的表达式自动推导结果的数据类型,但不会执行该表达式,同样作用于编译阶段。与 auto 相比不需要对变量进行初始化,可用于变量的声明。decltype 通常也会与 auto 搭配用于模板函数的返回类型后置。

int x = 1;
decltype(x) y = 2;

template <class T1, class T2>
auto add(T1 a, T2 b) -> decltype(a+b)
{
	return a + b;
}

3. nullptr

C++11 中使用 nullptr 来代替 NULL,避免不同系统对 NULL 具体数值的不同定义可能带来的问题,以及 NULL 与整型数据类型可能存在的二义性问题。

4. constexpr

constexpr 即 const expression,主要是告诉编译器其所修饰的变量、函数的结果在编译期就可确定值,可以进行相应的优化以节省内存和时间。而 const 只是表示变量被初始化后不能再修改,因此可能在运行期才能确定值,这时编译器为了安全可能不会对 const 进行优化。

实际上,对于变量和函数,constexpr 有着不同的行为。constexpr 变量必须初始化为编译期可确定的值,任何可能导致编译期无法确定值的表达式都是非法的。constexpr 函数则稍微宽松一些,如果函数返回值不要求在编译期中被使用,那么可以使用非 const 的参数输入,相当于普通的函数,但是函数内部的表达式除了输入的参数以外不允许包含其他编译期不能确定值的变量以及运算。在 C++11 中 constexpr 所修饰的函数只能包含返回语句,在 C++14 中 constexpr 所修饰的函数则可以包含更多的语句。

const int fn1(const int i) {
   return i + 1;
}

constexpr int fn2(int i) {
   // i并不需要用cosnt修饰
   return i + 1;
}

constexpr int fn3(const int &i) {
   // 如果使用引用最好加上const,因为非常量引用的初始值必须是左值,
   // 如果直接使用数字等常量右值作为参数输入,const引用可以自动地进行类型转换。
   // 还可以通过右值引用的方法来处理以上情况
   return i + 1;
}

constexpr int fac(int n) {
   // constexpr函数也可以是递归的
   return n == 1 ? 1 : n * fac(n - 1);
}

int main() {
   int x[fn1(5)];    // MSVC编译失败,g++可以编译通过
   int y[fn2(5)];    // 编译通过,通过constexpr修饰编译器可在编译期确定返回值

   constexpr int a;  // 编译失败,constexpr必须初始化
   int i = 2;
   constexpr int j = i + 1; // 编译失败,表达式中不能包含任何编译期不能确定值的变量
   int k = fn2(i);   // 编译通过,因为编译期不需要使用返回值,相当于普通函数
}

5. range-based for

range-based for 主要用于遍历一个数组或具有 begin() 和 end() 方法的容器,可以避免显式的迭代器使用。

vector<int> x(10);
int cnt = 0;
for(auto it = x.begin(); it < x.end(); it++) 
{
	// 普通的for循环需要明确指明开头和结尾
	*it = cnt++;
}
cnt = 0;
for (auto &xi : x)
{
	// 这种写法与上面等价,但是更加简洁
	xi = cnt++;
}
for( auto xi : x ) 
{ 
	// 注意与迭代器的区别,这时xi是x元素的值复制
	cout << xi << " ";
}
cout << endl;

6. lambda 表达式

lambda 表达式通常用来定义一些比较简单的匿名函数,并且能够像普通变量一样在局部作用域中随用随定义,而无需在文件全局作用域单独进行定义。相比普通函数,由于其定义可放在某个作用域的代码中间,所以其可以直接访问到该作用域的局部变量,而无需通过参数列表传递。另外,像普通局部变量一样,lambda 表达式也有局部的作用域与生命期,可以像普通变量一样赋值,也可以作为返回值返回。但与普通变量不同的是,lambda 表达式的赋值有点像 共享指针,即赋值操作不会在内存中产生新的 lambda 表达式,而是增加其引用计数;当 lambda 表达式离开某个局部作用域时,会导致其引用计数减少;当引用计数为 0,即离开了所有的局部作用域时,lambda 表达式就会被自动销毁,释放其表达式内部定义的局部变量。其通常形式为:

auto fn = [capture] (paras) {statements;}; // 注意最后要有分号
  • [] 用于捕获表达式所处作用域的局部普通变量,空[]代表不使用任何局部普通变量。静态变量不需要捕获。
  • [&] 默认表示表达式内所用到的所有局部普通变量都是以引用方式传递的,
  • [=] 默认表示 lambda 表达式内所用到的所有局部普通变量都是以值传递的。
  • [&, =] 的用法错误,两者不能同时存在。
  • 可以显式写明某个变量的传递方式,但必须与默认方式相反,否则错误。默认方式符号要写在最前面。
  • [&, a] 变量 a 以值传递,其他的以引用方式传递,
  • [=, &a] 变量 a 以引用传递,其他的以值传递。
  • [&, &a] [=, a] [a, &a] 这些都是错误的写法。
  • 当表达式在类成员函数里面时,需要捕获 this 指针(即 [this])才能访问类成员函数或者变量。
  • (paras) 包含了可输入参数的声明,没有参数时也可省略括号,但调用时还是需要括号。
  • 虽然 auto 不能作为函数输入参数类型,但在 C++14 中 lambda 表达式可以使用 auto 作为参数类型。
/* argsort实现示例
*/
int main()
{
    int x[5] = { 10, 3, 1, 9, 5 };
    int idx[5] = { 0, 1, 2, 3, 4 };
	// 对于静态数组,MSVC必须使用引用方式传递,g++两种方式都可以
	// 如果是指针则两种方式都可以,因为指针本身也是一个变量
    auto cmp = [&x](int i1, int i2) { return x[i1] > x[i2]; }; // 这里实现的是从大到小排列
    sort(idx, idx + 5, cmp);
    for (auto i : idx)
    {
        cout << i << ' ';
    }
    cout << endl;
    return 0;
}

因为 lambda 表达式可以在局部作用域定义,并使用局部作用域的变量,所以经常被用来编写函数闭包。例如在 Python 中,我们经常需要用到生成器来持续地生成列表元素,通常通过 next() 方法来获取,这种方法可以减少提前生成整个列表所需的内存。利用 lambda 表达式,我们同样可以用 C++ 写出类似的生成器。下面以斐波那契数列生成器为例:

/* 自定义shared_ptr的释放内存方式
 * 这里主要用来指示程序是否调用了智能指针的析构函数,确认会不会发生内存泄漏。
*/
void Del(int *p)
{
    if (NULL != p)
    {
        cout << "del " << p << endl;
        delete p;
    }
}

/* 基于lambda表达式的斐波那契数列生成器实现
 * 注意C++14才能支持auto返回类型,在g++中编译时需要加上--std=c++14
*/
auto yield_fib_iter()
{
    shared_ptr<int> a(new int(0), Del); // 记录第 n-2 个数
    shared_ptr<int> b(new int(1), Del);	// 记录第 n-1 个数
    auto next = [a, b]()	// 注意不要用引用传递,否则共享指针计数不会加1
    {
        int c = *b;
        *b = *a + *b;
        *a = c;
        return c;
    };
    return next; // 返回lambda表达式
}

void test()
{
    auto fib = yield_fib_iter(); // 定义一个局部的生成器
    for (int i = 0; i < 5; i++)
    {
        cout << fib() << endl;
    }
    // 利用自定义的shared_ptr删除器,我们可以看到当test()结束时,
    // 由于fib属于局部lambda表达式,其离开作用域时也会被销毁,导致shared_ptr的引用计数为0,
    // 从而两个shared_ptr所管理的动态内存也会被自动释放,不会造成内存泄漏
}

int main()
{
	test(); // 测试局部作用域的lambda表达式,你也可以选择返回该表达式
	
    auto fib = yield_fib_iter(); // 定义一个新的生成器,从第一个数开始生成
    for (int i = 0; i < 5; i++)
    {
        cout << fib() << endl;
    }
    return 0;
}

注意,函数闭包并没有改变变量的生命期,当函数结束时,所有函数内部的非静态变量都会被销毁(包括按值传递的参数)。如果 lambda 表达式通过引用来捕获变量,则必须保证该变量在 lambda 表达式被调用时还处在其生命期内。

在以上斐波那契数列生成器的实现中,我们使用了共享指针 shared_ptr 来存储第 n-2 和 n-1个数,当我们调用 yield_fib_iter() 函数并返回 lambda 表达式之后,yield_fib_iter() 函数内部的局部普通变量都会被销毁,包括两个共享指针本身,但是由于 lambda 表达式通过值传递的方式捕获复制了两个共享指针,共享指针所管理的动态内存的引用计数就会加 1,也就是说只要该 lambda 表达式没有被销毁,共享指针的内存引用计数就不会为 0,该内存就能一直被 lambda 表达式有效访问。当 lambda 表达式的生命期结束被销毁时,其内部的局部普通变量也会被销毁,两个共享指针的内存引用计数都变为 0,我们最初分配的动态内存就能被自动释放,而不会造成内存泄漏的问题。注意 auto next = [a, b]() 这里不要用 [&a, &b],因为对共享指针对象本身进行引用是不会增加其所管理的指针的引用计数的,这样一旦 yield_fib_iter() 函数结束,两个共享指针所管理的指针的计数就为 0,从而对应的内存也被销毁了。当我们再次调用 yield_fib_iter() 来获取另一个生成器时,注意此时返回的 lambda 表达式与之前返回的 lambda 表达式是独立的,相当于两个不同的局部变量,因此它们彼此之间不会相互影响,每个生成器都是从斐波那契数列的第一个数开始生成的。

你可能感兴趣的:(C/C++笔记,c++)