杂货边角(12):C++11动态性来源之类型推断 && auto\decltype\追踪返回类型

在程序员眼中,如果非要二元论的谈论编程语言,那么相信很多人会说出语言存在动态和静态语言之分,其中静态语言以C++、Java为代表,而动态语言则以近年来风头正盛的Python、Ruby领衔。其实语言的动静之分主要是在于对变量进行类型检查的时间点不同,对于静态语言而言,类型检查是发生在编译阶段,因为变量在.data区或者堆栈上的占用空间大小要提前确定,这样才能给予地址分配;而对于动态语言而言,变量的类型检查则是发生在运行阶段,通过所谓的类型推导技术来实现延迟检查的。以前大家都说C++太过于死板了,比不上Python等脚本语言来的便捷,所以做验证阶段很多人都是喜欢先用Python验证逻辑,然后才决定是否使用C++转化为工业级代码。现在C++11通过引入auto, 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);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值