首页 > 编程语言 >C++填坑系列——EffectiveModernC++之类型推导

C++填坑系列——EffectiveModernC++之类型推导

时间:2024-03-02 14:56:53浏览次数:26  
标签:const 推导 int auto ParamType C++ 填坑 EffectiveModern void

接下来会记录我在学习《Effective Modern C++》的一些总结和思考。
鉴于C++的知识太多了,我难以全面覆盖到学习,所以这里借此来补充和发散自己的学习心得:)

以下内容由学习这个网站Effective Modern C++的中文翻译内容得来
https://cntransgroup.github.io/EffectiveModernCppChinese/Introduction.html

Chapter 类型推导

C++98有一套类型推导的规则:用于函数模板的规则。
C++11修改了其中的一些规则并增加了两套规则,一套用于auto,一套用于decltype
C++14扩展了autodecltype可能使用的范围。

Item 1: Understand template type deduction

一个比较普通的函数模板形式是这样的,其中T代表了基础的类型;ParamType代表了推导出来的param的类型(可能会包含一些修饰符,const&之类的)。

template<typename T>
void f(ParamType param);

f(expr);

如何理解模板推导?我觉得秉持这样一个方法就行:

  1. 先使用expr的类型和ParamType的类型共同来推导出最终的ParamType类型;
  2. 再基于ParamType的类型模式匹配得到T的类型。

下面的代码展示了模板推导的一些具体使用方式,以及推导的结果,总结一下需要注意的点就是:

  1. 数组和函数在推导T时(模板参数非引用),会退化为指针;模板参数为引用时,会推到为原类型;
  2. 模板参数为万能引用时,考虑引用折叠;
  3. int &b = a; c = b;一个引用在操作符=的右边时,该引用会被脱掉,对左边的操作数并无影响;在模板推导中一样;
  4. 推导时需要考虑语义上expr的类型,是否为const,尤其是模板参数为&*时,需要区分顶层const和底层const的影响;
template <typename T>
void func1(T param);

template <typename T>
void func2(T& param);

template <typename T>
void func3(const T param);

template <typename T>
void func4(const T& param);

template <typename T>
void func5(T&& param);

void add(int a, int b);

template <typename T>
void func6(T* param);

