Effective Modern C++ 第一章 C++11/14/17中的类型推断

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 是引用或者指针, 但不是右值

  1. 模板作为函数参数时,会忽略实际函数参数中的引用成分,但是如果原来模板中用引用参数,那么模板的引用起作用。

  2. expr的类型会优先覆盖掉ParamType的类型,以实际引用的为准。

  3. 如果模板中出现引用,那么不管实际参数中是否有引用,都作为引用看待。

    为代码举例:

    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的作用就是用来进行多线程编程。在单线程中那就是只能起到限制编译器优化的作用。在单线程编程的时候,除了不优化之外,几乎没有其他的作用。

总结:

  1. 在参数推断的过程中,原来参数的引用总是总是被当作没有引用看待。
  2. 参数是按值传递的模板的时候,constvolatile 总是被当作non-const和non-volatile看待,比如说 f(T param)的时候,param的引用会被忽略。
  3. 参数推断的时候,只有模板引用(f(T& param))形式的数组名被当作非指针看待,其余的数组名一律视为指针。

Item 2: Understand auto type deduction

auto的引用分为3种情况:

  1. 推断类型是引用或者指针,但不是右值引用
  2. 推断类型是右值引用
  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
*/

总结:

  1. auto类型基本与template类型推断一样,唯一的区别在于auto假设初始化是一个std::initial_list,但是template没有这个限制。
  2. auto推断列表类型的时候,不管列表的数据是什么,永远是占用16个字节
  3. auto推断列表的时候,列表内部的数据类型必须一致了。
  4. 函数返回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.pex1.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};

可以使用列表进行构造的条件:必须是一个聚合体,聚合体定义:

  1. 无用户自定义构造函数
  2. 无私有或者受保护的非静态数据成员。静态数据成员的初始化是不能通过初始化列表来完成初始化的,它的初始化还是遵循以往的静态成员的额初始化方式私有或者保护成员无法直接访问 。
  3. 无基类和虚函数
  4. 无{}和=直接初始化的非静态数据成员

如果不是聚合体,也可以构造,但是构造的结果会发生变化。

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;
}

总结:

  1. decltype基本都是用于不被更改的变量或者表达式上。
  2. 对于返回T类型的左值表达式来说,decltype总是推断成T&类型
  3. 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;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值