文 / 李博(光宇广贞)
本篇实验平台信息请见《这篇文章》。
在《C++ 0x 新标准全部革新提案文档列表》中,N1478 N1527 N1607 N1705 N1978 N2115 N2343 等文件提案向新标准语言内核中添加 decltype 算符和 auto 关键字(旧体新义)。该提案由 BS 参与设计。decltype 算符用于查询表达式类型;auto 关键字修饰变量声明,指示编译器根据变量的初始化表达式推导变量应有的类型。
Auto
关于 auto 关键字的用法定义很明确,也没什么花样。首先是定义变量,这里 auto 其需求、其存在价值、其意义、其用法,和 C# 下的 var 关键字一模儿一样。和 var 一样,auto 声明的变量必须“在声明处完成初始化”,编译器才可根据初始化表达式推导变量的类型。无论 auto 还是 var,都算是强类型与易用性之间的妥协罢。
不过 C++ 下的 auto 比 C# 下的 var 要麻烦一些。因为 C++ 把“指针、引用、值”在语义上分得太清楚了。所以在使用上,会带来一些困惑,或者用当下流行的词儿,叫“纠结”……比如如下用法:
在此,var1 和 var2 都会被推导为 int* 型。不过“引用”就另说了,比如如下用法:
在此,refInt 是 int& 型,而 var3 是 int 型,var4 是 int& 型。我对此的解释是,尽管编译器对“引用”与“指针”的处理方式相同,但是语义上二者相差甚远。“指针”是表示地址的“值”,而“引用”是“值”的别号。不管 refInt 是否引用,处在等号右边,待之以右值,得到的便是其实体(entity type)——若是值,则是值本身;若是引用,则为引用所引之值。所以,auto 只会推导出等号右边的“右值”的实体类型,若需声明为引用,则须显式声明为 auto&。而对 var1 和 var2 来说,都会正确推导出“指针值”类型,var 2 的 auto 后面的 * 就被编译器忽略了。
噢,对了,还有一个 const,比如如下用法:
在此,intConst 是 const int 型。var5 为 int 型,var6 为 const int& 型,var7 为 const int 型。很奇怪呵。同理,编译器将只会推导出“右值”的实体类型,也就是 int,那么 var5 和 var7 都好解释了。而 var6 声明为引用,引用的是等号右边的“右值”,既是引用,编译器就会确认引用的是 const int 类型,因故 var6 为 const int& 型。显然 var6 做为引用与要引用的“右值”有关,而 var5 和 var7 和“右值”没有关系。
注意一:auto 不能做为模板参数。因为这违背了 auto 需要由初始化表达式来推导类型的原则。尽管 BS 一开始设计的时候认为这种用法应该被允许,但是 VS 2010 给以了拒绝。话又说回来了,如下所示,与其写成第一个句子,为何不直接写成第二个的样子呢?
显然,第二个句子才能体现 auto 的意义和存在价值。
注意二:auto 不能做为函数的参数类型和返回类型。同样是因为违背了 auto 推导类型的原则。函数在编译时要实例化,此时便需要确定参数的类型,以方便安排内存。声明为 auto 的话如何确定其类型呢?没法确定,所以这样用是不允许的。再说了,如果参数表可以 auto 的话,那倒是比重载函数还要强大了……返回类型更不可以 auto,函数返回值是“右值”,“右值”要提供类型说明的,不可以 auto。
除了在变量声明处用以声明变量之外,auto 还可以引导一种函数定义式,名为 forwarding function。由于网络翻译混乱,本文对此干脆提出一个自定义的比较“给劲”的叫法——“前导函数”。这个放到 decltype 的介绍中说。
好像还有件事儿没有交待,auto 关键字原来就有,也用来声明变量,但原先是用来声明“可变局部变量”的,和 static const 关键字相对。问题是,哪个非 static const 的变量不是 auto 的呢?我声明个整型局部变量用 int i 就可以了,几乎没有人写成“规范”的 auto int i 吧。于是,做为 C / C++ 语言中最废的关键字,auto 旧词在新标准中被赋予新义,获得了新生——当然旧生就死了,VS 2010 中再这么写 auto int i 的话会编译报错(注:但是 IDE 不做提示,不能不说 BETA1 版的 VS 2010 挺让人别扭的,同样存在问题的还有右值引用 &&)。
Decltype
decltype 就是 declared type 的缩写。先摆出 decltype 的语义三规则(N2343):
The type denoted by decltype ( e ) is defined as follows:
- If e is an id-expression or a class member access, decltype ( e ) is defined as the type of the entity named by e. If there is no such entity, or e names a set of overloaded functions, the program is ill-formed.
- If e is a function call or an invocation of an overloaded operator ( parentheses around e are ignored ), decltype ( e ) is defined as the return type of that funcion.
- Otherwise, where T is the type of e, if e is an lvalue, decltype ( e ) is defined as T&, otherwise decltype ( e ) is defined as T.
The operand of the decltype specifier is an unevaluated operand.
注意规则三开头儿的“Otherwise”这个词,读英语,看小词很重要……话说这个词也不算小词了呵。说得很明白!——非 #1 非 #2 者,一律由 #3 判断。#3 完全是根据表达式的左右值性质了。比如 decltype ( ( e ) ),前两条都不合适,走到第三条,由于 () 表达式为左值表达式,因故返回的是 T&。
插一小句,说说这个 () 表达式。为何说是左值呢?因为可以对表达式用 & 算符取址……只有左值才能取址。但这样一个式子:
& ( A + B ) // A 与 B 同类型,重载加法运算
非法,不可取址,因为加法重载函数返回的是右值,你括起来也是右值。这就是规则二里所说“括号被无视”的情况。( A + B ) 适用于规则二,而不是规则三。
再比如一个适用于 #3 的情况,就是类的 .* 或 –>* 算符,返回的将是引用 T&。不过这种访问类成员的方式和直接访问类成员的方式,结果有些不同——注意,后者适用于 #1,而前者适用于 #3。如下例:
在此,d1 到 d4 都好解释,全适用于 #1;d5 到 d8 不适用 #1 和 #2,只好适用于 #3。其中 d6 和 d8 对引用取引用,是非法操作。d7 的话,由于引用类型需要确定所引对像的可读写性,因此推导为 const int& 型。
在此把重点的挑出来聊了聊,更多关于讲解 decltype 适用规则的例子,请参见 N2115,Decltype ( version 6 )。
Decltype 和 SFINAE
又涉及到麻烦的模板类型推导带来的重载绑定(Overload Resolution)问题了。话说总提这个 SFINAE,全称是 Substitution Failure Is Not An Error。这是当今主流编译器进行模板类型推导的原则。那些拗口的理论文就不多聊了,本文开头第二段罗列的那堆“N”打头儿的论文有的是。至于编译器是如何处理“重载绑定”的,那是编译器供应商和非理性(疯狂)的程序员去关注的事儿(反正我关注了……)。这里引出一个 SFINAE 机制下的编程小技巧,见如下代码:
这段代码会编译失败,报错“找不到匹配的 Fck 函数实体”。问题出在 decltype ( t1 + t2, int() ) 这一句。decltype 算符会对括号内的逗号表达式进行类型推导验证。由于 T 类型,也就是 SbClass 类型没有“加号重载”的声明,原码中被注释掉了,因此逗号表达式推导失败,于是 Fck 模板函数实例化失败,于是 Fck 函数没有定义实体,结果报错。如果将注释掉的“加号重载”函数声明恢复,则推导成功,逗号表达式返回 int 型,因为 int() 是右值,适用于 #3,而 Fck 模板函数实例化成功,程序编译并运行成功,显示那句“ FckTemplate ”。
为何说是种“编程小技巧”呢?程序员可能需要提供一系列的 Fck 函数的重载定义式,如何让编译器选择出最合适的重载函数,是程序员要计算的事情。比如上片代码,只有实现了“加法重载”的类才匹配 Fck 模板函数,从而避免造成误用。
前导函数(Forwarding Function)
由 N1705,Decltype and Auto ( version 4 ) 提案陈述。对此新特性的讨论的话,还是先上代码吧:
如红圈所示的函数定义方式,抽象出来是这个样子的:
auto function-name ( parameter-list ) –> return-type
这种函数定义式的特色就是把“返回类型”的声明放到了“参数列表”的后面。之所以“非要”把二者的顺序调个个儿,一定有所需求。是何需求呢?
正如红圈圈示的地方,返回类型可能需要从参数表推导。这时把 decltype ( t1 + t2 ) 放到参数表前面就不合适了。因为在参数表出现之前,t1 和 t2 是未定义的。如开篇第二段所述,decltype 推导类型是根据表达式去“查询”,既然都查询了,那一定先有定义然后才能查询不是?所以返回类型必须晚于参数表出现,这就需要函数有先出现参数表后出现返回类型的声明方式。
或者会有人问,这个 Fck 函数直接返回 T& 类型不就完了?谁告诉你“加法重载”函数一定返回本类型的?怀疑的人可以自己去试试。
插一句,这里“加法重载”返回的是“右值引用”,关于“右值引用”的讨论请参见前文《测试 VS 2010 对 C++ 0x 标准的谨慎支持》。返回“右值引用”是符合“加法”的语义的——加法返回的应当是右值。
从如图所示的两个红色小波浪来看,VS 2010 的 IDE 对新特性的支持啊……真是没法儿说……尽管内核是实现了的,但是……从报错的情况来看,由于 Concept 被否了,编译器报错对底层的“发掘”是更带劲儿了……报的是乱七八糟……不能不说,尽管 C++0x 致力于让 C++ 语言变得更加易学易用,但至少从 VS 2010 的实现来看,在代码调试上,对程序员水平和素质的要求,反倒更高了……倒是一种讽刺。
最后说,与其走到今天这步,不得不让函数“倒过来”声明。为何 C++ 语言当初设计的时候不去采用先参数后返回的声明顺序呢?这算是继 GC、lambda 之后,又一次 IP 向 FP 的投降之举了吧。
参考:
《从 C++ 模板元编程生产质数看 F# 函数式编程思想》
《由 C++ 模板元编程看 F# 对链表的处理,兼谈 C 系语言和 FP 的优劣》
《测试 VS 2010 对 C++ 0x 标准的谨慎支持》
《看 VS 2010 对 C++ 0x 支持之 extern template》
《C++ 0x 即将夭折的新关键字 constexpr 为 VS 2010 拒绝》
《C++ 0x(C++ 09)新标准全部革新提案文档列表》
C++ 分类
F# 分类