现代C++学习笔记——第2章 语言可用性的强化

本文介绍了C++17中常量表达式、初始化列表、auto和decltype类型推导、控制流增强(if constexpr和区间for迭代)、模板改进(外部模板、类型别名模板、默认模板参数和变长参数模板)等,展示了现代C++在提升代码可读性和性能上的革新。
摘要由CSDN通过智能技术生成

2.1常量

在新版的C++标准中,使用nullptr来替代NULL指针, C++会把NULL和0视为同一各东西,这由编译器决定,有些编译器会将NULL定义为((void*)0),有些则会直接其其定义为0。在C++中是不允许直接将void*隐式转到其他类型的。但如果编译器把NULL定义为((void*)0),就会出现下面这段代码中

char *ch = NULL;                  //char *ch = ((void*)0);

没有了void *隐式转换的C++只好将NULL定义为0,

这将导致C++中重载特性发生混乱,看下面两个foo函数调用

void foo(char *);

void foo(int);

#include <iostream>
#include <type_traits>

void foo(char *)
{
    std::cout << "foo(char*) is called" << std::endl;
}

void foo(int)
{
    std::cout << "foo(int) is called" << std::endl;
}

int main()
{
    if(std::is_same<decltype(NULL), decltype(0)>::value)
        std::cout << "NULL == 0" << std::endl;
    if(std::is_same<decltype(NULL), decltype((void*)0)>::value)
        std::cout << "NULL == (void*)0" << std::endl;
    if(std::is_same<decltype(NULL), std::nullptr_t>::value)
        std::cout << "NULL == std::nullptr_t" << std::endl;

    foo(0);             //will call foo(int)
    foo(NULL);          // NULL == 0 will call foo(int)
    foo(nullptr);       //will call foo(char*)

    std::cout << "2_01_nullptr.cpp" << std::endl;
    return 0;
}

foo(NULL)调用了foo(int)的函数,这与原本调用foo(char*)函数相违背。

上面的代码中,我们使用了 decltype 和 std::is_same 这两个属于现代 C++ 的语法,简单来说, decltype 用于类型推导,而 std::is_same 用于比较两个类型是否相同。

constexpr

c++本身已经具备了常量表达式的概念,比如 1+2, 3*4这种表达式总是会产相同的结果并且没有副作用。如果编译器能够在编译时把这些表达式真接优化并植入到程序运行时,将能增加程序的性能。一个非常明显的例子就是在数组的定义阶段:

#include <iostream>

#define LEN 10

int len_foo()
{
    int i = 2;
    return i;
}

constexpr int len_foo_constexpr()
{
    return 5;
}

constexpr int fibonacci(const int n)
{
    //return n == 1 || n == 2 ? 1 : fibonacci(n - 1) + fibonacci(n - 2);

    if(1 == n)
        return 1;
    if(2 == n)
        return 1;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

int main()
{
    char arr_1[10];//合法
    char arr_2[LEN];//合法

    int len = 10;
    //char arr_3[len];//非法

    const int len_2 = len + 1;
    constexpr int len_2_constexpr = 1 + 2 + 3;

    //char arr_4[len_2];//非法

    char arr4[len_2_constexpr];//合法

    //char arr_5[len_foo() + 5];//非法
    char arr_6[len_foo_constexpr() + 1];//合法

    std::cout << fibonacci(10) << std::endl;
    //1, 1, 2, 3, 5, 8, 13, 21, 34, 55
    std::cout << fibonacci(10) << std::endl;
    std::cout << "Hello World!" << std::endl;
    return 0;
}

const是定义一个常数,constexpr是表示一个常量表达式,在C++标准中数组的长度必须是常量表达式,所以char arr_4[len_2]是非法的。

虽然constexpr定义的是一个常表达式,比如可以用来表示一天多少分钟,可以constexpr int m = 24 * 60;

C++11 提供了 constexpr 让用户显式的声明函数或对象构造函数在编译期会成为常量表达式,这

个关键字明确的告诉编译器应该去验证 len_foo 在编译期就应该是一个常量表达式。

此外, constexpr 修饰的函数可以使用递归:

constexpr int fibonacci(const int n) {
return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2);
}


从 C++14 开始, constexpr 函数可以在内部使用局部变量、循环和分支等简单语句,例如下面的
代码在 C++11 的标准下是不能够通过编译的:

(注:qt vs2017可以编译通过)

constexpr int fibonacci(const int n) {
if(n == 1) return 1;
if(n == 2) return 1;
return fibonacci(n-1) + fibonacci(n-2);
}

2.2 变量及其初始化
if/switch 变量声明强化
在传统 C++ 中,变量的声明虽然能够位于任何位置,甚至于 for 语句内能够声明一个临时变量
int,但始终没有办法在 if 和 switch 语句中声明一个临时的变量。例如:
 

