第20章:std::declval与表达式类型——在未构造对象的情况下获取表达式类型
说实话,我第一次看到 std::declval 的时候,心里想的是:「这玩意儿到底有什么用?」
一个函数,你没法调用它,因为它没有实现。它返回一个右值引用,但你根本不能真的用它来构造对象。听起来像个玩具,对吧?
但后来我在写一个泛型序列化库的时候,遇到了一个棘手的问题:我想在编译期判断某个类型 T 是否支持 + 操作符。可问题是,我手头根本没有 T 的实例。怎么办?
嗯,std::declval 就是为这种场景而生的。
20.1 核心概念:declval 是什么?
std::declval 定义在 <utility> 中。它的声明长这样:
template<typename T>
typename std::add_rvalue_reference<T>::type declval() noexcept;
注意,它没有实现体。你只能在 未求值的上下文(unevaluated context)中使用它,比如 decltype、sizeof、noexcept 等。
说白了,std::declval<T>() 就是假装我们有一个 T 类型的对象,然后让你看看对这个「假想对象」做某些操作会得到什么类型。
20.2 典型用法:在 decltype 中推导表达式类型
我个人最常用的场景,就是在 decltype 里配合 std::declval 来推导某个表达式的结果类型。
举个例子,我想知道两个 T 类型的值相加会得到什么类型:
#include <utility>
#include <type_traits>
template<typename T>
using add_result_t = decltype(std::declval<T>() + std::declval<T>());
// 用法
static_assert(std::is_same_v<add_result_t<int>, int>);
static_assert(std::is_same_v<add_result_t<double>, double>);
你看,这里根本没有创建任何 int 或 double 对象。编译器只看表达式 std::declval<T>() + std::declval<T>() 的类型,然后推导出结果。
std::declval<int&>() 返回 int&,std::declval<int&&>() 返回 int&&。
20.3 实战:检测类型是否支持某个操作
我在项目中遇到过需要写一个泛型加法函数,但要求只对支持 + 操作符的类型启用。用 std::declval 配合 SFINAE 就能轻松搞定:
#include <type_traits>
#include <utility>
// 辅助 trait:检测 T 是否支持加法
template<typename T, typename = void>
struct has_plus : std::false_type {};
template<typename T>
struct has_plus<T, std::void_t<decltype(std::declval<T>() + std::declval<T>())>>
: std::true_type {};
// 启用条件:T 支持加法
template<typename T>
std::enable_if_t<has_plus<T>::value, T>
add(T a, T b) {
return a + b;
}
// 测试
struct NoAdd {};
int main() {
static_assert(has_plus<int>::value); // 通过
static_assert(!has_plus<NoAdd>::value); // 通过
// add(NoAdd{}, NoAdd{}); // 编译错误,因为 NoAdd 不支持加法
}
这里 std::void_t 的作用是:如果 decltype(...) 是合法类型,那么特化版本被选中,has_plus<T> 继承自 true_type。否则,回退到主模板的 false_type。
decltype 里直接写 T() + T()。如果 T 没有默认构造函数,这就会编译失败。而 std::declval<T>() 完全绕过了构造函数,所以更安全。
20.4 处理引用和 const 限定
declval 返回的是右值引用,但如果你需要左值引用呢?
嗯,这里有个小技巧:
// 模拟左值
using lvalue_ref = decltype((std::declval<T>())); // 注意双括号
// 或者更直接
using lvalue_ref = T&;
实际上,std::declval<T&>() 返回的就是 T&,因为引用折叠规则。所以如果你需要左值,直接传左值引用类型即可:
// 获取 T 类型的左值引用
auto&& fake_lvalue = std::declval<T&>(); // 类型是 T&
// 在 decltype 中使用
using result_t = decltype(std::declval<T&>() + std::declval<T&>());
对于 const 限定,也是类似的:
// 模拟 const 对象
using const_ref = decltype(std::declval<const T&>());
// 检测 const 对象上的成员函数
using member_type = decltype(std::declval<const T&>().some_method());
20.5 知识体系:declval 的核心逻辑
下面这张图总结了 std::declval 在整个表达式类型推导中的位置和作用:
20.6 避坑指南
我总结几个实际开发中容易踩的坑:
- 不要在运行时调用 declval:它没有实现体,链接时会报错。只能在编译期使用。
- 注意引用折叠:
std::declval<T&>()返回T&,std::declval<T&&>()返回T&&,std::declval<T>()返回T&&。 - 配合 void_t 使用更安全:在 SFINAE 中,
std::void_t<decltype(...)>可以优雅地处理非法表达式。 - 不要忘记头文件:
#include <utility>,否则编译器会报错。
declval 表达式,然后用 std::void_t 包装,最后用 std::is_detected 或自定义 trait 来检测。这样代码更清晰。
20.7 总结
std::declval 是泛型编程中一个不起眼但极其重要的工具。它让我们在编译期「凭空」获得一个对象,从而推导出表达式类型,而无需真的构造对象。
说白了,它就是编译期的「假设分析」工具。没有它,很多 SFINAE 技巧和类型萃取都无法实现。
下次你写泛型代码时,如果遇到「我需要知道这个表达式返回什么类型,但我没有对象」的情况,记得用 std::declval。
公众号:蓝海资料掘金营,微信 deep3321