计算机 · 2021年5月22日 0

深入应用C++11笔记 第1章

使用c++11让程序更简洁、现代化

模板的细节改进

C++11改进了编译器的解析规则,尽可能地将多个右尖括号解析成模板参数结束符。

C++98/03中,两个连续右见括号会被解释成右移操作符,而不是模板参数结束表的结束,必须在两个右尖括号之间加上空格。C++11取消了这个限制。

模板的别名

template<typename Val>
using str_map_t = std::map<std::string, Val>;
//...
str_map_t<int> map1;

通过指定std::string为key,定义了一类新的模板str_map_t

template <typename T>
using type_t = T;
//...
type_t<int> i;

这里的type_t<int>等价于int。

函数模板的默认模板参数

在C++98/03中,类模板可以有默认模板参数,但是函数模板却不支持默认模板参数,而在C++11中函数模板支持默认模板参数。当所有模板参数都有默认参数时,函数模板的调用如同一个普通函数。对于类模板而言,哪怕所有参数都有默认参数,在使用时也必须在模板名后面跟随<>来实例化。默认模板参数和模板参数自动推导可以同时使用,但是显示指定模板参数时,模板参数是按照从左往右的顺序填充的。当自动推导失败时,使用默认模板参数。

列表初始化

统一的初始化

在C++98/03中只有普通数组和POD类型可以用初始化列表(initializer list)进行初始化,而在c++11中扩大了可以使用初始化列表进行初始化的类型范围。

使用初始化列表进行初始化时,{}前面写不写=都是一样的,都是直接构造而不是先构造一个临时变量再调用拷贝构造函数;

new操作符等可以用圆括号进行初始化的地方,也可以使用初始化列表;new数组的时候也可以使用初始化列表初始没一个元素:

int* a = new int { 123 };
double b = double { 12.12 };
int* arr = new int[3]{1, 2, 3};

注意上面的b是先对匿名对象进行列表初始化,再进行拷贝初始化;

列表初始化可以用在返回值上;

聚合类型(Aggregates)

只有聚合类型才可以使用列表初始化,聚合类型包括:

  • 类型是一个普通数组(任何类型的数组,即使是非聚会类型的数组),这里的数组指C里的数组,不包括容器;
  • 类型是一个类(class, struct, union),并且:
    • 无用户自定义的构造函数;
    • 无私有(Private)或保护(Protected)的非静态数据成员;
    • 无基类;
    • 无虚函数;
    • 不能有{}或者=直接初始化(brace-or-equal-initializer)的非静态数据成员(这种类成员的初始方式也是c++11新加的);

聚合类型的定义是非递归的,当一个类的非静态成员是非聚合类型时,这个类也有可能是聚合类型。对于聚合类型做列表初始化的时候,对该聚会类型里的非聚合类型成员可以直接用{}来初始化,相当于调用其无参构造函数。

初始化列表

c++11中的容器可以像未显式指定长度的数组一样进行初始化,如果我们的自定义类型也想具备这样的接受一个为显示指定长度的参数列表进行初始化,可以使用std::initializer_list类模板:

class Foo
{
public:
  Foo(std::initializer_list<int>){}
};

Foo foo = { 1, 2, 3, 4, 5 }; // OK!

std::initializer_list可以用类似list的接口(begin()end()size())来进行访问,只是std::initializer_list里存的都是const引用,无法通过迭代器修改std::initializer_list,但是可以通过对整个std::initializer_list重新赋值对其进行修改。

防止类型收窄

使用列表初始化时,对于下面这些情况编译器会生产警告或者错误:

  • 从浮点数隐式转换为整型;
  • 从高精度浮点数隐式转换为低精度浮点数;
  • 从一个整型隐式转换为一个浮点数,并且超出了浮点数的表示范围;
  • 从一个整型隐式转换为一个长度较短的整型,并且超出了其表示范围;

基于范围的for循环

for循环的类型可以写auto让编译器进行自动推导,也可以显示指定类型,并且指定的类型只要是容器里的类型支持隐式转换的类型即可,如:

     std::vector<int> arr;
     for(int n : arr);

如果要修改容器里的值需要显示指定引用:

      for(auto &n : arr)

range based for loop只是语法糖,在循环里面修改容器可能导致迭代器失效

range based for loop支持容器,数组,或者自定义的类型,for循环会先试图通过类的begin()end()方法定位begin、end迭代器,再尝试用全局的begin()end()函数定位。所以对于自定义类型,只要支持begin()end()和前置自增函数就可以对其使用基于范围的for循环。

std::function和bind绑定器

