当前位置: 首页 > news >正文

Effective C++阅读笔记(item 1-4)

文章目录

  • 理解模板类型推导
  • 理解auto类型推导
  • 理解decltype
  • 学会查看类型推导结果

理解模板类型推导

c++的auto特性是建立在模板类型推到的基础上。坏消息是当模板类型推导规则应用于auto环境时,有时不如应用于template时那么直观。我们可能很自然的期望T和传递进函数的实参是相同的类型,也就是,Texpr的类型。但有时情况并非总是如此,T的类型推导不仅取决于expr的类型,也取决于ParamType的类型。这里有三种情况:

template<typename T>
void f(ParamType param)

情景一:ParamType是一个指针或引用,但不是通用引用

  1. 如果expr类型是一个引用,会忽略引用部分【因为引用部分独立于T】
  2. 然后expr的类型与ParamType进行模式匹配来决定T

情景二:ParamType是一个通用引用(T&&)

  1. 如果expr是左值,T和paramType都会被推到为左值引用。第一,这是模板类型推导中唯一一种T被推导为引用的情况。
  2. 如果expr是右值,就使用正常的(也就是情景一)推导规则(忽略引用后于ParamType进行模式匹配来决定T)

情景三:ParamType既不是指针也不是引用

void f(T param);

  1. 和之前一样,如果expr的类型是一个引用,忽略这个引用部分
  2. 如果忽略expr的引用性(reference-ness)之后,expr是一个const,那就再忽略const。如果它是volatile,也忽略volatilevolatile对象不常见,它通常用于驱动程序的开发中。关于volatile的细节请参见40)

忽略引用行在指向常量的常量指针情况下只会忽略底层const,这一特性只会对形参本身有效,因此指针的常量特定会被忽略,但是指针所指对象的常量特性会被保留。

在类型推导中,这个指针指向的数据的常量性constness将会被保留,但是当拷贝ptr来创造一个新指针param时,ptr自身的常量性constness将会被忽略。

特殊情况:数组实参(函数类似)

在大多数情况下数组会被退化为指针。如果我们ParamType就是T,那么传递数组实参和传递指针形参情况类似,推断的结果是一个指针。但是如果声明为传递引用形参的模板,T被推到为真正的数组!

template<typename T>
void f(T& param);       
f(name);//name是一个数组指向const char[13],T也是一个数组,且ParamType为const char(&)[13]

有趣的是,可声明指向数组的引用的能力,使得我们可以创建一个模板函数来推导出数组的大小:

//在编译期间返回一个数组大小的常量值(//数组形参没有名字,
//因为我们只关心数组的大小)
template<typename T, std::size_t N>                     //关于
constexpr std::size_t arraySize(T (&)[N]) noexcept      //constexpr
{                                                       //和noexceptreturn N;                                           //的信息
}                                                       //请看下面

总结

  • 在模板类型推导时,有引用的实参会被视为无引用,他们的引用会被忽略
  • 对于通用引用的推导,左值实参会被特殊对待
  • 对于传值类型推导,const和/或volatile实参会被认为是non-const的和non-volatile
  • 在模板类型推导时,数组名或者函数名实参会退化为指针,除非它们被用于初始化引用

理解auto类型推导

当一个变量使用auto进行声明时,auto扮演了模板中T的角色,变量的类型说明符扮演了ParamType的角色。废话少说,这里便是更直观的代码描述,考虑这个例子:


template<typename T>            //概念化的模板用来推导x的类型
void func_for_x(T param);
auto x=27;
func_for_x(27);                 //概念化调用://param的推导类型是x的类型template<typename T>            //概念化的模板用来推导cx的类型
void func_for_cx(const T param);
const auto cs=x;
func_for_cx(x);                 //概念化调用://param的推导类型是cx的类型template<typename T>            //概念化的模板用来推导rx的类型
void func_for_rx(const T & param);
const auto& rx=x
func_for_rx(x);                 //概念化调用://param的推导类型是rx的类型

不同于模板类型推导的特殊情况

这就造成了auto类型推导不同于模板类型推导的特殊情况。当用auto声明的变量使用花括号进行初始化,auto类型推导推出的类型则为std::initializer_list。如果这样的一个类型不能被成功推导(比如花括号里面包含的是不同类型的变量),编译器会拒绝这样的代码:

auto x1 = 27;                   //类型是int,值是27
auto x2(27);                    //同上
auto x3 = { 27 };               //类型是std::initializer_list<int>,//值是{ 27 }
auto x4{ 27 };                  //同上auto x5 = { 1, 2, 3.0 };        //错误!无法推导std::initializer_list<T>中的T

对于模板,无法推导花括号为std::initializer_list

template<typename T>
void f(std::initializer_list<T> initList);f({ 11, 23, 9 });               //T被推导为int,initList的类型为//std::initializer_list<int>

