杂货边角(12):C++11动态性来源之类型推断 && auto\decltype\追踪返回类型
2018-02-01 22:08
375 查看
在程序员眼中,如果非要二元论的谈论编程语言,那么相信很多人会说出语言存在动态和静态语言之分,其中静态语言以C++、Java为代表,而动态语言则以近年来风头正盛的Python、Ruby领衔。其实语言的动静之分主要是在于对变量进行类型检查的时间点不同,对于静态语言而言,类型检查是发生在编译阶段,因为变量在.data区或者堆栈上的占用空间大小要提前确定,这样才能给予地址分配;而对于动态语言而言,变量的类型检查则是发生在运行阶段,通过所谓的类型推导技术来实现延迟检查的。以前大家都说C++太过于死板了,比不上Python等脚本语言来的便捷,所以做验证阶段很多人都是喜欢先用Python验证逻辑,然后才决定是否使用C++转化为工业级代码。现在C++11通过引入
目录:
Sec1 auto关键词
Sec2 追踪返回值类型
Sec3 decltype
使用auto关键词对于代码的可维护性和代码可阅读性有极大的提升,分为如下几个场景:
1 . 冗长变量类型命名场景
现在C++命名空间、类、模板等机制,导致现在复合变量类型极为复杂,想下面这种变量声明方式在使用STL库中很常见
这种变量声明方式过于繁琐,如果不借助IDE的提示补全功能,很难保证不会出现手抖出错的情况。这种情况下,其实
2 . 调用外部API,无需手动声明返回值类型用以承接函数返回值
C++中,存在着很多隐式或者用户自定义的类型转换规则(比如整型与字符型进行加法运算后,表达式返回int,这便是一条隐式转换),这些规则不容易记忆,尤其是同类API接口较多时,很容易混淆。这时候,完全可以通过auto占位,让编译器完成后续的返回值承接问题。
此外还有一点好处,便是auto的使用其实某种意义上有“宏一次性批量替换”的好处,比如上面代码,如果PI内部的返回值要变成
3 . 继承函数宏的优点,并进一步优化
4 . 配合范型编程,更加通畅
C++范型编程的能力此前主要是template特性引入的,而C++11通过auto、decltype和类型追踪以及可变参数模板variadic template将C++范型编程的能力提升了一个新的层面,来看下下面的范型增强案例
C++11通过引入追踪返回值类型和variadic template等动态脚本特性,就是为了弥补此前C++给人属于静态语言的死板印象,相信大多数人跟我一样,简单的小程序还是喜欢用Python写,验证逻辑之后如果存在性能的需求才会转向C++重新开发。现今C++11引入这一系列动态特性后,让C++写起简答的程序也是和Python等动态语言一样自然流畅。
应用于嵌套函数的定义
此前讨论过工厂方法模式封装不同功能函数细节的设计模式,使用追踪返回值类型可以极大地增强中间件的范型能力。
关于
但是RTTI最大的不足在于,它是在运行时才能确定信息,程序员使用RTTI只能识别类型信息但往往在我们使用STL库等时是需要再编译时期便确定下动态类型,是要使用动态类型而非仅仅识别动态类型,故而C++98的动态特性并不能满足要求。而随着C++11进一步提升范型编程的能力,类型推导这一编译时期确定动态类型不得不引入。在范型编程中参数类型变成了未知,但是RTTI是运行时才能确定,显然在编译期间无法满足我们范型编程的需求,而decltype发挥威力便是在编译期间延迟绑定类型信息,虽有前后,但是保证在编译期间完成所有类型信息的确定,这便是decltype的魅力所在。
可以看到decltype的类型推导并不像auto一样从变量声明的初始化表达式即右式获得变量的类型,decltype是以一个普通表达式为参数,返回该表达式返回值的类型。在C++11中,decltype配合typedef/using完成类型推导是常用的操作,比如如下代码:
auto,
decltype来实现其动态特性。
目录:
Sec1 auto关键词
Sec2 追踪返回值类型
Sec3 decltype
Sec1. auto关键词
auto作为C早期的关键词之一,是用来表明修饰的变量为局部变量,存放在堆栈上,但是这个关键词和
static正好对应,功能实现重复,故而标准委员会重新修改了
auto的含义。用来作为关键词指示编译器,
auto修饰的变量的类型需要由编译器在编译时期自主推导。
auto声明的变量必须被初始化,以使编译器能够从其初始化表达式中推导出其类型。所以
auto更接近一种类型声明时的“占位符”,编译器在编译时期会将auto替换为变量实际的类型。
使用auto关键词对于代码的可维护性和代码可阅读性有极大的提升,分为如下几个场景:
1 . 冗长变量类型命名场景
现在C++命名空间、类、模板等机制,导致现在复合变量类型极为复杂,想下面这种变量声明方式在使用STL库中很常见
std::vector< std::string > val; ... std::vector< std::string > :: iterator i = val.begin()
这种变量声明方式过于繁琐,如果不借助IDE的提示补全功能,很难保证不会出现手抖出错的情况。这种情况下,其实
val变量的类型此前已经通过
std::vector< std::string >声明过了,所以完全没有必要再次重复一次输入,可以借鉴宏利用预编译器完成编译器替换的思想,这种重复的变量声明操作完全也可以交给编译器顺藤摸瓜。
std::vector< std::string > val; ... auto i = val.begin()
2 . 调用外部API,无需手动声明返回值类型用以承接函数返回值
C++中,存在着很多隐式或者用户自定义的类型转换规则(比如整型与字符型进行加法运算后,表达式返回int,这便是一条隐式转换),这些规则不容易记忆,尤其是同类API接口较多时,很容易混淆。这时候,完全可以通过auto占位,让编译器完成后续的返回值承接问题。
class PI{ public: double operator* (float v) { return (double) val * v; //这里精度被扩展了 } }; /*对于PI,如果该类定义在其他的地方,main程序的coder可能不知道PI的作者为了避免数据溢出或者精度降低,而 *在PI类内部主动扩展了精度为double,所以如果main的coder如果想当然的将return_value声明为float, *那么显然就不能享受PI作者的细心设计了 */ int main() { float radius = 1.7e10; PI pi; auto circumference = 2 * (pi * radius); }
此外还有一点好处,便是auto的使用其实某种意义上有“宏一次性批量替换”的好处,比如上面代码,如果PI内部的返回值要变成
long double,如果main中用的是显式地类型声明,如
double circumference = 2*pi*radius,那么显然要修改的地方便是分布多处极为难以维护的。所以auto还具有的便是宏的一次性替换自适应特性。
3 . 继承函数宏的优点,并进一步优化
#define Max1(a, b) ((a) > (b)) ? (a) : (b) #define Max2(a, b) ({ \ auto _a = (a); auto _b = (b); (_a > _b) ? _a : _b; }) int main() { int m1 = Max1(1*2*3*4, 5+6+7*8); //如果使用宏,则无论是取a还是b,其中一项都会计算两次 int m2 = Max2(1*2*3*4, 5+6+7*8); //使用了auto可以在宏函数的基础上进一步添加处理存储的步奏,从能避免重复计算 }
4 . 配合范型编程,更加通畅
C++范型编程的能力此前主要是template特性引入的,而C++11通过auto、decltype和类型追踪以及可变参数模板variadic template将C++范型编程的能力提升了一个新的层面,来看下下面的范型增强案例
/* *level1: 只使用template 和 auto,这样的加法对于任何两中基本数据类型都可以复用,省去了重载的麻烦 *但是为了保证返回值的精度,即返回值必须得为所有组合情况兜底,则只能声明为size=8的double,限制了范型 *的威力 */ template<typename T1, typename T2> double Sum(T1 & t1, T2 & t2) { auto s = t1 + t2; return s; } /* *level2: 使用template 和 decltype配合,使用decltype(t1+t2)作为额外参数用以承接返回值 *这一版本虽然对返回值没有double的限死,增加了改函数模板的复用性,但是比较麻烦的是需要coder *在调用函数时,额外定义返回者承接变量,如下所示 */ template<typename T1, typename T2> void Sum(T1 & t1, T2 & t2, decltype(t1 + t2) & s) { s = t1 + t2; } int main() { int a = 3; long b = 5; float c = 1.0f, d = 2.3f; long e; float f; Sum(a, b, e); //可以看到e需要事先声明为long,然后作为引用传递传给函数,显然可以看到范型编程能力缺陷 Sum(c, d, f); } /*那么是否可以直接写成下面这样呢?*/ template<typename T1, typename T2> decltype(t1 + t2) Sum(T1 & t1, T2 & t2) { return t1 + t2; } /*看起来逻辑很完美,但是! decltype(t1+t2)在前,编译器在推导时,表达式中t1和t2都未声明,虽然它们靠的 *近,但是编译器只能从左往右地读入变量符号,随意上面的方式不行*/ /*思考一下,就可以知道,本来template特性的实现就是借助编译器在运行时延迟绑定,就已经存在取巧了,那么 *上面的思路挺好的,只不过decltype(t1+t2)在前让编译器过不了先定义后使用这关,那么我们完全可以采用语义上 *的特殊变形,来实现返回值类型后确定的功能,这便是追踪返回类型 */ template<typename T1, typename T2> auto Sum(T1 & t1, T2 & t2) -> decltype(t1 + t2) { return t1 + t2; } //先由auto关键字占位,然后后续通过复合符号->decltype(t1+t2)来实现返回值延迟确定
Sec2. 追踪返回值类型
追踪返回值类型个人举得是一个很好的语法设计,和普通函数最大的区别在于返回类型的后置,从而可以配合auto、decltype完成返回值类型延迟绑定。简单的追踪返回值类型声明函数的对比如下int func(char* a, int b); ---> auto func(char* a, int b) ->int;
C++11通过引入追踪返回值类型和variadic template等动态脚本特性,就是为了弥补此前C++给人属于静态语言的死板印象,相信大多数人跟我一样,简单的小程序还是喜欢用Python写,验证逻辑之后如果存在性能的需求才会转向C++重新开发。现今C++11引入这一系列动态特性后,让C++写起简答的程序也是和Python等动态语言一样自然流畅。
template<typename T1, typename T2> auto Sum(const T1& t1, const T2& t2) -> decltype(t1 + t2){ return t1 + t2; } template<typename T1, typename T2> auto Mul(const T1& t1, const T2& t2) -> decltype(t1 * t2){ return t1 * t2; } int main() { auto a = 3; auto b = 4L; auto pi = 3.14; auto c = Mul( Sum(a, b), pi); cout << c << endl; }
应用于嵌套函数的定义
/*传统的函数指针嵌套*/ int pf3() { return 10; } int (*pf2())(){ return pf3; } int ( *( *pf() )() ) (){ return pf2; } /*使用追踪返回值类型机制,则可以让嵌套逻辑更为清晰*/ int pf3() { return 10; } auto pf2() -> int (*)() { return pf3; } auto pf1() -> auto (*)() -> int (*)() { return pf2; } #include <type_traits> //关键库文件,包含了或许的类型比较操作is_same ... cout << is_same<decltype(pf1), decltype(pf)>::value; //1
此前讨论过工厂方法模式封装不同功能函数细节的设计模式,使用追踪返回值类型可以极大地增强中间件的范型能力。
double foo(int a){ return (double)a + 0.1; } int foo(double b){ return (int)bl } template<typename T> auto Forward(T t) -> decltype(foo(t)){ return foo(t); } int main() { cout << Forward(2) << endl; //2.1 cout << Forward(0.5) << endl; //0 }
Sec3. decltype
其实从C++98开始,标准委员会就已经有意识地在增强C++的脚本能力,比如RTTI运行时类型识别机制,以typeid\type_info等数据为核心构建。如库文件< typeinfo.h>提供的type_info数据结构,coder可以通过typeid主动查询某个变量相对应的type_info数据。如下virtual bool isType(const std::type_info& _type) { return typeid(CStaticDelegate<ReturnType, ParamType...>) == _type; }
关于
#include <typeinfo> #include <iostream> using namespace std; class White{}; class Black{}; int main() { White a; Black b; White c; cout << typeid(a).name() << endl; //5White cout << typeid(b).name() << endl; //5Black bool a_b_sametype = (typeid(a).hash_code() == typeid(b).hash_code()); bool a_c_sametype = (typeid(a).hash_code() == typeid(c).hash_code()); cout << "A and B?" << (int)a_b_sametype << endl; //0 cout << "A and C?" << (int)a_c_sametype << endl; //1 }
但是RTTI最大的不足在于,它是在运行时才能确定信息,程序员使用RTTI只能识别类型信息但往往在我们使用STL库等时是需要再编译时期便确定下动态类型,是要使用动态类型而非仅仅识别动态类型,故而C++98的动态特性并不能满足要求。而随着C++11进一步提升范型编程的能力,类型推导这一编译时期确定动态类型不得不引入。在范型编程中参数类型变成了未知,但是RTTI是运行时才能确定,显然在编译期间无法满足我们范型编程的需求,而decltype发挥威力便是在编译期间延迟绑定类型信息,虽有前后,但是保证在编译期间完成所有类型信息的确定,这便是decltype的魅力所在。
float a_te; double b_te; decltype(a_te + b_te) c_te; cout << typeid(c_te).name() << endl; //打印"d",g++打印double
可以看到decltype的类型推导并不像auto一样从变量声明的初始化表达式即右式获得变量的类型,decltype是以一个普通表达式为参数,返回该表达式返回值的类型。在C++11中,decltype配合typedef/using完成类型推导是常用的操作,比如如下代码:
using size_t = decltype(sizeof(0)); //typedef decltype(sizeof(0)) size_t; using ptrdiff_t = decltype((int*)0 - (int*)0); using nullptr_t = decltype(nulltype);
相关文章推荐
- C++11特性--auto,decltype,返回类型后置,using=,nullptr
- c++11之auto自动类型推断和decltype类型获取
- C++11:类型推导和追踪函数返回类型decltype
- auto decltype 用于返回值类型后置时的占位
- C++11 auto类型说明符如for(atuo &x : s)
- C++11:"auto"和"decltype"类型说明符的思考
- C++11系列-改进的类型推导:auto、decltype和新的函数语法
- C++11新特性:类型别名,auto类型,decltype类型
- C++11新特性——auto和decltype类型推导
- C++11 FAQ中文版:decltype – 推断表达式的数据类型
- C++11系列-改进的类型推导:auto、decltype和新的函数语法
- C++11 auto&decltype
- C++11特性(7):auto、decltype(自匹配类型)
- c++11 追踪返回类型
- C++11系列-改进的类型推导:auto、decltype和新的函数语法
- C++11系列-改进的类型推导:auto、decltype和新的函数语法
- C++11初探:类型推导,auto和decltype
- C++11 FAQ中文版:auto – 从初始化中推断数据类型
- C++11特有的数值、数组初始化方法、常量的符号名称 const和浮点数、bool、自动推断类型auto
- C++11标准 类型别名 auto decltype 范围for循环等测试