int main() {
  int a = 10;
  int& b = a;
  const int c = 20;
  const int& d = a;
  const int& e = c;
  int* f = &a;
  const int* g = &a;
  const int* const h = &a;
  int* p = &a;
  int arr[1]{1};

  /**
   * void func1(T param);
   * 1.无论传递什么,param都会成为它的一份拷贝(一个完整的新对象),那在模板内部无论怎么修改param,对外部实参都没啥影响(指针除外)。
   * 2.传递引用,会忽略&;传递const,会忽略const;
   * 3.传递指针,会将T推导为指针类型;
   *   但是:如果是底层const会保留const(指针传值会复制指针本身的内容,这样还可以通过该地址更改指向变量的内容);如果是顶层const会忽略;
   * 4.函数和数组会退化为指针进行推导;
   */
  func1(a);    // T=int                 -> T-int, ParamType-int
  func1(b);    // T=int&                -> T-int, ParamType-int
  func1(c);    // T=const int           -> T-int, ParamType-int
  func1(d);    // T=const int&          -> T-int, ParamType-int
  func1(e);    // T=const int&          -> T-int, ParamType-int
  func1(f);    // T=int*                -> T-int*, ParamType-int*
  func1(g);    // T=const int*          -> T-const int*, ParamType-const int*
  func1(h);    // T=const int* const    -> T-const int*, ParamType-const int*
  func1(p);    // T=int*                -> T-int*, ParamType-int*
  func1(arr);  // T=int[1]              -> T-int*, ParamType-int*
  func1(add);  // T=void (*)(int, int)  -> T-void (*)(int, int), ParamType-void (*)(int, int)

  /**
   * void func3(const T param);
   * 这里的const是个顶层const,它修饰的是T这个类型,这个类型应该当做整体来看,
   * 比如说T是个int*指针,那就表示指针是个const,它不能再指向其他了。
   * 推导T的规则和func(Tparam)都是一样的,只不过是会给ParamType添加一个顶层const的修饰符
   */
  func3(a);    // const T=int                 -> T-int, ParamType-const int
  func3(b);    // const T=int&                -> T-int, ParamType-const int
  func3(c);    // const T=const int           -> T-int, ParamType-const int
  func3(d);    // const T=const int&          -> T-int, ParamType-const int
  func3(e);    // const T=const int&          -> T-int, ParamType-const int
  func3(f);    // const T=int*                -> T-int*, ParamType-int* const
  func3(g);    // const T=const int*          -> T-const int*, ParamType-const int* const
  func3(h);    // const T=const int* const    -> T-const int*, ParamType-const int* const
  func3(p);    // const T=int*                -> const T=int* -> T-int*, ParamType-int* const
  func3(arr);  // const T=int[1]              -> const T=int* -> T-int*, ParamType-int* const
  func3(add);  // const T=void (*)(int, int)  -> T-void (*)(int,int), ParamType-void (*const)(int,int)

  /**
   * void func2(T& param);
   * 1. 函数和指针不会退化为指针进行推导;
   */
  func2(a);    // T&=int                -> T-int, ParamType-int&
  func2(b);    // T&=int&               -> T-int, ParamType-int&
  func2(c);    // T&=const int          -> T-const int, ParamType-const int&
  func2(d);    // T&=const int&         -> T-const int, ParamType-const int&
  func2(e);    // T&=const int&         -> T-const int, ParamType-const int&
  func2(f);    // T&=int*               -> T-int*, ParamType-int*&
  func2(g);    // T&=const int*         -> T-const int*, ParamType-const int*&
  func2(h);    // T&=const int* const   -> T-const int*, ParamType-const int* const&
  func2(p);    // T&=int*               -> T-int*, ParamType-int*&
  func2(arr);  // T&=int[1]             -> T-int[1], ParamType-int &param[1]
  func2(add);  // T&=void()(int,int)    -> T-void()(int,int), ParamType-void(&param)(int,int)

  /**
   * void func5(T&& param);
   * 1.如果expr是左值,T和ParamType会被推导为左值引用。这是模板类型推导唯一一种T被推导为引用的情况。
   * 2.如果expr是右值,就使用正常的推导规则。
   */
  func5(10);  // ParamType-int&&, T-int
  int&& t = 10;
  func5(t);    // 这里t依然是个左值(类型是右值引用),ParamType-int&, T-int&
  func5(a);    // T&&=int               -> ParamType-int&, T-int&
  func5(b);    // T&&=int&              -> ParamType-int&, T-int&
  func5(c);    // T&&=const int         -> ParamType-const int&, T-const int&
  func5(d);    // T&&=const int&        -> ParamType-const int&, T-const int&
  func5(e);    // T&&=const int&        -> ParamType-const int&, T-const int&
  func5(f);    // T&&=int*              -> ParamType-int*&, T-int*&
  func5(g);    // T&&=const int*        -> ParamType-const int*&, T-const int*&
  func5(h);    // T&&=const int* const  -> ParamType-const int* const&, T-const int* const&
  func5(p);    // T&&=int*              -> ParamType-int*&, T-int*&
  func5(arr);  // T&&=int[1]            -> ParamType-int(&param)[1], T-int(&param)[1]
  func5(add);  // T&&=void()(int,int)   -> ParamType-void(&)(int,int), T-void(&)(int,int)

  /**
   * void func6(T* param);
   */
  func6(f);    // T*=int*                -> T-int, ParamType-int*
  func6(g);    // T*=const int*          -> T-const int, ParamType-const int*
  func6(h);    // T*=const int* const    -> T-const int, ParamType-const int*
  func6(p);    // T*=int*                -> T-int, ParamType-int*
  func6(arr);  // T*=int[1]              -> T-int, ParamType-int*
  func6(add);  // T*=void (*)(int, int)  -> T-void ()(int, int), ParamType-void (*)(int, int)
}

Item 2: Understand auto type deduction

auto类型推导记住这两点即可:
1.auto类型推导和模板类型推导相同,例外是std::initializer_list
2.auto在c++14中允许出现在函数返回值或者lambda函数形参中,但是它的工作机制是模板类型推导那一套方案,而不是auto类型推导。

auto类型推导除了一个例外之外,其他推导规则和模板类型推导一样。

只记录下这个例外:std::initializer_list(文章最后记录下这是个啥)

对于auto来说,使用大括号来初始化一个变量时,该变量的类型会被推到为std::initializer_list

  auto a{27};     // int
  auto b = {27};  // std::initializer_list<int>

以上是c++11中的规则。在c++14中,auto允许用于函数返回值并会被推导,而且C++14的lambda函数也允许在形参声明中使用auto。但是在这些情况下auto使用的是模板类型推导的一套规则在工作,而不是auto的类型推导规则:

//// 
  auto func2 = [](auto& param) {};
  func2({1, 2, 3});  // 编译器会报错,虽然lambda形参有auto修饰,但是无法推导出来类型
////
auto func() {
  return {1, 2, 3};  // 编译器会报错:Cannot deduce return type from initializer list
}

