导读:写 C++ 泛型的时候,你是不是经常被各种复杂类型折磨?明明逻辑很简单,却要绞尽脑汁手写长长的类型名;auto 很好用,但它只能用来定义变量,想要单纯拿表达式的类型就束手无策。C++11 带来的
decltype关键字,搭配auto的返回类型后置语法,一举解决了大量泛型编程痛点,今天我们就掰开揉碎聊透这两个利器。
Bilibili 同步视频
一、auto 不够用?decltype 登场📌
我们都知道 C++11 的auto,编译器在编译阶段根据初始化表达式自动推导变量类型。但auto有一个硬性约束:使用 auto 的变量必须初始化,它是为定义变量而生的。
设想一个场景:我不想创建新变量,仅仅希望获取某一段表达式对应的类型,该怎么办? 这就是decltype的使命。
decltype(exp),语法看着和sizeof(exp)十分相像,同样是编译期完成推导,不会真正执行 exp 表达式,不会产生运行时开销,仅仅提取表达式的类型信息。
cpp
#include <iostream>
int main()
{
int x = 0;
decltype(x) y = 1; // y → int
decltype(x + y) z = 0; // z → int
const int& i = x;
decltype(i) j = y; // j → const int&,保留引用、const限定符
const decltype(z)* p = &z; // *p 为 const int,p 为 const int*
decltype(z)* pi = &z; // pi → int*
decltype(pi)* pp = π // pp → int**
std::cout << "demo run okn";
return 0;
}
💡小知识点:C/C++ 里
*、&属于说明符,是绑定变量名,不是绑定类型标识符。const decltype(z)* p,decltype(z)得到int,const修饰的是*p,最终指针 p 的类型是const int *,不要误以为是(const int)*这种概念。
对比auto最核心差异: auto在部分场景会丢弃引用、const/volatile(cv)修饰符;而 **decltype会原汁原味保留表达式的 cv 属性与引用属性 **。这也是它在泛型里大放异彩的关键原因。
二、decltype 三条推导规则,别再踩括号大坑⚠️
decltype看着简单,但是暗藏不少坑,最出名的就是多加一对括号类型直接大变样。它的推导分为三条铁则:
-
如果 exp 是标识符、类成员访问表达式:
decltype(exp)就等于 exp 本身的类型。 -
如果 exp 是函数调用表达式:
decltype(exp)等于函数返回值类型。 -
其他所有表达式:表达式是左值 → 推导为该类型的左值引用;是右值,则直接取表达式本身类型。
场景 1:标识符 & 类成员访问
直接拿变量名字、访问类的成员,老老实实返回原有类型,cv、引用全部保留。
cpp
#include <iostream>
class Foo {
public:
static const int Number = 0;
int x;
};
int main()
{
int n = 0;
volatile const int &x = n;
decltype(n) a = n; // a → int
decltype(x) b = n; // b → const volatile int &
decltype(Foo::Number) c = 0; // c → const int
Foo foo;
decltype(foo.x) d = 0; // d → int,类成员访问,规则1生效
return 0;
}
场景 2:表达式是函数调用
decltype 直接照搬函数返回类型。这里有一个很容易忽略的冷知识:
⚠️性能 & 语法提醒:非类类型的纯右值,会被忽略 cv 限定符;只有类对象的纯右值可以保留 const/volatile。
cpp
#include <iostream>
class Foo{};
int& func_int_r(void); // 返回左值 int&
int&& func_int_rr(void); // 返回xvalue int&&
int func_int(void); // 返回纯右值 int
const int& func_cint_r(void); // 返回左值 const int&
const int&& func_cint_rr(void); // 返回xvalue const int&&
const int func_cint(void); // 返回纯右值(基础类型,const被丢弃)
const Foo func_cfoo(void); // 返回类纯右值,const保留
int main()
{
int x = 0;
decltype(func_int_r()) a1 = x; // int &
decltype(func_int_rr()) b1 = 0; // int &&
decltype(func_int()) c1 = 0; // int
decltype(func_cint_r()) a2 = x; // const int &
decltype(func_cint_rr()) b2 = 0; // const int &&
decltype(func_cint()) c2 = 0; // int!注意:const被吃掉
decltype(func_cfoo()) ff = Foo();// const Foo,类类型保留const
return 0;
}
编译上面代码,GCC 会给出警告:
type qualifiers ignored on function return type。给返回基础类型的函数加上 const,属于无效写法,不建议这么写。
场景 3:其余表达式,括号的魔法✨
这是最容易翻车的地方!同样是访问成员,decltype(foo.x) 和 decltype((foo.x)) 完全是两个东西!
外层多包裹一层括号,就不再属于标识符表达式,触发规则 3!
cpp
#include <iostream>
struct Foo { int x; };
int main()
{
const Foo foo = Foo();
decltype(foo.x) a = 0; // 规则1:int
decltype((foo.x)) b = a; // 规则3!foo.x是const int左值 → const int &
int n = 0, m = 0;
decltype(n + m) c = 0; // n+m是右值 → int
decltype(n += m) d = c; // +=返回左值 → int &
return 0;
}
仅仅多一对圆括号,类型从int变成const int&,无数 C++ 开发者在这里踩过坑。记住口诀:标识符别乱套括号,套括号就进入左值右值判断逻辑。
三、decltype 实战:拯救糟糕的泛型代码💻
decltype 最大舞台就是泛型编程。我们来看一个经典痛点:容器迭代器。
下面这份 C++98 风格代码,看着没问题,但是传入 const 容器直接编译爆炸!
cpp
#include <vector>
template <class ContainerT> class Foo
{
typename ContainerT::iterator it_; // bug!const容器需要const_iterator
public:
void func (ContainerT& container)
{
it_= container.begin();
}
};
int main ()
{
typedef const std::vector<int> container_t;
container_t arr;
Foo<container_t> foo;
foo.func(arr); // 编译报错!const vector没有普通iterator
return 0;
}
C++98 时代怎么解决?只能写模板特化,重新写一整套针对const ContainerT的版本,大量代码重复,维护起来十分折磨人。
有了 decltype,我们直接推导begin()返回什么迭代器,一版代码通吃普通容器和 const 容器:
cpp
#include <vector>
template <class ContainerT> class Foo
{
decltype(ContainerT().begin()) it_; // 自动推导是iterator还是const_iterator
public:
void func (ContainerT& container)
{
it_ = container.begin();
}
};
int main ()
{
typedef const std::vector<int> container_t;
container_t arr;
Foo<container_t> foo;
// 可以正常编译运行
foo.func(arr);
return 0;
}
除此之外,还可以利用 decltype 从已有变量萃取内部类型,不用手写冗长的模板参数:
cpp
#include <vector>
int main()
{
std::vector<int> v;
decltype(v)::value_type i = 0; // 提取vector<int>的value_type,即int
return 0;
}
标准库内部也大量使用 decltype:
cpp
typedef decltype(nullptr) nullptr_t;
typedef decltype(sizeof(0)) size_t;
从表达式直接衍生出新类型,代码表意更加直观。
四、auto + decltype:返回类型后置语法🚀
泛型里面还有另一个头疼的问题:函数返回值依赖函数参数运算结果。
比如写一个通用加法模板,T 和 U 是不同类型,相加之后返回值该写什么?
❌ 错误尝试:
cpp
// 编译报错!t和u写在参数列表,返回值位置t、u还没有被定义
template<typename T,typename U>
decltype(t+u) add(T t,U u)
{
return t+u;
}
C++ 返回类型写在函数最前面,此时参数变量还未声明,无法直接使用t+u。 早期 C++11 未正式定型时,开发者只能写出非常反人类的黑魔法:
cpp
template<typename T,typename U>
decltype((*(T*)0)+(*(U*)0)) add(T t,U u)
{
return t+u;
}
强行构造空指针模拟对象做类型推导,可读性极差,万一 T/U 没有默认构造还会衍生别的隐患。
于是 C++11 推出 ** 返回类型后置(trailing‑return‑type)** 语法。 auto占住返回值的前置位置,->后面再写 decltype 推导真正返回类型。
✅优雅的最终版本:
cpp
template <typename T,typename U>
auto add (T t,U u)-> decltype(t + u)
{
return t+ u;
}
此时参数 t、u 已经完成声明,decltype(t+u)可以正常使用,可读性拉满!
再举一个重载函数匹配的示例:
cpp
int& foo(int& i);
float foo(float& f);
template <typename T>
auto func(T& val) -> decltype(foo(val))
{
return foo(val);
}
编译器会根据传入 T 的类型,自动推导出 func 该返回 int & 还是 float。
五、小结📝
-
decltype(exp)编译期获取表达式类型,不会执行表达式;相比 auto,完整保留引用、const、volatile 属性。 -
熟记三条推导规则,警惕额外括号带来的类型突变,这是高频 bug 点。
-
在泛型编程中大显身手:自动推导迭代器、萃取容器内部类型,避免大量模板特化。
-
返回类型后置语法
auto fn(...) -> decltype(...),解决返回值依赖函数参数的泛型难题。

📌拓展小提示:C++14 之后又新增
auto作为函数返回值,不需要写后置 decltype;但 C++11 环境下,返回类型后置依然是必备技能。理解 decltype,也是后续学习右值引用、引用折叠、std::declval 的重要基础。
希望读完本篇,下次再看到 decltype 不再一头雾水,写泛型代码少踩坑~