#include <iostream>
#include <vector>
#include <algorithm>

int main()
{
    std::vector<int> vec = {1,2,3,4};

    //在C++17之前
    const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 2);
    if(itr != vec.end())
    {
        *itr = 3;
    }
    //vec = {1,3,3,4}

    //需要重新定义一个新的变量
    const std::vector<int>::iterator itr2 = std::find(vec.begin(), vec.end(), 3);
    if(itr2 != vec.end())
    {
        *itr2 = 4;
    }

    
    for(std::vector<int>::iterator element = vec.begin(); element != vec.end(); ++element)
    {
        std::cout << *element << std::endl;
    }

    std::cout << "2_03_if_switch.cpp" << std::endl;
    return 0;
}

在上面的代码中,我们可以看到 itr 这一变量是定义在整个 main() 的作用域内的,这导致当我们
需要再次遍历整个 std::vectors 时,需要重新命名另一个变量。 C++17 消除了这一限制,使得我们可以在 if(或 switch)中完成这一操作:
 

//将临时变量放到if语句内  C++17支持
    if(const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 3); itr != vec.end())
    {
        *itr = 4;
    }

怎么样,是不是和 Go 语言很像?

初始化列表
初始化是一个非常重要的语言特性,最常见的就是在对象进行初始化时进行使用。在传统 C++ 中,不同的对象有着不同的初始化方法,例如普通数组、 POD (Plain Old Data,即没有构造、析构和虚函数的类或结构体)类型都可以使用 {} 进行初始化,也就是我们所说的初始化列表。而对于类对象的初始化,要么需要通过拷贝构造、要么就需要使用 () 进行。这些不同方法都针对各自对象,不能通用。例如:
 

#include <iostream>
#include <vector>

class Foo
{
public:
    int value_a;
    int value_b;
    Foo(int a, int b):value_a(a), value_b(b){}
};


int main()
{
    //before c++11
    int arr[3] = {1,2,3};
    Foo foo(1, 2);
    std::vector<int> vec = {1,2,3,4,5};

    std::cout << "arr[0]:" << arr[0] << std::endl;
    std::cout << "foo:" << foo.value_a << "," << foo.value_b << std::endl;
    for(std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it)
    {
        std::cout << *it << "\t";
    }
    std::cout << std::endl;
    std::cout << "2_04_initializer_list.cpp" << std::endl;
    return 0;
}

为了解决这个问题, C++11 首先把初始化列表的概念绑定到了类型上,并将其称之为 std::initializer_list,允许构造函数或其他函数像参数一样使用初始化列表,这就为类对象的初始化与普通数组和 POD 的初始化方法提供了统一的桥梁,例如:
 

#include <initializer_list>
#include <vector>

class MagicFoo
{
public:
    std::vector<int> vec;
    MagicFoo(std::initializer_list<int> list) 
    {
        for (std::initializer_list<int>::iterator it = list.begin();it != list.end(); ++it)
            vec.push_back(*it);
    }
};

int main() {
    // after C++11
    MagicFoo magicFoo = {1, 2, 3, 4, 5};
    std::cout << "magicFoo: ";
    for (std::vector<int>::iterator it = magicFoo.vec.begin(); it != magicFoo.vec.end();++it) 
        std::cout << *it << "\t";
    std::cout << std::endl;
    return 0;
}

这种构造函数被叫做初始化列表构造函数,具有这种构造函数的类型将在初始化时被特殊关照。
初始化列表除了用在对象构造上,还能将其作为普通函数的形参,例如:
 

public:
    void foo(std::initializer_list<int> list)
    {
        for(std::initializer_list<int>::iterator it = list.begin(); it != list.end(); ++it)
            vec.push_back(*it);
    }

magicFoo.foo({6,7,8,9});
其次, C++11 还提供了统一的语法来初始化任意的对象,例如:
Foo foo2 {3, 4};
合并代码运行结果如下:

 结构化绑定
结构化绑定提供了类似其他语言中提供的多返回值的功能。在容器一章中,我们会学到 C++11 新
增了 std::tuple 容器用于构造一个元组,进而囊括多个返回值。但缺陷是, C++11/14 并没有提供一种简单的方法直接从元组中拿到并定义元组中的元素,尽管我们可以使用 std::tie 对元组进行拆包,但我们依然必须非常清楚这个元组包含多少个对象,各个对象是什么类型,非常麻烦。
C++17 完善了这一设定,给出的结构化绑定可以让我们写出这样的代码:
 

#include <iostream>
#include <tuple>
#include <string>

std::tuple<int, double, std::string> f()
{
    return std::make_tuple(1, 3.2, "768");
}

