第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)中使用它,比如 decltypesizeofnoexcept 等。

说白了,std::declval<T>() 就是假装我们有一个 T 类型的对象,然后让你看看对这个「假想对象」做某些操作会得到什么类型。

关键点: declval 不会真的构造对象,它只在编译期「模拟」一个对象的存在。

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>);

你看,这里根本没有创建任何 intdouble 对象。编译器只看表达式 std::declval<T>() + std::declval<T>() 的类型,然后推导出结果。

小技巧: 如果 T 是引用类型,declval 会返回对应的引用。比如 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 在整个表达式类型推导中的位置和作用:

std::declval 核心逻辑 std::declval<T>() 返回 T&&(右值引用) 只能在未求值上下文(unevaluated context)中使用 decltype 中推导 表达式类型 sizeof 中计算 表达式大小 noexcept 中检测 异常安全性 SFINAE 检测操作符 推导成员函数返回类型 泛型代码中的类型萃取

20.6 避坑指南

我总结几个实际开发中容易踩的坑:

  • 不要在运行时调用 declval:它没有实现体,链接时会报错。只能在编译期使用。
  • 注意引用折叠std::declval<T&>() 返回 T&std::declval<T&&>() 返回 T&&std::declval<T>() 返回 T&&
  • 配合 void_t 使用更安全:在 SFINAE 中,std::void_t<decltype(...)> 可以优雅地处理非法表达式。
  • 不要忘记头文件#include <utility>,否则编译器会报错。
个人习惯: 我一般在写类型萃取(type traits)时,会先写一个辅助的 declval 表达式,然后用 std::void_t 包装,最后用 std::is_detected 或自定义 trait 来检测。这样代码更清晰。

20.7 总结

std::declval 是泛型编程中一个不起眼但极其重要的工具。它让我们在编译期「凭空」获得一个对象,从而推导出表达式类型,而无需真的构造对象。

说白了,它就是编译期的「假设分析」工具。没有它,很多 SFINAE 技巧和类型萃取都无法实现。

下次你写泛型代码时,如果遇到「我需要知道这个表达式返回什么类型,但我没有对象」的情况,记得用 std::declval


公众号:蓝海资料掘金营,微信 deep3321