3 类模板的模板参数推导
注意是类模板不是我们之前一直说的函数模板,
如果对pair了解一般不会使用
pair<int, int> pr{1, 42};
这种方式来创建一个pair
而是使用
auto pr = make_pair(1, 42);
更方便简洁
这是因为函数make_pair是个模板函数,可以进行我们之前一直说的函数模板自动参数推导,使用者不需要实例化模板参数。
在C++17以前类模板一直是无法进行参数自动推导的
在C++17以后就Ok了,可以直接写为
pair pr{1, 42};
我们在之前讲array时讲过它的主要缺点是不能像C数组一样自动从初始化列表来推断数组的大小
int a1[] = {1, 2, 3};//C数组根据初始化列表自动推导数组的大小
// array<int> a3{1, 2, 3}; 不行
array<int, 3> a2{1, 2, 3}; // 啰嗦
c++17中不存在这个问题,根据类模板参数自动推导。
array a{1, 2, 3};
// 得到 array
//array
这种自动推导机制,可以是编译器根据构造函数来自动生成
template <typename T>
struct MyObj {
MyObj(T value);
…
};
MyObj obj1{string("hello")};
// 得到 MyObj<string>
MyObj obj2{"hello"};
// 得到 MyObj<const char*>
//也可以手工提供一个推导向导
template <typename T>
struct MyObj {
MyObj(T value);
…
};
MyObj(const char*) -> MyObj<string>;//显示提供导向 当构造函数中传入const char*的实参时,将模板T设定为string
MyObj obj{"hello"};
// 得到 MyObj<string>
4结构化绑定
之前讲关联容器的时候讲过一个例子
multimap<string, int>::iterator lower, upper;
std::tie(lower, upper) = mmp.equal_range("four");
这个例子中equal_range函数返回值是个pair,我们希望用两个变量来接收这个pair。所以就不得不声明两个变量,并使用tie将两个变量组合成一个pair来接收结果。
在 C++11/14 里,这里是没法使用 auto 的。好在 C++17 引入了一个新语法,解决了这个问题
auto [lower, upper] = mmp.equal_range("four");
这个语法使得我们可以用 auto 声明变量来分别获取 pair 或 tuple(不止两个子项) 返回值里各个子项,可以让代码的可读性更好。
attr(可选) cv-auto ref-运算符(可选) [ 标识符列表 ] = 表达式 ; (1)
attr(可选) cv-auto ref-运算符(可选) [ 标识符列表 ] { 表达式 } ; (2)
attr(可选) cv-auto ref-运算符(可选) [ 标识符列表 ] ( 表达式 ) ; (3)
测试 但不知道什么毛病
std::tuple<float &, char &&, int> tpl(p, std::move(l), m);
const auto&[a, b, c] = tpl;// float & char & (pair和数组也不行当保存的数据为引用类型,
//在结构化绑定场景下const限定对引用类型完全没用 只有在定义tuple时把const限定加上才行)
因为Cv-qualified references are ill-formed except when the cv-qualifiers are introduced through the use of a typedef (7.1.3) or of a template type argument(14.3), in which case the cv-qualifiers are ignored.cv限定下的引用在typedef和模板参数类型的场景下会出现错误形式,在这些场景下cv限定会被忽略
typedef int& ref;const ref error;—-error是int&类型 而不是const int&,cv限定被忽略了
////////////////老师的解释
你举的这个例子相当复杂。常见情况下,一般不会用引用绑定去绑定到一个带引用的 tuple 上,所以我也不讨论这样的特殊情况了。
对于这个例子,编译器的第一步动作是:const auto& e = tpl;我们随即得到 e 的类型是 const std::tuple
重点const auto&修饰的是tql这个tuple而不是tuple中的内容,tuple中的内容类型在其定义时就决定好了的。
a、b、c 本质上就是 get<0>(e)、get<1>(e)、get<2>(e) 的语法糖了。对于 tuple 里的引用成员,get 的结果就是这个被引用指向的数据本身 而与 tuple无关:你对它修改时没有改变 tuple 本身,而是改变它指向的内容(即初始化tuple时传入用于初始化引用的变量)—可以理解为tuple 类中的引用或指针成员,修改他并没有改变指针或引用的指向(即没有改变tuple对象内的数据,其成员变量的值没有被修改),而是修改了指向的那个东西的数据(可以把引用改成指针来想象一下;引用可以看作会自动解引用的指针)。对于 tuple 里的非引用成员,其已经成为tuple对象的一部分数据了,它的类型限定会受到tuple类型限定的影响,get 的结果实际是指向 tuple 这个整体的引用—可以形象地理解为,这个非引用成员是tuple(类)的一个数据成员这就好比 s.int_value 的类型是整数,但你修改了 s.int_value 会修改 s (s对象的数据不一样了 因为有成员变量被修改了)一样。即使你获得的结果不是引用)。
下面的例子可以帮助你理解:
int n = 0;
std::tuple<int*, int> tup{&n, n};
auto& [p, m] = tup;
m = 1;
到此为止,tup 里的整数也变成了 1。
还有一点当tuple中含右值引类型量时(也就是char&&的那个c),auto&[a, b, c] = tpl;如果把引用声明去掉auto[a, b, c] = tpl;会出错。我推理一下
std::tuple
auto e= tpl;
auto类型推断出来是std::tuple
std::tuple
调用tpl的拷贝构造(tuple类本身是没有移动构造的),那么就是值拷贝
相当于需要将tql内的各个类型的变量值拷贝到e上,tql上的char&& c是有名的左值,不能用一个左值去初始化一个右值引用变量,所以报错。
而auto&
std::tuple
////////////////老师的解释
const auto& d = a;//const int& 常引用展开来说是常量的引用 引用本质上是常指针(顶层const),当引用初始化完其指针指向就不能改了,在引用基础上再加const(底层const) 就是常量的引用,即引用初始化后,引用变量的内容就不能修改了(内存内容不能修改)
d = a;//错误 常量的引用 内容不能修改
绑定数组
int a[2] = {1,2};
auto [x,y] = a; // 创建 e[2],复制 a 到 e,然后 x 指代 e[0],y 指代 e[1]
auto& [xr, yr] = a; // xr 指代 a[0],yr 指代 a[1]
绑定元组
float x{};
char y{};
int z{};
std::tuple<float&,char&&,int> tpl(x,std::move(y),z);
const auto& [a,b,c] = tpl;
// a 指名指代 x 的结构化绑定;decltype(a) 为 float&
// b 指名指代 y 的结构化绑定;decltype(b) 为 char&
// c 指名指代 tpl 的第 3 元素的结构化绑定;decltype(c) 为 const int&
绑定pair
要在没有结构化绑定的情况下迭代std::map<>的元素,编写程序:
for (const auto& elem : mymap)
{
std::cout << elem.first << ": " << elem.second << '\n';
}
通过使用结构化绑定,代码变得可读性更强:
for (const auto& [key,val] : mymap)
{
std::cout << key << ": " << val << '\n';
}
绑定到数据成员
struct S {
mutable int x1 : 2;
volatile double y1;
};
S f();
//将结构体的成员绑定到新名称x,y
const auto [x, y] = f; // x 是标识 2 位位域的 int 左值
// y 是 const volatile double 左值
遍历容器好的 高效的写法
std::map<int, string> students;
for (const auto& [id, name] : students)
{
cout << id << name << endl;
}
auto [u, v] = ms; 这个结构化绑定等同于下面3句
auto会被推导为值类型,所以在为u,v赋值时可能发生拷贝(or 移动)
const auto& [u,v] = ms; // a reference, so that u/v refer to ms.i/ms.s (&ms.i==&e.i)
这里,匿名实体被声明为一个const引用,这意味着u和v是初始化的对ms的const引用的成员i和s的名称,因此,对ms成员的任何更改都会影响u和/或v的值。
5 列表初始化
在c++98中,标准容器比起C风格数组至少有一个明显劣势,标准容器不能很方便地初始化容器里的内容。
C数组
int a[] = {1, 2, 3, 4, 5};
//c++98中 vector必须这么写
vector<int> v;
v.push(1);
v.push(2);
v.push(3);
v.push(4);
v.push(5);
c++11开始引入列表初始化后,可以像下面这么写
vector<int> v{1, 2, 3, 4, 5};
对于这句语句编译器实际做了为{1,2,3,4,5}这样的表达式自动生成一个初始化列表initializer_list,在这个例子中初始化列表的类型是initializer_list
并且并不只有标准库容器才能使用初始化列表初始化,我们自己写的类也可以使用初始化列表。
注意initializer_list
但是不能进行隐式的窄化转换(6中有提到),vector
我们一般只有在类需要处理长度不同的列表时,才会写 初始化列表的构造函数,否则一般不写initializer_list构造。
#include <initializer_list>
template <class T>struct S {
std::vector<T> v;
S(std::initializer_list<T> l) : v(l) {
std::cout << "constructed with a " << l.size() << "-element list\n";
}
void append(std::initializer_list<T> l) {
v.insert(v.end(), l.begin(), l.end());
}
std::pair<const T*, std::size_t> c_arr() const {
return {&v[0], v.size()}; // 在 return 语句中复制列表初始化
// 这不使用 std::initializer_list
}
};//类中含 向量这种需要多同参数初始化的成员变量,则可以定义初始化列表构造函数和初始化列表做参数的函数
S<int> s = {1, 2, 3, 4, 5}; // 复制初始化
s.append({6, 7, 8}); // 函数调用中的列表初始化
for (int x : {-1, -2, -3}) // auto 的规则令此带范围 for 工作
std::cout << x << ' ';
6 统一初始化
在c++11中可以使用{}进行对象的初始化,以防止创建对象的初始化语句被编译器语法分析(the most vexing parse)为函数声明。
ifstream ifs{utf8_to_wstring{filename}};
我们可以在所有初始化对象的地方使用大括号而不是小括号。它还有一个特点:当一个构造函数没有标成explicit—可隐式调用(explicit关键字只能用于修饰只有一个参数的类构造函数, 它的作用是表明该构造函数的调用必须是显示的, 这个构造函数无法隐式调用),对于可以隐式调用的构造函数,我们可以使用大括号不写类型的方式进行构造
Obj getObj()
{
return {1.0};//调用Obj(double)来构造这个返回对的对象
}
当Obj类有定义浮点数构造函数,上面这么写是正确的。也可以通过{}调用无参数构造,或者{x,xx,xxx}多参数构造。
{1.0}和Obj(1.0)的主要区别是,Obj(1.0)在没有Obj(double)时也会调用Obj(int)。而使用大括号时编译器会拒绝窄转换,不接受以{1.0},Obj{1.0}的形式来调用Obj(int)
这个语法的主要限制是因为初始化列表构造的调用也是用{}来触发的,当一个类既有使用初始化列表的构造函数,又有不使用初始化列表的构造函数。 当我们使用{}来构造对象时,编译器会千方百计地调用初始化列表的构造函数。
建议
如果一个类没有初始化列表构造函数时,初始化该类对象时可以全部使用{}来初始化对象
如果一个类有初始化列表构造函数时,则只能在我们想调用初始化列表构造函数时使用{}来构造对象,其余时候均使用()来构造对象
7 数据成员的默认初始化
c++98语法,数据成员可以在构造函数中进行初始化(废话)。 当数据成员较多、构造函数又有多个的话,逐个去初始化是个累赘,并且很容易在增加数据成员时,在某个构造中将对它的初始化漏掉
class Complex {
public:
//当数据成员再多一些的时候很容易忘记将这个数据成员的默认初始化,忘记添加在:后面
Complex()
: re_(0) , im_(0) {}
Complex(float re)
: re_(re), im_(0) {}
Complex(float re, float im)
: re_(re) , im_(im) {}
…
private:
float re_;
float im_;
};
c++11允许在声明数据成员时,直接给予一个初始化表达式。 当构造函数的初始化:中没有写这个数据成员时,则是用声明时的表达式(默认值)作为这个数据成员的初始化值。
class Complex {
public:
Complex() {}
Complex(float re) : re_(re) {}
Complex(float re, float im)
: re_(re) , im_(im) {}
private:
float re_{0};//0相当于这个成员的默认值 当构造:后没有初始化re_,则用0来默认初始化re_
float im_{0};
};