int main()
{
    auto [x, y, z] = f();
    std::cout << x << ", " << y << ", " << z << std::endl;
    std::cout << "2_05_structure_binding.cpp" << std::endl;
    return 0;
}

运行结果

2.3 类型推导
在传统 C 和 C++ 中,参数的类型都必须明确定义,这其实对我们快速进行编码没有任何帮助,尤
其是当我们面对一大堆复杂的模板类型时,必须明确的指出变量的类型才能进行后续的编码,这不仅拖慢我们的开发效率,也让代码变得又臭又长。
C++11 引入了 auto 和 decltype 这两个关键字实现了类型推导,让编译器来操心变量的类型。这
使得 C++ 也具有了和其他现代编程语言一样,某种意义上提供了无需操心变量类型的使用习惯。
auto
auto 在很早以前就已经进入了 C++,但是他始终作为一个存储类型的指示符存在,与 register 并
存。在传统 C++ 中,如果一个变量没有声明为 register 变量,将自动被视为一个 auto 变量。而随着register 被弃用(在 C++17 中作为保留关键字,以后使用,目前不具备实际意义),对 auto 的语义变更也就非常自然了。
使用 auto 进行类型推导的一个最为常见而且显著的例子就是迭代器。你应该在前面的小节里看到
了传统 C++ 中冗长的迭代写法:
 

// 在 C++11 之前
// 由于 cbegin() 将返回 vector<int>::const_iterator
// 所以 itr 也应该是 vector<int>::const_iterator 类型
for(vector<int>::const_iterator it = vec.cbegin(); itr != vec.cend(); ++it)

而有了 auto 之后可以:
 

#include <initializer_list>
#include <vector>
#include <iostream>

class MagicFoo
{
public:
    std::vector<int> vec;
    MagicFoo(std::initializer_list<int> list)
    {
        // 从 C++11 起, 使用 auto 关键字进行类型推导
        for(auto it = list.begin(); it != list.end(); ++it)
            vec.push_back(*it);
    }
};

//注意: auto 不能用于函数传参,因此下面的做法是无法通过编译的(考虑重载的问题,我们应该使用模板):
//wrong:  'auto' not allowed in function prototype
//int add(auto x, auto y)
//{
//    return x+y;
//}

int main()
{
    MagicFoo magicFoo = {1,2,3,4,5};
    std::cout << "magicFoo:";
    for(auto it = magicFoo.vec.begin(); it != magicFoo.vec.end(); ++it)
    {
        std::cout << *it << "\t";
    }
    std::cout << std::endl;

    //一些其他的常见用法:
    auto i = 5; // i 被推导为 int
    auto arr = new auto(10);// arr 被推导为 int *

    //此外, auto 还不能用于推导数组类型:
    //auto auto_arr2[10] = {arr};//error 'auto_arr2' declared as array of 'auto'
    return 0;
}

decltype
decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的。它的用法和
typeof 很相似:
decltype(表达式)
有时候,我们可能需要计算某个表达式的类型,例如:
auto x = 1;
auto y = 2;
decltype(x+y) z;
你已经在前面的例子中看到 decltype 用于推断类型的用法,下面这个例子就是判断上面的变量 x,y, z 是否是同一类型:
 

#include <iostream>
#include <type_traits>