新标准:

==在c++14允许auto用于函数返回值并会被推导。==c++14也允许lambda函数在形参声明中使用auto,但是在这些情况下,auto实际使用模板类型推导的那一套规则在工作,而不是auto类型推导。所以下面这样的代码不会通过编译。

auto createInitList(){return {1,2,3};
}
std::vector<int> v;auto resetV = [&v](const auto& newValue){ v = newValue; };        //C++14resetV({ 1, 2, 3 });            //错误!不能推导{ 1, 2, 3 }的类型

理解decltype

我们将从一个简单的情况开始,没有任何令人惊讶的情况。相比模板类型推导和auto类型推导,decltype只是简单的返回名字或者表达式的类型(不会忽略const和引用)。decltype作用与返回左值的表达式得到的是一个引用。(我们可以为返回左值的表达式赋值)。例如decltype中表达式的内容是一个解引用(*p),返回的并不是指针所指向的对象本身,而是一个引用。

在C++11中,decltype最主要的用途就是用于声明函数模板,而这个函数返回类型依赖于形参类型。函数名称前面的auto不会做任何的类型推导工作。相反的,他只是暗示使用了C++11的尾置返回类型语法,即在函数形参列表后面使用一个”->“符号指出函数的返回类型,尾置返回类型的好处是我们可以在函数返回类型中使用函数形参相关的信息。

template<typename Container, typename Index>    //可以工作,
auto authAndAccess(Container& c, Index i)       //但是需要改良->decltype(c[i])
{authenticateUser();return c[i];
}

C++11允许自动推导单一语句的lambda表达式的返回类型, C++14扩展到允许自动推导所有的lambda表达式和函数,甚至它们内含多条语句。对于authAndAccess来说这意味着在C++14标准下我们可以忽略尾置返回类型,只留下一个auto。使用这种声明形式,auto标示这里会发生类型推导。更准确的说,编译器将会从函数实现中推导出函数的返回类型。

然而这么做会出现一个问题,大多数T类型的容器会返回一个T&,但是模板类型推导期期间,表达式的引用性会被忽略。要想让authAndAccess像我们期待的那样工作,我们需要使用decltype类型推导来推导它的返回值,即指定authAndAccess应该返回一个和c[i]表达式类型一样的类型。

c++14中可以使用decltype(auto)说明符使得这成为可能。实际上我们可以这样解释它的意义:auto说明符表示这个类型将会被推导,decltype说明decltype的规则将会被用到这个推导过程中。因此我们可以这样写authAndAccess

decltype(auto)的使用不仅仅局限于函数返回类型,当你想对初始化表达式使用decltype推导的规则,你也可以使用:

template<typename Container, typename Index>    //C++14版本,
decltype(auto)                                  //可以工作,
authAndAccess(Container& c, Index i)            //但是还需要
{                                               //改良authenticateUser();return c[i];
}
decltype(auto) muWidget2 = cw;

上述authAndAccess函数实际上还存在一些漏洞。为了使函数支持左值引用和右值引用==(这种情况很少,但向authAndAccess传递一个临时变量也并不是没有意义,有时候用户可能只是想简单的获得临时容器中的一个元素的拷贝,比如这样)==,我们需要将声明改为通用引用(也可以重载函数)。

注意这里我们也并不知道index的类型,但我们并没有声明为引用的形式。因为就容器索引来说,我们遵照标准模板库对于索引的处理是有理由的(比如std::stringstd::vectorstd::dequeoperator[]),所以我们坚持传值调用。

最后我们用std::forward来实现通用引用(22)

template<typename Container, typename Index>    //最终的C++14版本
decltype(auto)
authAndAccess(Container&& c, Index i)
{authenticateUser();return std::forward<Container>(c)[i];//使用std::forward保证实参c和形参c的引用类型一致(都是左值引用或右值引用)
}
//对于c++11需要用尾置返回值
//->decltype(std::forward<Contrainer>(c)[i])

特殊情况

decltype应用于变量名会产生该变量名的声明类型。然而,对于比单纯的变量名更复杂的左值表达式,decltype可以确保报告的类型始终是左值引用。也就是说,如果一个不是单纯变量名的左值表达式的类型是T,那么decltype会把这个表达式的类型报告为T&

我们回到最开始说的*p的情况,p是一个单纯变量名,如果decltype§返回的是p的声明类型,然而*p是返回左值的复杂表达式了,因此decltype返回的是左值引用,类型是表达式的类型。

总结

decltype(单独使用或者与auto一起用)可能会偶尔产生一些令人惊讶的结果,但那毕竟是少数情况。通常,decltype都会产生你想要的结果,尤其是当你对一个变量使用decltype时,因为在这种情况下,decltype只是做一件本分之事:它产出变量的声明类型。

  • decltype总是不加修改的产生变量或者表达式的类型。
  • 对于T类型的不是单纯的变量名的左值表达式,decltype总是产出T的引用即T&
  • C++14支持decltype(auto),就像auto一样,推导出类型,但是它使用decltype的规则进行推导。

