decltype(auto) 将 decltype 的类型推导机制与 auto 语法的便利性结合在一起。虽然 auto 应用模板参数推导规则,将数组衰减为指针,并去除顶层 cv 资格和引用,decltype(auto) 保留初始化表达式的确切类型。具体而言,如果表达式是一个未加括号的变量名,decltype 将返回声明的类型;如果它是一个带括号的左值表达式,那么它将返回一个左值引用。这使得函数能够完美地转发其返回值,而无需明确指定 decltype 表达式或担心引用折叠的复杂性。
我们需要实现一个通用包装器,用于数据库访问器,条件性地返回缓存记录的引用或新构建的默认值。关键要求是保留确切的返回类型语义——引用必须保持引用,以避免复制大型对象,而值应根据需要移动或复制。
一个候选解决方案利用了一个显式的尾返回类型与 decltype 和 std::declval,指定 decltype(std::declval<Accessor>()(key))。优点:它明确记录了类型转换,并且在 C++11 中有效。缺点:语法冗长,需要将参数完美转发给 std::declval,在处理多个重载或条件逻辑时变得不可维护。
另一种方法使用普通的 auto 作为返回类型,假设编译器会推导出合适的类型。优点:简洁易读。缺点:auto 应用衰减规则,将 Record& 转换为 Record 并去除 const 资格,这导致不必要的深拷贝,并且在调用者期望只读引用时违反了 const 正确性。
我们选择了 decltype(auto) 作为返回类型,这将 decltype 的类型保留规则应用于返回的表达式。这个选择消除了样板代码,同时保证左值引用、const 资格和右值引用正确传播给调用者。结果是一个零开销的通用外观,处理值和引用返回,而无需代码重复或隐式转换,减少了高频缓存查找的延迟。
为什么 decltype((var)) 产生左值引用类型,而 decltype(var) 产生声明的类型,这如何影响 decltype(auto) 返回语句?
decltype 采用两条不同的规则:对于未加括号的 id 表达式(如 var),它生成该实体的声明类型;对于任何其他表达式,包括括号表达式如 (var),它产生该表达式的类型,如果该表达式是左值,则这是一个左值引用类型。当使用 decltype(auto) 时,返回 (var) 会创建对局部变量的引用,导致在函数退出时出现悬空引用。因此,在使用 decltype(auto) 时,必须避免返回语句中的不必要括号,因为额外的括号会将表达式类别从 id 表达式更改为左值表达式。
decltype(auto) 如何与 xvalue(即将过期的值)相互作用,与 prvalues 有何不同?
decltype(auto) 精确地保留值类别,遵循 decltype 语义。如果函数返回一个 xvalue(例如 std::move(obj)),那么 decltype(auto) 将推导类型为右值引用(T&&),而 auto 将推导为 T。这种区别在实现必须保留返回临时对象的移动语义的完美转发工厂函数时至关重要,而不强制进行拷贝或要求在调用站点上显式的 std::move 注释。
当与大括号初始化列表一起使用 decltype(auto) 时会发生什么,为什么与 auto 推导不同?
当使用大括号初始化列表如 {1, 2, 3} 初始化时,auto 推导为 std::initializer_list<int>,但 decltype(auto) 尝试将大括号初始化列表本身推导为一个类型,这是一个非推导上下文,导致代码格式不正确。这阻止了 decltype(auto) 直接返回大括号初始化列表,而 auto 可以推导出 std::initializer_list 临时对象。这个微妙的区别源于 decltype 完全保留表达式类型,包括在非推导上下文中表达式不是变量或函数调用的情况。