int main()
{
    auto x = 1;
    auto y = 2;
    decltype(x + y) z = 3;

    if(std::is_same<decltype (x), int>::value)
        std::cout << "type x == int" << std::endl;
    if(std::is_same<decltype (x), float>::value)
        std::cout << "type z == float" << std::endl;
    if(std::is_same<decltype (x), decltype (z)>::value)
        std::cout << "type z == type x" << std::endl;

    return 0;

其中, std::is_same<T, U> 用于判断 T 和 U 这两个类型是否相等。输出结果为:

 尾返回类型推导
你可能会思考,在介绍 auto 时,我们已经提过 auto 不能用于函数形参进行类型推导,那么 auto
能不能用于推导函数的返回类型呢?还是考虑一个加法函数的例子,在传统 C++ 中我们必须这么写:

template<typename R, typename T, typename U>
R add(T x, U y) {
return x+y
}
注意: typename 和 class 在模板参数列表中没有区别,在 typename 这个关键字出现之前,都
是使用 class 来定义模板参数的。但在模板中定义有嵌套依赖类型的变量时,需要用 typename
消除歧义
这样的代码其实变得很丑陋,因为程序员在使用这个模板函数的时候,必须明确指出返回类型。但
事实上我们并不知道 add() 这个函数会做什么样的操作,获得一个什么样的返回类型。
在 C++11 中这个问题得到解决。虽然你可能马上会反应出来使用 decltype 推导 x+y 的类型,写
出这样的代码:
decltype(x+y) add(T x, U y)
但事实上这样的写法并不能通过编译。这是因为在编译器读到 decltype(x+y) 时, x 和 y 尚未被定
义。为了解决这个问题, C++11 还引入了一个叫做尾返回类型(trailing return type),利用 auto 关键
字将返回类型后置:
template<typename T, typename U>
auto add2(T x, U y) -> decltype(x+y){
return x + y;
}
令人欣慰的是从 C++14 开始是可以直接让普通函数具备返回值推导,因此下面的写法变得合法:
template<typename T, typename U>
auto add3(T x, U y){
return x + y;
}
可以检查一下类型推导是否正确
 

#include <iostream>
#include <type_traits>

//before c++11
template<typename R, typename T, typename U>
R add(T x, U y)
{
    return x + y;
}

//after C++11
template<typename T, typename U>
auto add2(T x, U y)->decltype(x + y)
{
    return x + y;
}
//after C++14
template<typename T, typename U>
auto add3(T x, U y)
{
    return x + y;
}

int main()
{
    //before c++11
    int z = add<int, int, int>(1, 2);
    std::cout << z << std::endl;

    //after c++11
    auto w = add2<int, double>(1, 2.0);
    if(std::is_same<decltype(w), double>::value)
        std::cout << "w is double:";
    std::cout << w << std::endl;
    //after c++14
    auto q = add3<double, int>(1.2, 2);
    std::cout << "q: " << q << std::endl;

    return 0;
}

运行结果:

 decltype(auto)
decltype(auto) 是 C++14 开始提供的一个略微复杂的用法。
要理解它你需要知道 C++ 中参数转发的概念,我们会在语言运行时强化一章中详细介绍,你
可以到时再回来看这一小节的内容。
简单来说, decltype(auto) 主要用于对转发函数或封装的返回类型进行推导,它使我们无需显式的
指定 decltype 的参数表达式。考虑看下面的例子,当我们需要对下面两个函数进行封装时:
std::string lookup1();
std::string& lookup2();
在 C++11 中,封装实现是如下形式:
std::string look_up_a_string_1() {
return lookup1();
}
std::string& look_up_a_string_2() {
return lookup2();
}
而有了 decltype(auto),我们可以让编译器完成这一件烦人的参数转发:
 

#include <iostream>
#include <string>

std::string lookup1()
{
    return "lookup1";
}

std::string& lookup2()
{
    std::string *str = new std::string("lookup2");
    return *str;
}

decltype(auto) look_up1_string()
{
    return lookup1();
}

decltype(auto) look_up2_string()
{
    return lookup2();
}

int main()
{
    std::cout << "str1 = " << look_up1_string() << std::endl;
    std::cout << "str2 = " << look_up2_string() << std::endl;
    std::cout << "2_09_decltype_auto.cpp" << std::endl;
    return 0;
}

运行结果:

2.4 控制流
if constexpr
正如本章开头出,我们知道了 C++11 引入了 constexpr 关键字,它将表达式或函数编译为常量结
果。一个很自然的想法是,如果我们把这一特性引入到条件判断中去,让代码在编译时就完成分支判断,岂不是能让程序效率更高? C++17 将 constexpr 这个关键字引入到 if 语句中,允许在代码中声明常量表达式的判断条件,考虑下面的代码:
 

#include <iostream>

template<typename T>
auto print_type_info(const T& t)
{
    if constexpr (std::is_integral<T>::value)
        return t + 1;
    else
        return t + 0.001;
}

int main()
{
    std::cout << print_type_info(5) << std::endl;
    std::cout << print_type_info(3.14) << std::endl;
    std::cout << "2_10_if_constexpr.cpp" << std::endl;
    return 0;
}

运行结果:

在编译时,实际代码就会表现为如下:
 

int print_type_info(const int& t) {
    return t + 1;
}
double print_type_info(const double& t) {
    return t + 0.001;
}

int main() 
{
    std::cout << print_type_info(5) << std::endl;
    std::cout << print_type_info(3.14) << std::endl;
}

区间 for 迭代
终于, C++11 引入了基于范围的迭代写法,我们拥有了能够写出像 Python 一样简洁的循环语句,我们可以进一步简化前面的例子:
 

#include <iostream>
#include <vector>
#include <algorithm>

int main()
{
    std::vector<int> vec = {1,2,3,4};
    if(auto iter = std::find(vec.begin(), vec.end(), 3); iter != vec.end())
        *iter = 4;

    for(auto element: vec)
        std::cout << element << "\t";       //read only
    std::cout << std::endl;
    for(auto &element: vec)
        element += 1;                       //writeable
    for(auto element: vec)
        std::cout << element << "\t";       //read only
    std::cout << std::endl;
    std::cout << "2_11_for_loop.cpp" << std::endl;
    return 0;
}

运行结果:

 2.5 模板
C++ 的模板一直是这门语言的一种特殊的艺术,模板甚至可以独立作为一门新的语言来进行使用。模板的哲学在于将一切能够在编译期处理的问题丢到编译期进行处理,仅在运行时处理那些最核心的动态服务,进而大幅优化运行期的性能。因此模板也被很多人视作 C++ 的黑魔法之一。

外部模板
传统 C++ 中,模板只有在使用时才会被编译器实例化。换句话说,只要在每个编译单元(文件)中编译的代码中遇到了被完整定义的模板,都会实例化。这就产生了重复实例化而导致的编译时间的增加。并且,我们没有办法通知编译器不要触发模板的实例化。为此, C++11 引入了外部模板,扩充了原来的强制编译器在特定位置实例化模板的语法,使我们能
够显式的通知编译器何时进行模板的实例化:
template class std::vector<bool>; // 强行实例化
extern template class std::vector<double>; // 不在该当前编译文件中实例化模板
尖括号 “>”
在传统 C++ 的编译器中, >> 一律被当做右移运算符来进行处理。但实际上我们很容易就写出了嵌套模板的代码:
std::vector<std::vector<int>> matrix;
这在传统 C++ 编译器下是不能够被编译的,而 C++11 开始,连续的右尖括号将变得合法,并且
能够顺利通过编译。甚至于像下面这种写法都能够通过编译:
 

#include <iostream>
#include <vector>

template class std::vector<bool>;               // 强行实例化
extern template class std::vector<double>;      // 不在该当前编译文件中实例化模板

template<bool T> class MagicType
{
public:
    bool magic = T;
};

int main()
{
    // ">>" 在模板内
    std::vector<std::vector<int>> matrix;
    std::vector<int> vec = {12,13};
    matrix.push_back(vec);

    vec.push_back(15);
    matrix.push_back(vec);


    std::cout << "matrix[1][2] = " << matrix[1][2] << std::endl;

    std::vector<MagicType< (1>2) >> magic;//合法, 但不建议写出这样的代码
    std::cout << "2_12_external_template.cpp" << std::endl;
}

运行结果:

 类型别名模板
在了解类型别名模板之前,需要理解『模板』和『类型』之间的不同。仔细体会这句话: 模板是用来产生类型的。 在传统 C++ 中, typedef 可以为类型定义一个新的名称,但是却没有办法为模板定义一个新的名称。因为,模板不是类型。例如:
template<typename T, typename U>
class MagicType {
public:
T dark;
U magic;
};
// 不合法
template<typename T>
typedef MagicType<std::vector<T>, std::string> FakeDarkMagic;
C++11 使用 using 引入了下面这种形式的写法,并且同时支持对传统 typedef 相同的功效:通常我们使用 typedef 定义别名的语法是: typedef 原名称 新名称;,但是对函数指针等别名的定义语法却不相同,这通常给直接阅读造成了一定程度的困难
 

#include <iostream>
#include <vector>
#include <string>

template<typename T, typename U>
class MagicType{
public:
    T dark;
    U magic;
};

typedef int (*process)(void *);
using NewProcess = int(*)(void *);
template<typename T>
using TrueDarkMagic = MagicType<std::vector<T>, std::string>;

int main()
{
    TrueDarkMagic<bool> you;
    std::string str = "scott";
    you.dark.push_back(true);
    you.magic = str;
    std::cout << you.dark.at(0) << "  " << you.magic << std::endl;

    std::cout << "2_13_alias_template.cpp" << std::endl;
    return 0;
}

运行结果:

 默认模板参数
我们可能定义了一个加法函数,下面看它的使用过程

#include <iostream>

//c++11 传统版本
template<typename T, typename U>
auto add(T x, U y)->decltype (x+y)
{
    return x + y;
}

//要使用add,就必须每次都指定其模板参数的类型。
//在C++11中提供一种便利,可以指定模板的默认参数;
template<typename T = int, typename U = int>
auto add2(T x, U y)->decltype (x+y)
{
    return x + y;
}


int main()
{
    std::cout << "add(11, 12) = " << add<int, int>(11, 12) << std::endl;
    std::cout << "add2(11, 12) = " << add2(11, 12) << std::endl;

    std::cout << "2_14_default_template_param.cpp" << std::endl;
    return 0;
}

运行结果:

变长参数模板
模板一直是 C++ 所独有的黑魔法(一起念: Dark Magic)之一。在 C++11 之前,无论是类模板
还是函数模板,都只能按其指定的样子,接受一组固定数量的模板参数;而 C++11 加入了新的表示方法,允许任意个数、任意类别的模板参数,同时也不需要在定义时将参数的个数固定。
template<typename... Ts> class Magic;
模板类 Magic 的对象,能够接受不受限制个数的 typename 作为模板的形式参数,例如下面的定义:
class Magic<int,
std::vector<int>,
std::map<std::string,
std::vector<int>>> darkMagic;
既然是任意形式,所以个数为 0 的模板参数也是可以的: class Magic<> nothing;。
如果不希望产生的模板参数个数为 0,可以手动的定义至少一个模板参数:
template<typename Require, typename... Args> class Magic;
变长参数模板也能被直接调整到到模板函数上。传统 C 中的 printf 函数,虽然也能达成不定个数的形参的调用,但其并非类别安全。而 C++11 除了能定义类别安全的变长参数函数外,还可以使类似printf 的函数能自然地处理非自带类别的对象。除了在模板参数中能使用 ... 表示不定长模板参数外,函数参数也使用同样的表示法代表不定长参数,这也就为我们简单编写变长参数函数提供了便捷的手段,例如:
template<typename... Args> void printf(const std::string &str, Args... args);
那么我们定义了变长的模板参数,如何对参数进行解包呢?
首先,我们可以使用 sizeof... 来计算参数的个数,:
template<typename... Ts>
void magic(Ts... args) {
std::cout << sizeof...(args) << std::endl;
}
我们可以传递任意个参数给 magic 函数:
magic(); // 输出 0
magic(1); // 输出 1
magic(1, ""); // 输出 2
其次,对参数进行解包,到目前为止还没有一种简单的方法能够处理参数包,但有两种经典的处理
手法:
1. 递归模板函数
递归是非常容易想到的一种手段,也是最经典的处理方法。这种方法不断递归地向函数传递模板参
数,进而达到递归遍历所有模板参数的目的:
#include <iostream>
template<typename T0>
void printf1(T0 value) {
std::cout << value << std::endl;
}
template<typename T, typename... Ts>
void printf1(T value, Ts... args) {
std::cout << value << std::endl;
printf1(args...);
}
int main() {
printf1(1, 2, "123", 1.1);
return 0;
}
2. 变参模板展开
你应该感受到了这很繁琐,在 C++17 中增加了变参模板展开的支持,于是你可以在一个函数中完
成 printf 的编写:
template<typename T0, typename... T>
void printf2(T0 t0, T... t) {
std::cout << t0 << std::endl;
if constexpr (sizeof...(t) > 0) printf2(t...);
}

事实上,有时候我们虽然使用了变参模板,却不一定需要对参数做逐个遍历,我们可以利用
std::bind 及完美转发等特性实现对函数和参数的绑定,从而达到成功调用的目的。
3. 初始化列表展开
递归模板函数是一种标准的做法,但缺点显而易见的在于必须定义一个终止递归的函数。
这里介绍一种使用初始化列表展开的黑魔法:
template<typename T, typename... Ts>
auto printf3(T value, Ts... args) {
std::cout << value << std::endl;
(void) std::initializer_list<T>{([&args] {
std::cout << args << std::endl;
}(), value)...};
}
在这个代码中,额外使用了 C++11 中提供的初始化列表以及 Lambda 表达式的特性(下一节中将
提到)。通过初始化列表, (lambda 表达式, value)... 将会被展开。由于逗号表达式的出现,首先会执行前面的 lambda 表达式,完成参数的输出。为了避免编译器警告,我们可以将 std::initializer_list显式的转为 void。
 

#include <iostream>
#include <vector>
#include <string>

//sizeof...
template<typename... Ts>
void magic(Ts... args)
{
    std::cout << sizeof...(args) << std::endl;
}

//recursive parameter unpack
template<typename T0>
void printf1(T0 value)
{
    std::cout << value << std::endl;
}
template<typename T, typename... Ts>
void printf1(T value, Ts... args)
{
    std::cout << value << std::endl;
    printf1(args...);
}

//2.variadic template parameter unfold
template<typename T0, typename... T>
void printf2(T0 t0, T... t)
{
    std::cout << t0 << std::endl;
    if constexpr (sizeof...(t) > 0)
            printf2(t...);
}

//3.parameter unpack using initializer_list
template<typename T, typename... Ts>
auto printf3(T value, Ts... args)
{
    std::cout << value << std::endl;
    (void) std::initializer_list<T> {([&args]{
        std::cout << args << std::endl;
    }(), value)...};
}

int main()
{
    magic();
    magic(1);
    magic(1, "");
    std::cout << std::endl;

    printf1(1, 2, "123", 1.1);
    printf2(1, 2.3, "abc");
    printf3(111,123, "alpha", 1.2);

    return 0;
}

运行结果:

折叠表达式
C++ 17 中将变长参数这种特性进一步带给了表达式,考虑下面这个例子:
 

#include <iostream>

template<typename ... T>
auto sum(T ... t)
{
    return (t + ...);
}

int main()
{
    std::cout << sum(1,2,3,4,5) << std::endl;

    return 0;
}

运行结果

非类型模板参数推导
前面我们主要提及的是模板参数的一种形式:类型模板参数。
template <typename T, typename U>
auto add(T t, U u) {
        return t+u;
}
其中模板的参数 T 和 U 为具体的类型。但还有一种常见模板参数形式可以让不同字面量成为模板参数,即非类型模板参数:
 

#include <iostream>

template<typename T, int BufSize>
class buffer_t
{
public:
    T& alloc();
    void free(T& item);

//private:
    T data[BufSize];
};

int main()
{
    buffer_t<int, 100> buf; //100作为模板参数
    std::cout << sizeof(buf.data) << std::endl;

    return 0;
}

在这种模板参数形式下,我们可以将 100 作为模板的参数进行传递。在 C++11 引入了类型推导这
一特性后,我们会很自然的问,既然此处的模板参数以具体的字面量进行传递,能否让编译器辅助我们进行类型推导,通过使用占位符 auto 从而不再需要明确指明类型?幸运的是, C++17 引入了这一特性,我们的确可以 auto 关键字,让编译器辅助完成具体类型的推导,例如:
 

#include <iostream>

template<auto value>
void foo()
{
    std::cout << value << std::endl;
    return;
}

int main()
{
    foo<10>();
    return 0;
}

运行结果:

10

2.6 面向对象
委托构造
C++11 引入了委托构造的概念,这使得构造函数可以在同一个类中一个构造函数调用另一个构造函数,从而达到简化代码的目的:
 

#include <iostream>

class Base{
public:
    int value1;
    int value2;

    Base()
    {
        value1 = 1;
    }
    Base(int value):Base() // 委托 Base() 构造函数
    {
        value2 = value;
    }
};

int main()
{
    Base b(2);
    std::cout << "b.value1 = " << b.value1 << std::endl;
    std::cout << "b.value2 = " << b.value2 << std::endl;

    return 0;
}

运行结果:

 继承构造
在传统 C++ 中,构造函数如果需要继承是需要将参数一一传递的,这将导致效率低下。 C++11 利
用关键字 using 引入了继承构造函数的概念:
 

#include <iostream>

class Base
{
public:
    int value1;
    int value2;
    Base()
    {
        value1 = 1;
    }
    Base(int value): Base()// 委托 Base() 构造函数
    {
        value2 = value;
    }
};

class Subclass: public Base
{
public:
    using Base::Base;//继承构造
};

int main()
{
    Subclass s(3);

    std::cout << "s.value1 = " << s.value1 << std::endl;
    std::cout << "s.value2 = " << s.value2 << std::endl;
    return 0;
}

运行结果:

 显式虚函数重载
在传统 C++ 中,经常容易发生意外重载虚函数的事情。例如:
struct Base {
        virtual void foo();
};
struct SubClass: Base {
        void foo();
};
SubClass::foo 可能并不是程序员尝试重载虚函数,只是恰好加入了一个具有相同名字的函数。另
一个可能的情形是,当基类的虚函数被删除后,子类拥有旧的函数就不再重载该虚拟函数并摇身一变成为了一个普通的类方法,这将造成灾难性的后果。
C++11 引入了 override 和 final 这两个关键字来防止上述情形的发生。
override 当重载虚函数时,引入 override 关键字将显式的告知编译器进行重载,编译器将检查基函
数是否存在这样的虚函数,否则将无法通过编译:

#include <iostream>
#include <string>

class Base
{
public:
    std::string str;
    int value;
    Base() = delete;
    Base(std::string s) { str = s; }

    //delegate constructor
    Base(std::string s, int v): Base(s)
    {
        value = v;
    }
    //final constructor
    virtual void foo() final
    {
        return;
    }
    virtual void foo(int v)
    {
        value = v;
    }
};

class Subclass final: public Base
{
public:
    double floating;
    Subclass() = delete;
    //inherit constructor
    Subclass(double f, int v, std::string s): Base(s, v)
    {
        floating = f;
    }
    //explifict constructor
    virtual void foo(int v) override
    {
        std::cout << v << std::endl;
        value = v;
    }
};


int main()
{
    // Subclass oops; // illegal, default constructor has deleted
    Subclass s(1.2, 3, "abc");

    s.foo(1);

    std::cout << "s.floating = " << s.floating << std::endl;
    std::cout << "s.value======" << s.value << std::endl;
    std::cout << "s.str========" << s.str << std::endl;

    return 0;
}

运行结果:

 显式禁用默认函数
在传统 C++ 中,如果程序员没有提供,编译器会默认为对象生成默认构造函数、复制构造、赋值
算符以及析构函数。另外, C++ 也为所有类定义了诸如 new delete 这样的运算符。当程序员有需要时,可以重载这部分函数。
这就引发了一些需求:无法精确控制默认函数的生成行为。例如禁止类的拷贝时,必须将复制构造
函数与赋值算符声明为 private。尝试使用这些未定义的函数将导致编译或链接错误,则是一种非常不优雅的方式。并且,编译器产生的默认构造函数与用户定义的构造函数无法同时存在。若用户定义了任何构造函数,编译器将不再生成默认构造函数,但有时候我们却希望同时拥有这两种构造函数,这就造成了尴尬。
C++11 提供了上述需求的解决方案,允许显式的声明采用或拒绝编译器自带的函数。例如:
class Magic {
public:
        Magic() = default; // 显式声明使用编译器生成的构造
        Magic& operator=(const Magic&) = delete; // 显式声明拒绝编译器生成构造
        Magic(int magic_number);
}
强类型枚举
在传统 C++ 中,枚举类型并非类型安全,枚举类型会被视作整数,则会让两种完全不同的枚举类
型可以进行直接的比较(虽然编译器给出了检查,但并非所有), 甚至同一个命名空间中的不同枚举类型的枚举值名字不能相同,这通常不是我们希望看到的结果。
C++11 引入了枚举类(enumeration class),并使用 enum class 的语法进行声明:
enum class new_enum : unsigned int {
        value1,
        value2,
        value3 = 100,
        value4 = 100
};
这样定义的枚举实现了类型安全,首先他不能够被隐式的转换为整数,同时也不能够将其与整数数
字进行比较,更不可能对不同的枚举类型的枚举值进行比较。但相同枚举值之间如果指定的值相同,那么可以进行比较:
if (new_enum::value3 == new_enum::value4) {
        // 会输出
        std::cout << "new_enum::value3 == new_enum::value4" << std::endl;
}

在这个语法中,枚举类型后面使用了冒号及类型关键字来指定枚举中枚举值的类型,这使得我们能
够为枚举赋值(未指定时将默认使用 int)。而我们希望获得枚举值的值时,将必须显式的进行类型转换,不过我们可以通过重载 << 这个算符来进行输出,可以收藏下面这个代码段:

#include <iostream>

template<typename T>
std::ostream& operator<<(typename std::enable_if<std::is_enum<T>::value, std::ostream>::type& stream, const T& e)
{
    return stream << static_cast<typename std::underlying_type<T>::type>(e);
}

enum Left {
    left_value1 = 1,
    left_value2
};

enum Right {
    right_value1 = 1,
    right_value2
};

enum class new_enum: unsigned int {
    value1,
    value2,
    value3 = 100,
    value4 = 100
};

int main()
{
    if(Left::left_value1 == Right::right_value2)
        std::cout << "Left::left_value1 == Right::right_value2" << std::endl;
    // compile error
//    if(new_enum::left_value1 == 1) {
//        std::cout << "true!" << std::endl;
//    }
    if (new_enum::value3 == new_enum::value4) {
        std::cout << "new_enum::value3 == new_enum::value4" << std::endl;
    }

    std::cout << new_enum::value3 << std::endl;


    return 0;
}

运行结果:

 总结
本节介绍了现代 C++ 中对语言可用性的增强,其中最为重要的几个特性是几乎所有人都需要了解并熟练使用的:
1. auto 类型推导
2. 范围 for 迭代
3. 初始化列表
4. 变参模板
习题
 

#include <iostream>
#include <map>

template <typename Key, typename Value, typename F>
void update(std::map<Key, Value>& m, F foo) {
    // TODO:
    for(auto&& [key, value]:m) value = foo(key);
}

int main()
{
    std::map<std::string, long long int> m {
        {"a", 1},
        {"b", 2},
        {"c", 3}
    };

    update(m, [](std::string key) {
        return std::hash<std::string>{}(key);
    });

    for (auto&& [key, value] : m)
        std::cout << key.c_str() << ":" << value << std::endl;

    return 0;
}

运行结果:

 2. 尝试用折叠表达式实现用于计算均值的函数,传入允许任意参数。
 

#include <iostream>

template<typename ... T>
auto average(T ... t)
{
    return (t + ...) / sizeof...(t);
}


int main()
{
    std::cout << average(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0) << std::endl;
    return 0;
}

运行结果:

 


 

*

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值