学会查看类型推导结果

我们探究三种方案:在你编辑代码的时候获得类型推导的结果,在编译期间获得结果,在运行时获得结果。

IDE编辑器

在IDE中的代码编辑器通常可以显示程序代码中变量,函数,参数的类型,你只需要简单的把鼠标移到它们的上面,举个例子,有这样的代码中:

运行时输出

std::cout << typeid(x).name() << '\n';  //显示x和y的类型
std::cout << typeid(y).name() << '\n';

调用std::type_info::name不保证返回任何有意义的东西,但是库的实现者尝试尽量使它们返回的结果有用。实现者们对于“有用”有不同的理解。举个例子,GNU和Clang环境下x的类型会显示为”i“,y会显示为”PKi“,这样的输出你必须要问问编译器实现者们才能知道他们的意义:”i“表示”int“,”PK“表示”pointer to konst const“(指向常量的指针)。(这些编译器都提供一个工具c++filt,解释这些“混乱的”类型)Microsoft的编译器输出得更直白一些:对于x输出”int“对于y输出”int const *

std::type_info::name规范批准像传值形参一样来对待这些类型。正如item1中提到的,如果传递的是一个引用,那么引用部分(reference-ness)将被忽略,如果忽略后还具有const或者volatile,那么常量性constness或者易变性volatileness也会被忽略。

依赖于库

std::type_info::name和IDE失效的地方,Boost TypeIndex库(通常写作Boost.TypeIndex)被设计成可以正常运作。这个库不是标准C++的一部分,也不是IDE或者TD这样的模板。Boost库(可在boost.com获得)是跨平台,开源,有良好的开源协议的库,这意味着使用Boost和STL一样具有高度可移植性。

这里是如何使用Boost.TypeIndex得到f的类型的代码

#include <boost/type_index.hpp>template<typename T>
void f(const T& param)
{using std::cout;using boost::typeindex::type_id_with_cvr;//显示Tcout << "T =     "<< type_id_with_cvr<T>().pretty_name()<< '\n';//显示param类型cout << "param = "<< type_id_with_cvr<decltype(param)>().pretty_name()<< '\n';
}std::vetor<Widget> createVec();         //工厂函数
const auto vw = createVec();            //使用工厂函数返回值初始化vw
if (!vw.empty()){f(&vw[0]);                          //调用f}
//假设vw是一个const类型,我们传递的是其中一个元素的地址(该元素也是const),因此T被推断为const(底层const保留)和*,而param 被推断为const * const&,顶层const在形参列表中写明,&在形参列表中写明。T =     Widget const *
param = Widget const * const&

相关文章:

  • 【Vue】路由2——编程式路由导航、 两个新的生命周期钩子 以及 路由守卫、路由器的两种工作模式
  • 网络工程师案例分析
  • 【Android】一键创建Keystore + Keystore 参数说明 + 查询SHA256(JDK Keytool Keystore)
  • 从 “龟速” 到流畅,英国 - 中国 SD-WAN 专线让分公司直连总部系统
  • js逆向反调试的基本 bypass
  • 智慧招生:实时数字人在院校招生中的应用
  • 深度学习实战107-基于Qwen3+GraphRAG+Agent的智能文档管理系统:精准问答与决策支持
  • 【OSS】如何使用OSS提供的图片压缩服务
  • 大二周周练翻译
  • 动态规划-LCR 089.打家劫舍-力扣(LeetCode)
  • 多类别异常检测新SOTA-MVMCAD
  • 如何通过生成式人工智能认证(GAI认证)提升自己的技能水平?
  • 中小实验室质检LIMS 系统选型 从成本管控到竞争力升级的黄金法则
  • Mybatis操作数据库(2)
  • [学习]POSIX消息队列的原理与案例分析(完整示例代码)
  • 基于DolphinScheduler抽取通用EventBus组件:支持延迟与事件驱动
  • Ubuntu开机自启服务
  • 关于systemverilog中在task中使用force语句的注意事项
  • 第三十八节:视频处理-视频保存
  • 线程调度与单例模式:wait、notify与懒汉模式解析
  • 外媒称北斗挑战GPS地位,外交部:中国的北斗也是世界的北斗
  • 习近平:坚定信心推动高质量发展高效能治理,奋力谱写中原大地推进中国式现代化新篇章
  • 一季度支持科技创新和制造业发展减税降费及退税4241亿元
  • 广东信宜一座在建桥梁暴雨中垮塌,镇政府:未造成人员伤亡
  • 英国知名歌手批政府:让AI公司免费使用艺术家作品是盗窃
  • 打造信息消费新场景、新体验,上海信息消费节开幕