Chapter 1, Deducing Type
Item 1: Template type deduction
一些基础知识:
关于左值和右值的一些解释:https://book.2cto.com/201306/25366.html
一般来说,能找到地址的、具有名字的、位于等号左侧的都是左值,否则是右值。右值一般是匿名的,或者函数返回值也是右值。
一般情景:
int a=1,b=2,c=3;
a=b+c; // 在这里,a是左值,而(b+c)这个整体是右值,不能被引用,没有地址
///
int* d=&(b+c); // 这个操作是错误的,因为右值在没有常引用之前,没有实际意义
int& d=(b+c); // 这个操作也是错误的,右值必须是"常引用"。
const int& d=(b+c); // 这个操作是正确的
返回值:
template<typename T>
T value=ReturnValue(); // value的值是复制的ReturnValue的返回值
const T& r_value=ReturnValue(); // ReturnValue函数返回值就是一个右值,r_value引用的返回值。
// 且返回值的生命周期一直延续到没有任何引用为止
使用右值的时候,右值必须有一个引用才行。否则只能通过把右值的值复制给左值的方式保留右值的内容,复制完毕后,右值立刻被销毁。不能通过指针指向右值,因为右值等效理解为没有地址。
关于const的使用
const int a;
和int const a;
完全等效。
int* const p;
先看const
再看*
,是p是一个常量类型的指针,不能修改这个指针的指向,但是这个指针所指向的地址上存储的值可以修改。
const int *p; //const int &p 一个意思,不过这里是引用
先看*
再看const
,定义一个指针指向一个常量,不能通过指针来修改这个指针指向的值。
template<typename T>
void f(ParamType param);
f(expr); //deduce T and ParaType from expr
Case 1: ParamType 是引用或者指针, 但不是右值
-
模板作为函数参数时,会忽略实际函数参数中的引用成分,但是如果原来模板中用引用参数,那么模板的引用起作用。
-
expr的类型会优先覆盖掉ParamType的类型,以实际引用的为准。
-
如果模板中出现引用,那么不管实际参数中是否有引用,都作为引用看待。
为代码举例:
template<class T> void f(T& param); // param is reference int x=27; // x is an int const int cx=x; // cx is an const int const int& rx=x; // rx is an reference to x as a const int f(x); // T is int, param is int& f(cx); // cx is const int, param is const int& f(rx); // T is const int, param is const int&
实例:
#include <iostream> using namespace std; template<typename T> void f(T x){ // 乘2测试 x=2*x; } template<typename T> void f1(T& x){ // 乘2测试 x=2*x; } int main() { int x=27; const int cx=x; int& rx=x; f(rx); // 忽略掉引用的成分,x的值不会发生任何改变,对应情况1 cout<<"x="<<x<<", cx="<<cx<<", rx="<<rx<<endl; f1(x); // x是int&, 由f1(T&)中的木板参数中的引用决定,对应情况3 cout<<"x="<<x<<", cx="<<cx<<", rx="<<rx<<endl; return 0; } /* output: x=27, cx=27, rx=27 x=54, cx=27, rx=54 */
template<typename T>
void f(const T& param); // param is a ref-to-const
int x=27;
const int cx=x;
const int& rx=x;
f(x); // T is int, param is type of const int&
f(cx); // T is int, param is type of const int&
f(rx); // T is int, param is type of const int&
指针类型的伪代码:
template<typename T>
void f(T* param); // param is now a pointer
int x=27; // as before
const int* px=&x; // px is a ptr to x as a const int
f(&x); // T is int, param is int*
f(px); // T is const int, param is const int*
Case2 ParamType是右值
函数声明形式:
template<typename T>
void f(T&& param); // param声明为右值引用
int x=27;
const int cx=x;
const int& rx=x;
f(x); // x是左值,T是int&,param是int&
f(cx); // cx是左值,T是const int&,param是const int&
f(rx); // rx是左值,T是const int&,param是const int&
f(27); // 27是右值,T是int,param是int&&
总结:这里用到了C++的参数引用折叠的规则,对于形参是右值的模板,如果传入的是左值,那么按照左值的引用进行处理;其余的情况按照右值进行处理。
Case3: ParaTYpe不是指针和引用
如果expr是引用,忽略引用。
template<typename T>
void f(T param);
int x=27;
const int cx=x;
const int& rx=x;
数组类型
数组类型按值传递
按值传递的时候,推断类型会把数组类型等效成指针类型。
const char name[]="J. P. Briggers"; // name是const char[13]
const char* ptrToName=name; // 指针的赋值
template<typename T>
void f(T param);
f(name); // 此时,name是数组类型,但是T是const char*,此时传递的是指针
数组类型按引用传递
数组类型按引用传递的时候,会把数组类型推断成实际的数组类型
const char name[]="J. P. Briggers"; // name是const char[13]
const char* ptrToName=name; // 指针的赋值
template<typename T>
void f(T& param);
f(name); // 此时,name是数组类型,T也是数组类型,此时传递的是数组
实例:
#include<iostream>
using namespace std;
template<typename T>
int f(T param){
return sizeof(param);
}
template<typename T>
int f1(T& param){
return sizeof(param);
}
//64位系统中,指针占用8个字节,32位占用4个
int main(){
const char name[]="Hello World!"; // 总的长度是13
const char* ptrToName=name;
int a,b;
a=f(name); // 推断成数组指针类型,对应第一种情况。这里仅仅返回指针的子节数
b=f(ptrToName);
cout<<a<<" "<<b<<endl;
a=f1(name);
b=f1(ptrToName); // 推断成传入数组,等效成第二种情况。返回数组的占用的子节数。
cout<<a<<" "<<b<<endl;
return 0;
}
/*
输出:
8 8
13 8
*/
函数参数
volatile
的简介: volatile是给编译器的指示来说明对它所修饰的对象不应该执行优化。volatile的作用就是用来进行多线程编程。在单线程中那就是只能起到限制编译器优化的作用。在单线程编程的时候,除了不优化之外,几乎没有其他的作用。
总结:
- 在参数推断的过程中,原来参数的引用总是总是被当作没有引用看待。
- 参数是按值传递的模板的时候,
const
和volatile
总是被当作non-const和non-volatile看待,比如说f(T param)
的时候,param
的引用会被忽略。 - 参数推断的时候,只有模板引用(
f(T& param)
)形式的数组名被当作非指针看待,其余的数组名一律视为指针。
Item 2: Understand auto type deduction
auto
的引用分为3种情况:
- 推断类型是引用或者指针,但不是右值引用
- 推断类型是右值引用
- 推断类型不是指针也不是引用。
Case1:
const auto& rx = x; //引用
此时,auto
就是推断的x的实际类型。
Case2:
const auto* rx = x; //指针
auto
推断的是x的实际类型。
Case3:
int x;
const int cx = x;
const int& rx = x;
auto&& uref1 = x; // uref1是int&
auto&& uref2 = cx; // cx是uref2是const int&和左值,所以uref2是const int&
auto&& uref3 = 27; // 27是右值,uref3是int&&
同样的类似于参数折叠规则,只要实际的数据是左值,那么右值推断一律成为左值引用(包含const)。
关于数组的一些补充:
const char str[] = "Hello World !";
auto arr1 = str; // arr1是const char*
auto& arr2 = str; // arr2是char(&)[13],是实际的数组名
关于函数的一些补充:
void fun(int,double);
auto f = fun; // f是void(*)(int,double),函数指针
auto& f = fun; // f是void(&)(int,double),函数引用
auto
模板推断内部的类型必须一致!
auto x = {1, 2, 3.0}; //是错误的,不能有二意性
auto x = {1, 2, 3}; //正确的,推断为int类型,但是sizeof(x)==16
区别说明:
auto
可以推断列表,而template
不能推断列表类型。
#include <iostream>
using namespace std;
struct Node {
int a, b, c;
};
template<typename T>
void f(T param) {}
template<typename T>
void f1(std::initializer_list<T> initlist) {
cout << sizeof(initlist) << " ";
}
int main() {
auto x = {1, 2, 3};
// f({1,2,3}); 这是一个错误的模板,无法推断列表初始的形式
cout << sizeof(x) << " ";
f1({1, 2, 3}); // 这里推断T是int类型
cout << sizeof(std::initializer_list<int>) << " ";
return 0;
}
/*输出
16 16 16
*/
模板的类型大小永远是16字节,与推断的类型无关。
2018/1/22号的一点补充:
auto
可以通过引用的方式更改类的私有成员的值:
#include <iostream>
#include <vector>
class Widget {
public:
using DataTYpe = std::vector<double>;
DataTYpe& data() {
return values;
}
void p() {
std::cout << values[0] << std::endl;
}
private:
DataTYpe values;
};
int main() {
Widget w;
auto& vals1 = w.data(); // 私有成员的引用
vals1.push_back(4.1); // 更改私有成员
std::cout << vals1[0] << std::endl;
w.p();
return 0;
}
/*
输出:
4.1
4.1
*/
总结:
auto
类型基本与template
类型推断一样,唯一的区别在于auto
假设初始化是一个std::initial_list
,但是template
没有这个限制。auto
推断列表类型的时候,不管列表的数据是什么,永远是占用16个字节auto
推断列表的时候,列表内部的数据类型必须一致了。- 函数返回
auto
或者lambda的时候,参数是一个隐藏的模板类型,而不是auto
类型。
补充内容:
C++复制(拷贝)构造函数:
一般情况下,采用传入参数的方式。
class Ex{
public:
Ex(int a,int b){_a=a;_b=b;}
~Ex(){}
private:
int _a,_b;
}
int main(){
Ex ex(1,2); // 传入值,初始化。
return 0;
}
借助另一个对象初始化
class Ex{}; // 上一个例子的Ex
Ex A(10,20);
Ex B=A;
Ex B(A);
对第4行来说,当传递给对象一个对象作为参数时,编译器会自动为每一个对象构造一个拷贝构造函数,但仅仅是使用传入的类的数值初始化成员。
class Ex{}; // 上个例子的Ex
//下面这些代码由编译器自动生成,这是一个默认构造函数
Ex::Ex(const Ex& ex){
_a=ex._a;
_b=ex._b;
}
上述使用默认构造函数的方法仅仅是一种浅拷贝。这种方式不会处理静态数据成员;同时,也仅仅是对数据成员进行一个赋值。如果存在动态的数据对象的时候,这种方式有很大的隐患。
#include <iostream>
using namespace std;
class Ex{
public:
Ex(int n=10){
p=new int[n];
}
~Ex(){delete[] p;}
int* p;
};
int main() {
Ex ex1(20);
Ex ex2(ex1);
if(ex1.p==ex2.p){
cout<<"same address"<<endl;
}
else{
cout<<"different address"<<endl;
}
return 0;
}
//程序输出same address
这种方式的默认构造函数,会造成ex2.p
和ex1.p
指向同一个内存地址块,而不是开辟一块新的内存地址。这就是一种浅拷贝。
深拷贝的方式:自己重写默认拷贝函数,对于上边的例子,重写为:
#include <iostream>
using namespace std;
class Ex{
public:
Ex(int n=10){
p=new int[n];
}
Ex(const Ex& ex){
p=new int[100];
}
~Ex(){delete[] p;}
int* p;
};
int main() {
Ex ex1(20);
Ex ex2(ex1);
if(ex1.p==ex2.p){
cout<<"same address"<<endl;
}
else{
cout<<"different address"<<endl;
}
return 0;
}
在这个例子中,注意重写复制构造函数的时候,需要至少一个默认构造函数,否则会有被复制的对象没有复制的对象的错误。
防止使用默认构造函数的方式:把默认构造函数在私有范围内声明一下就行。私有成员不能被调用。这种情况主要用于类中有动态成员的情景。比如下面的代码:
#include <iostream>
using namespace std;
class Ex{
public:
Ex(int n=10){
p=new int[n];
}
~Ex(){delete[] p;}
int* p;
private:
Ex(const Ex& );
};
int main() {
Ex ex1(20);
Ex ex2(ex1);// 这里有编译错误,不能调用私有成员
Ex ex3=ex1; // 编译错误,这也是拷贝构造函数的种类
return 0;
}
拷贝构造函数不能处理静态成员,静态成员要在运行时候初始化。在调用时,不对静态成员做处理。
#include <iostream>
using namespace std;
class Ex{
public:
Ex(){
++n;
}
~Ex(){} //这里没有任何操作
int* p;
static int n;
};
int Ex::n=0; //注意静态成员要在运行时间初始化。
int main() {
Ex ex1;
Ex ex2(ex1);
Ex ex3(ex1);
cout<<Ex::n<<endl;
return 0;
}
//输出 1
C++初始化的方式:
C++11及以后的版本,为了统一初始化方式,提出了列表初始化的概念。
C++98/03中,只能对普通数组和POD(plain old data)进行列表初始化。比如:
int arr[]={1,2,3,4,5};
struct A{
int x,y
}a={1,2}; // a.x=1,a.y=2;
A a1{1,2}; // a.x=1,a.y=2;
int b{10}; // b==10
C++11及以后的版本中,初始化列表可以用于任何对象,以类为例:
#include <iostream>
using namespace std;
class Ex {
public:
Ex(int t = 0) {
n = t;
}
int n;
private:
Ex(const Ex& ex); //禁止调用拷贝构造函数
};
int main() {
//介绍三种初始化方式
Ex ex1(10);
Ex ex2{20};
Ex ex3 = {30};
cout << ex1.n << " " << ex2.n << " " << ex3.n << endl;
return 0;
}
在动态结构中:
int* a = new int[3] {1, 2, 3};
int b[] = {4, 5, 6};
int* c = new int{7};
可以使用列表进行构造的条件:必须是一个聚合体,聚合体定义:
- 无用户自定义构造函数
- 无私有或者受保护的非静态数据成员。静态数据成员的初始化是不能通过初始化列表来完成初始化的,它的初始化还是遵循以往的静态成员的额初始化方式私有或者保护成员无法直接访问 。
- 无基类和虚函数
- 无{}和=直接初始化的非静态数据成员
如果不是聚合体,也可以构造,但是构造的结果会发生变化。
std::initial_list
的介绍:
在C++11中,对于任意的STL容易都与和为显示指定长度的数组一样的初始化能力。
int arr[] = { 1, 2, 3, 4, 5 };
std::map < int, int > map_t { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 } };
std::list<std::string> list_str{ "hello", "world", "china" };
std::vector<double> vec_d { 0.0,0.1,0.2,0.3,0.4,0.5};
std::initialzer_list
可以接受任意长度的同类型的数据也就是接受可变长参数{…}
#include <iostream>
using namespace std;
struct Ex {
int x, y, z;
Ex(std::initializer_list<int>list) {
auto it = list.begin();
x = *it++;
y = *it++;
z = *it++;
}
};
struct Ex2 {
int x, y, z;
Ex2(int a, int b, int c): x(a), y(b), z(c) {}
};
int main() {
Ex ex1{1, 2, 3};
cout << ex1.x << " " << ex1.y << " " << ex1.z << endl;
Ex2 ex2{1, 2, 3};
cout << ex2.x << " " << ex2.y << " " << ex2.z << endl;
Ex ex3(std::initializer_list<int> {4, 5, 6}); // 直接调用模板列表初始化
cout << ex3.x << " " << ex3.y << " " << ex3.z << endl;
return 0;
}
/*
1 2 3
1 2 3
4 5 6
*/
Item 3: Understand decltype
decltype
告诉我们我们给出的名字或者表达式的类型。
比如:
class Widget {};
const int i = 1; // decltype(i) is const int
bool f(const Widget& w); // decltype(w) is const Widget&
// decltype(f) is bool (const Widget&)
struct Point {
int x; y; //decltype(Point::x) and decltype(Point::y)is int
};
Widget w; // decltype(w) is Widget
if(f(w)) {} // decltype(f(w)) is bool
vector<int> v; // decltype(v) is vector<int>
if(v[0] == 0) // decltype(v[0]) is int&
decltype
类型推断,可以理解成声明后延。C++11中,decltype
用于声明函数返回值以来与参数本身类型的函数模板。但是,C++中,利用[]
返回容器引用的时候,还是需要依赖容器的类型,decltype
的使用极大的方便了这样的表达。
template<typename Container, typename Index>
auto Test(Container& c, Index i)->decltype(c[i]) {
return c[i];
}
decltype(auto) Test1(Container& c, Index i) {
return c[i];
}
int main() {
return 0;
}
//Test与Test1的声明是等价的。不过Test1是C++14的标准。
在上述的代码Test
函数中,c
是一个Container
容器,我们的目的是返回c
的下标为i
的索引,但是我们无法得知用户实际使用容器的存放的数据类型,很难使用传统的方式返回值,因此在这里,直接使用auto
结合decltype
的方式。
补充说明一点:在C++11/14中,对于绝大多数容器来说,[ ]的返回一个T&
,是返回的引用类型。
#include <iostream>
#include <vector>
using namespace std;
template<typename Container, typename Index>
auto f(Container& c, Index i)->decltype(c[i]) {
return c[i];
}
template<typename Container, typename Index>
auto f_wrong(Container& c, Index i) { // 函数本身的声明是正确的
return c[i];
}
int f_test(int* a, int i) {
return a[i];
}
int main() {
vector<int>v{1, 2, 3};
int a[] {1, 2, 3};
f(v, 0) += 1; // 在这里返回的是v[0]的引用,等效成v[0]+=1
cout << v[0] << endl;
f(a, 0) += 1; // 同理,对于普通数组同样生效
cout << a[0] << endl;
f_wrong(v,0) += 1; // 这是错误的方式,auto返回推断的时候去掉引用的成分,返回的实际上是右值。
f_test(a, 0) += 1; // 这里是错误的方式,返回的是右值
return 0;
}
decltype
不仅仅是在函数返回值的时候使用有效,也可以在初始化声明变量或者表达式的时候
在非函数参数中:
class Ex {};
Ex ex;
const Ex& ex1 = ex; // ex1是const Ex&
auto myEx1 = ex1; // myEx1是Ex类型,auto自动去掉引用的成分
decltype(auto) myEx2 = ex1; // myEx2是const Ex&
一些其他的说明:
decltype((x))
返回x
的引用。
decltype(auto)f1() {
int x = 0;
return x; // decltype(x) is int, f1 returns int
}
decltype(auto)f2() {
int x = 0;
return (x); // decltype((x)) is int&, f1 returns int&
}
int main() {
//cout<<(f2()+=1)<<endl; 这是错误的操作,指向了一个局部的变量
return 0;
}
总结:
decltype
基本都是用于不被更改的变量或者表达式上。- 对于返回
T
类型的左值表达式来说,decltype
总是推断成T&
类型 - C++14支持
decltype(auto)
, 使用一般的decltype
推断规则。 -
Item 4: Know how to view deduced types
IDE推断:
一般适合简单的数据类型。IDE无法推断过于复杂或者运行时推断的类型。
编译器诊断:
编译器报错的时候显示。
使用库函数
库函数typeid().name()
#include <iostream>
using namespace std;
int main() {
struct Node {
int x, y;
} N;
std::cout << typeid(N).name() << std::endl;
}