decltype 这玩意,到今天都像个老朋友……
它不是为了让你写得更“高级”,而是为了让类型别再靠猜。
你写模板写到凌晨,就会理解这句话的分量。
那会儿还叫 C++0x,标准还没定,可项目已经要交付了。
你说急不急?
于是大家嘴上讲“可移植”,手上却在各家编译器之间打补丁。
那时候大家写的都是 typeof。
GCC 有。
Clang 也有。
听上去挺美,对吧?
可 MSVC 没有,一到 Windows 就露馅。
你就会听见同事在工位上骂人,骂得还挺有文学性。
那种骂不是脾气差,是被类型系统逼到墙角……
语言不给我一个标准答案。 那我只能自己造一个。
Boost 当年就干过这事:Boost.Typeof。
一堆宏,一堆魔法,专门把 typeof 伪装成“可移植”。
你可以说它土,也可以说它太重。 但它很诚实,因为模板已经把类型玩疯了…… 而你最常写不出来的,偏偏就是函数的返回类型。
返回值写不出来这件事
我见过最典型的场景,就是写一个 add。
听起来像练习题,可在泛型库里它就是日常。
真正难的不是相加,是返回类型。
在 C++11 之前,你经常只能这么干: 先造一张类型表。 再祈祷它别过期。
template <class T, class U>
typename promote<T, U>::type add(T a, U b) {
return a + b;
}
promote 是谁?
你我都知道,就是“我们自己维护的一张类型表”。
后来标准库里有了 std::common_type,算是把这张表“收编”成官方版本。
但你别指望它能兜住所有工程细节,尤其是那些自定义运算符。
今天加 int + double,明天加自定义数值类型。
后天又有人重载了 operator+,你那张表就开始摇摇欲坠……
更要命的是,你真正想要的答案其实很直白:a + b 的类型。
可你偏偏没法在传统语法里把它写出来。
你只能把表达式写在心里:
a + b
你当然想把它直接写进返回类型里,可传统语法偏偏要你先写返回类型。
那时候参数 a、b 还没出场,名字都还没报上来。
你只能叹口气:这就是尾置返回类型登场的原因。
尾置返回类型:把语序改一下
C++11 的做法很像老程序员的脾气:不绕。 直接把语序改了,先把参数写完,再谈返回值。
template <class T, class U>
auto add(T a, U b) -> decltype(a + b) {
return a + b;
}
你看,auto 在这里不是“自动推导”,更像一句“占位”。
真正的答案写在箭头后面:decltype(a + b)。
这套箭头语法,你在 C++11 的 lambda 里也见过。
后来 C++14 让 auto 真正能推导函数返回值,这条路就更顺了!
但在 C++11 的语境里,箭头还是主力。
委员会那会儿挺实用主义:能复用就复用。
decltype:它拿到的到底是什么
decltype 只做一件事:问编译器“这个表达式是什么类型”。 它不帮你脑补,也不帮你纠错——你递什么,它就判什么。
你递上去什么“证据”,它就按证据判。
同一个 x,括号一加,口供就变了……
int x = 0;
decltype(x) a = x;
decltype((x)) b = x;
decltype(x) 是 int,因为 x 作为变量名出现,它按声明类型来。
decltype((x)) 是 int&,因为 (x) 是左值表达式,decltype 就给你引用。
括号不是装饰。 在 decltype 眼里,括号是口供。
再看一个更“工程味”的证据:指针解引用。
你以为你在拿出 int,其实你拿到的是一个能被修改的位置。
int* p = &x;
decltype(*p) r = x;
工程里最常见的用法:把返回值原样带走
容器的 operator[],是我见过最多次的 decltype 用武之地。
因为你总想写个小封装,把“下标访问的结果”原样带走。
它可能是 T&,也可能是代理对象,比如 vector<bool> 那种“历史名作”。
template <class C>
auto at(C& c, std::size_t i) -> decltype(c[i]) {
return c[i];
}
decltype(c[i]) 不是装饰,它是在告诉编译器:我不猜。
你 [] 给我什么,我就原样返回什么。
写成按值返回,代理就丢了。 写成引用返回,又可能引用到不该引用的东西。
最稳的做法,是让类型自己说话。
还有一种封装:把成员函数的返回类型借过来
你也写过这种工具函数,拿一个容器的 front()。
你当然可以手写返回类型,但一手写就容易写出一堆 value_type、reference。
template <class C>
auto front(C& c) -> decltype(c.front()) {
return c.front();
}
这行 decltype(c.front()) 看着像偷懒,其实很讲道理。
最懂返回类型的人,本来就应该是 front() 自己。
decltype 不会“执行”表达式
很多新人会担心:decltype(f()) 会不会把 f() 调一遍?
不会,它只是在问类型。
int f();
using R = decltype(f());
f() 不会运行,但类型会被算出来,这就是 decltype 的“探针味”:
你可以拿它在模板里试探某个表达式是否成立,而不用真去跑。
最容易踩的坑:名字都是左值
这坑我踩过,也见过别人踩,专挑深夜上线前来。 看起来特别像“写得很高级”,实际上很危险。
template <class T>
auto id(T&& t) -> decltype((t)) {
return t;
}
t 是个名字,有名字的东西在表达式世界里就是左值。
所以 (t) 是左值表达式,decltype((t)) 也就变成了引用。
你以为你写了一个“原样返回”,实际上你写了一个“到处借引用”。 如果你拿它去接一个临时对象,那引用会比对象活得更久——然后你就开始追一只幽灵。
想保守一点,就按值返回,把生命周期问题当场解决。 这不是“更快”,这是“更敢睡觉”。
template <class T>
auto safe_id(T&& t) -> typename std::decay<T>::type {
return std::forward<T>(t);
}
收个尾
decltype 和尾置返回类型,表面上是两套语法,骨子里是一件事。 让返回类型回到它该来的地方,从表达式里来,从真实的代码里来。
你用对了,它把你从“手抄类型”里解放出来。 你用错了,它也能把 bug 藏进类型系统。 而类型系统一旦帮你藏,往往就藏得特别深……