Item 3: Understand decltype

之前有写了一篇文章,记录decltype的定义是一些用法,见:https://mp.weixin.qq.com/s/SYewMlUo-eLIk0c2U5mlhA

在C++11中,decltype最主要的用途就是用于声明函数模板,而这个函数返回类型依赖于形参类型

比如说,一个模板的参数是一个容器和一个索引,该模板的功能是返回这个容器索引位置的元素,但是这个元素我们并不知道具体的类型,所以需要推导。

c++11,函数authAndAccess1前面的auto不会做任何的类型推导。只是表明使用了C++11的尾置返回类型语法,即在函数形参列表后面使用一个->符号指出函数的返回类型,尾置返回类型的好处是可以在函数返回类型中使用函数形参相关的信息。

那下面的代码中authAndAccess2authAndAccess3有啥区别呢?

对于一般的容器来说,operator[]返回的应该是是一个T&的类型,是可以修改容器内元素的;authAndAccess3使用auto来推导返回值(编译器使用模板类型推导规则),模板类型推导会去掉引用的部分,因此返回类型是T,这样就无法修改容器内元素;authAndAccess2中使用decltype(auto)会保留c[i]的类型。

//// c++11写法
template <typename Container, typename Index>
auto authAndAccess1(Container& c, Index i) -> decltype(c[i]) {
  return c[i];
}
//// c++14写法
template <typename Container, typename Index>
decltype(auto) authAndAccess2(Container& c, Index i) {
  return c[i];
}
template <typename Container, typename Index>
auto authAndAccess3(Container& c, Index i) {
  return c[i];
}

Item 附录

1. 数组指针和引用数组

char(a)[2]表示a是一个数组,是一个包含两个元素的char数组;
char(*a)[2]表示的是a是一个指针,它指向一个包含两个元素的char数组;
char(&a)[2]表示a是一个数组引用,引用一个包含两个元素的char数组;

下面展示代码,其中&bbb*ccc的地址一致,可以看到通过ccc获取到数组元素需要两次寻址(需要两次解引用)。

char bbb[2]{'1', '2'};
char(*ccc)[2] = &bbb;
char(&ddd)[2] = bbb;
printf("%p\n", &bbb);         // 0x7ff7bcc3d33e
printf("%p\n", &ccc);         // 0x7ff7bcc3d330
printf("%p\n", *ccc);         // ccc指向的地址和bbb是一样的,0x7ff7bcc3d33e
printf("%c\n", *bbb);         // 1
printf("%c\n", *(bbb + 1));   // 2
printf("%c\n", *(*ccc));      // 1;要获取数组元素,需要先解一次引用(需要两次寻址)
printf("%c\n", *(*ccc + 1));  // 2;要获取数组元素,需要先解一次引用(需要两次寻址)

2. std::initializer_list

std::initializer_list是 C++11 引入的一个轻量级的模板类,提供了一种访问初始化列表(由大括号括起来的值列表)的方法。这个类特别适用于构造函数和函数,允许它们接收任意数量的同类型参数,而不需要预先定义参数的数量。

构建时机:

  1. 大括号初始化被用来列表初始化一个对象时,该对象的构造函数可以接收一个std::initializer_list的参数;
  2. 大括号初始化作为赋值操作符的右操作数或者作为函数调用的参数时,该赋值操作符和函数调用可以接收一个std::initializer_list的参数;
  3. 大括号初始化用于auto中,包括应用于基于范围的for循环。
//// 1.
std::vector<int> vec1{1, 2, 3, 4};
// 在cppinsights中看到编译器执行的代码其实是
std::vector<int, std::allocator<int> > vec1 = std::vector<int, std::allocator<int> >{std::initializer_list<int>{1, 2, 3, 4}, std::allocator<int>()};

//// 2.
vec1.insert(vec1.end(), {5, 6, 7});
// 在cppinsights中看到编译器执行的代码其实是
vec1.insert(__gnu_cxx::__normal_iterator<const int *, std::vector<int, std::allocator<int> > >(vec1.end()), std::initializer_list<int>{5, 6, 7});

//// 3.
auto x = {1,23};
// 在cppinsights中看到编译器执行的代码其实是
std::initializer_list<int> x = std::initializer_list<int>{1, 23};

//// 3.
for (auto v : {3, 4, 5, 6}) {}
// 在cppinsights中看到编译器执行的代码其实是
  {
    std::initializer_list<int> && __range1 = std::initializer_list<int>{3, 4, 5, 6};
    const int * __begin1 = __range1.begin();
    const int * __end1 = __range1.end();
    for(; __begin1 != __end1; ++__begin1) {
      int v = *__begin1;
    }
  }