可调用对象(Callable Objects):

  • 函数指针;
  • 函数对象(或者仿函数):具有operator()成员函数的类对象;
  • 一个可以被转换为函数指针的对象;
  • 一个类成员(函数)指针;

std::function是可调用对象的包装器。它是一个类模板,可以容纳除了类成员(函数)指针之外的所有可调用对象。通过指定它的模板参数,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟执行它们。

std::function的模板参数是函数签名(即函数类型,包含返回值和参数表),如:

   std::function<void(void)> fr1;
   std::function<int(int)> fr2;

std::bind可以用来给可调用对象设置默认参数,比如:

   auto fr = std::bind(output, std::placeholders::_1);

bind的返回值是一个仿函数,可以直接赋值给一个std::function。上面的std::place_holders::_1表示说这个参数由调用时给的第一个参数赋值。place_holders用来决定该空位参数取调用时的第几个参数的值。如:

   void output(int x, int y)
   {
     cout << x << ' ' << y;
   }
   int main(int argc, char **argv)
   {
     std::bind(output, 1, 2,)();
     std::bind(output, std::placeholders::_1, 2)(1);
     std::bind(output, 2, std::placeholders::_1)(1);
     std::bind(output, 2, std::placeholders::_2)(1, 2);
     return 0;
   }

std::bind可以绑定参数,使得成员函数也可以放入std::function保存;std::bind的使用也使得bind1st和bind2nd没有了存在的必要;std::bind还可以对多个函数进行组合:

   using std::placeholders::_1;
   //查找集合中大于5小于10的元素个数
   auto f = std::bind(std::logical_and<bool>(),
                      std::bind(std::greater<int>(), _1, 5),
                      std::bind(std::less_equal<int>(), _1, 10));
   int count = std::count_if(coll.begin(), coll.end(), f);

lambda表达式

语法形式:

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

其中capture是捕获列表;params是参数表;opt是函数选项;ret是返回值类型;body是函数体。

  • 返回值定义可以省略,编译器可以根据return语句自动推导返回值类型;初始化列表不能用于返回值的自动推导;
  • lambda表达式在没有参数列表时,参数列表可以省略;
  • lambda表达式可以通过捕获列表捕获一定范围内的变量:
    • []不捕获任何变量;
    • [&]捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获);
    • [=]捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获);
    • [=,&foo]按值捕获外部作用域中所有变量,并按引用捕获foo变量;
    • [bar]按至捕获bar变量,同时不捕获其他变量;
    • [this]捕获当前类中的this指针,让lambda表达式拥有和当前类成员函数同样的访问权限。如果已经使用了&或者=,就默认添加此选项。捕获this的目的是可以在lambda中使用当前类的成员函数和成员变量;
    • 对于延迟调用并且按值捕获的lambda表达式,运算结果是由当时的输入决定的,声明这个lambda表达式时参数的值已经复制到lambda表达式里了;
    • lambda表达式中按值捕获的参数是默认不可以修改的,除非在声明lambda表达式的时候加上mutable修饰:auto f2 = [=]() mutable { return a++; };
    • lambda表达式的类型在C++11中被称为”闭包类型(Closure Type)”,可以看作一个带有operator()的类,仿函数。lambda表达式是就地定义仿函数闭包的”语法糖”。lambda表达式捕获的任何外部变量最终均会变为闭包类型的成员变量。没有捕获任何变量的lambda表达式可以被转换成一个普通的函数指针;
    • 因为lambda表达式的operator()默认是const的,而const成员函数是无法修改成员变量的值的,所以lambda表达式默认无法修改成员变量的值,而mutable的作用就在于取消operator()的const;

tuple元组

tuple元组是固定大小的不同类型值的组合,是泛化的pair。tuple可以用来代替简单的结构体。

创建tuple

     tuple<const char*, int>tp = make_tuple(sendPack, nSendSize);

或者使用std::tie创建一个元组的左值引用,

     int x = 1;
     int y = 2;
     string s = "aa";
     auto tp = std::tie(x, s, y);
     //tp的类型实际是std::tuple<int&, string&, int&>

获取元组的值

     const char *data = tp.get<0>(); //获取第一个值
     int len = tp.get<1>();			 //获取第二个值

或者使用std::tie

     int x, y;
     string a;
     std::tie(x, a, y) = tp;

可以用std::ignore占位符来表示不解某个位置的值:

std::tie(std::ignore, std::ignore, y) = tp;

forward_as_tuple

https://en.cppreference.com/w/cpp/utility/tuple/forward_as_tuple

看的不是很明白,大概是说可以把几个参数打包在一起作为函数的参数,并且起到类似完美转发的作用。

用tuple_cat连接多个tuple