特点:

  • 类型是安全的:保证列表中的所有元素必须都是相同的类型,否则推导模板中的T会失败;
  • 自动推导类型:在使用std::initializer_list时,不需要指定元素的具体类型,编译器会自动推导出列表中元素的类型;
  • 只读:std::initializer_list中的元素是只读的,不能修改列表中的元素;
  • 轻量级:std::initializer_list本身并不拥有所包含的元素。只是持有指向临时数组的指针和数组的大小;
  • 支持范围迭代:std::initializer_list支持基于范围的for循环;
  • 标准库兼容性:很多标准库容器都有接受std::initializer_list作为参数的构造函数,初始化容器变得非常方便。

标签:const,推导,int,auto,ParamType,C++,填坑,EffectiveModern,void
From: https://www.cnblogs.com/pplearn/p/18048630

相关文章

  • C++填坑系列——EffectiveModernC++之新手怎么进入现代C++
    新手怎么进入现代C++1.使用auto来自动推导变量类型2.使用{}来创建变量和对象3.使用nullptr来创建空指针4.使用using代替typedef进行别名定义5.使用enumclass代替enum进行枚举定义6.使用=delete来禁止调用一个函数7.使用override来修饰继承链中的重写函数8.使用co......
  • C++填坑系列——EffectiveModernC++之特殊成员函数
    Chapter移步现代c++之特殊成员函数Item17:Understandspecialmemberfunctiongeneration总结:有必要了解各个函数什么时候自动生成;自动生成的函数有可能产生预期外的行为;特殊成员函数(编译器自动生成):默认构造函数,析构函数,拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋......
  • C++ 多线程笔记2 线程同步
    C++多线程笔记2线程同步并发(Concurrency)和并行(Parallelism)并发是指在单核CPU上,通过时间片轮转的方式,让多个任务看起来像是同时进行的。实际上,CPU在一个时间段内只会处理一个任务,但是由于切换时间非常快,用户感觉像是多个任务同时在进行。这种方式的优点是可以充分利用CPU资源,......
  • C++ 类构造函数 & 析构函数
    带参数的构造函数默认的构造函数没有任何参数,但如果需要,构造函数也可以带有参数。这样在创建对象时就会给对象赋初始值,如下面的例子所示:1#include<iostream>2usingnamespacestd;34classLine5{6public:7voidsetLength(doublelen);8......
  • 万物容器与 c++ 类型反射
    这是一篇组会分享,并且是拖了很长很长时间的那种。这次不会再鸽了这篇文章可以说是针对某cpp佬的公众号的两篇原创内容的笔记c++反射--包容一切的all容器(上)c++反射--包容一切的all容器(中)什么是反射这个好像没有严格的定义,但是概括的说,「反射」是指在程序运行期对程序......
  • C++中cin的详细用法
    1.cin简介cin是C++编程语言中的标准输入流对象,即istream类的对象。cin主要用于从标准输入读取数据,这里的标准输入,指的是终端的键盘。此外,cout是流的对象,即ostream类的对象,cerr是标准错误输出流的对象,也是ostream类的对象。这里的标准输出指的是终端键盘,标准错误输出指的是终端的......
  • C++填坑系列——类型推导 decltype
    decltypedecltype主要是为了解决类型推导的问题,特别是在模板编程和泛型编程中应用较广泛。decltype关键字用于以表达式为参数,推导表达式返回的类型,该类型会保留所有信息。c++11提出的新特性,decltype关键字。和auto一样都是用来做编译时类型推导的,但是也有一些区别:auto:从......
  • C++填坑系列——左值和右值
    c++的表达式首先介绍下c++的表达式是什么?看下cppreference是怎么说的。Anexpressionisasequenceofoperatorsandtheiroperands,thatspecifiesacomputation.也就是说,在C++中,表达式(Expression)是由操作数(Operands)和运算符(Operators)组成的序列。左值和右值就是c++中......
  • C++填坑系列——lambda表达式
    lambda表达式总结:lambda表达式原理:被编译器转换为类+初始化对象的代码;格式:[captureslist](paramslist)specifiersexception->retType{funtionbody}按值捕获和按引用捕获的优缺点以及解决方法;一.lambda原理lambda函数会被编译器转换为类,并定义实现一个operato......
  • C++ 类访问修饰符
    私有(private)成员成员和类的默认访问修饰符是private,如果没有使用任何访问修饰符,类的成员将被假定为私有成员。私有成员变量或函数在类的外部是不可访问的,甚至是不可查看的。只有类和友元函数可以访问私有成员。实际操作中,我们一般会在私有区域定义数据,在公有区域定义相关的函数......