跳到主要内容

初始化、类型推导与现代语法

本节目标

理解 C++23 的初始化选择、auto 和 decltype 推导,以及 CTAD 的适用边界。

初始化决定对象如何诞生,推导决定声明中省略的类型如何补全。二者都依赖上下文:先看初始化形式和表达式类别,再判断最终类型;不要把花括号或 auto 当成无条件安全的简写。

初始化形式地图

初始化器的形状会决定对象沿哪条规则建立,不能把圆括号、花括号和等号只看成书写偏好。常见形式可以先放到同一张地图中判断:

写法初始化形式主要判断点
T value;默认初始化目标是基础类型还是类类型,以及类是否有可用的默认构造语义
T value{};值初始化基础类型从零开始;类类型仍按自身的构造规则判断
T value(args);直接初始化int direct(42); 一样,把实参交给目标类型
T value = expression;复制初始化检查表达式到目标类型的转换是否可用
T value{args};列表初始化同时检查窄化限制,以及列表构造函数是否影响选择

这五种形式并非总能互换。声明时应先确认目标类型是否有构造函数、转换限制或列表构造函数,再选择最能表达意图的写法;尤其不能假定圆括号与花括号在重载选择和窄化检查上永远相同。

列表初始化与窄化检查

花括号会在列表初始化阶段检查窄化,让可能丢失信息的转换先成为诊断:

int count{7}; // 接受:整数值可以直接初始化 int
// int count{3.5}; // 拒绝:不能在列表初始化中悄悄丢弃小数部分

代表程序里的 int list{7}; 遵循同一条规则。若业务确实要求把浮点数转成整数,应先明确截断、舍入和范围处理的约定;仅用强制转换压掉窄化诊断,却不说明业务规则,并没有解决信息丢失问题。

花括号也不是“更严格的圆括号”:除了窄化检查,它还会改变构造函数和 initializer_list 的选择。

默认初始化与值初始化

有没有提供初始化器,会直接改变基础类型局部对象的起点。具有自动存储期的 int count; 经过默认初始化后没有可供读取的确定值;若把它当作零参与计算,程序就已经越过了可读边界。写成 int count{}; 则是明确的值初始化,使这个基础类型对象从零开始。

对类类型,默认初始化和值初始化还会受到用户声明构造函数的影响,不能从基础类型的零初始化结果类推;应回到该类型自身的构造语义判断。

auto 占位类型推导

auto 只替代声明中待推导的类型,引用关系仍要由声明形状表达:

const int source{3};
auto copy = source; // int
auto& alias = source; // const int&
const auto& view = source; // const int&

普通 auto 的规则接近按值模板参数推导,通常丢弃初始化器类型的顶层 const 和引用,因此 copy 是独立的 int 值。代表程序中的 auto autoValue = direct; 也是同一情形:autoValue 不是 direct 的别名,修改副本不会让原变量同步变化。

需要借用时,应像 auto&const auto& 这样把引用写进声明;auto&& 也会让引用关系参与推导,不能与普通的按值 auto 混用。

decltype 的两套规则

decltype 先区分未加括号的名称和一般表达式,这两种输入不能套用同一条规则:

int direct{42};
decltype(direct) value = direct; // int
decltype((direct)) alias = direct; // int&

对未加括号的名称,decltype 查询并保留该名称的声明类型。额外括号让 (direct) 按一般表达式处理;它是左值,所以 decltype((direct)) 得到 int&。对其他一般表达式,规则同样由值类别决定:左值得到 T&,xvalue 得到 T&&,纯右值得到 T

因此,不能把 decltype(name)decltype((name)) 当成同一种查询。比较 autodecltype 时,也不能只看变量名称,还要看表达式是否带括号以及它的值类别。

decltype(auto) 与返回类型

当声明需要沿用 decltype 的结果时,decltype(auto) 会把表达式的引用语义一并保留下来:

decltype(auto) ref = retainReference(value);

retainReference 返回 const int&,这里的 ref 也会保留为引用,前提是 value 在引用使用期间始终有效。函数不得返回局部对象的引用;decltype(auto) 不会修复这种错误,只会把悬垂关系隐藏在推导结果里。

下面的完整程序让被引用的对象继续存活,并同时报告本章使用的初始化与推导结果。

initialization_report.cpp
#include <array>
#include <concepts>
#include <iostream>

[[nodiscard]] const int& retainReference(const int& value) {
return value;
}

int main() {
int direct(42);
int list{7};
auto autoValue = direct;
static_assert(std::same_as<decltype(autoValue), int>);
decltype(auto) referenceValue = retainReference(autoValue);
static_assert(std::same_as<decltype(referenceValue), const int&>);
const auto values = std::array{1, 2, 3};

std::cout << "direct=" << direct << '\n';
std::cout << "list=" << list << '\n';
std::cout << "auto_rule=int\n";
std::cout << "decltype_rule=const-int-ref\n";
std::cout << "ctad_size=" << values.size() << '\n';
}

输出:

direct=42
list=7
auto_rule=int
decltype_rule=const-int-ref
ctad_size=3

这里的完整程序是正文展示和测试共用的唯一源码;短片段只用于说明概念。

模板推导规则预览

占位声明里的 auto 由初始化器补全;函数模板的参数则不仅看实参,还要把实参放进声明给出的参数模式中匹配:

template<class T> void inspect_value(T);
template<class T> void inspect(const T&);

第一个按值模式通常像普通 auto 一样丢弃顶层 const 和引用;第二个模式把 const 与引用关系留在形参中,T 由实参和 const T& 共同推导。反过来,若某个模板参数只出现在返回类型中,调用实参没有可用于匹配它的位置,不能期待编译器仅凭期望返回类型自动补全。

本章只建立 auto 占位符与参数模式的共同边界;重载决议、转发引用和约束组合留待模板专题展开。

类模板实参推导

CTAD 把类模板实参的推导放在构造表达式处。代表程序中的写法可以分两步阅读:

const auto values = std::array{1, 2, 3};

右侧的 std::array{1, 2, 3} 先依据可用的构造信息和推导指引,得到元素类型为 int、长度为三的 std::array<int, 3>;左侧的 const auto 再用这个结果声明常量对象。推导是否可用取决于类模板提供的构造函数和推导指引,不能据此假设所有类模板都能从任意构造表达式推导。

CTAD 只减少重复书写,不会取消元素类型必须一致、构造参数必须合法等要求。

initializer_list 的重载优先级

同样的两个整数放进花括号和圆括号,可能选择不同的构造路径:

std::vector<int> values{3, 5}; // 两个元素:3、5
std::vector<int> filled(3, 5); // 三个元素,每个值都是 5

花括号调用时,适用的 initializer_list 构造函数会优先参与选择,因此 {3, 5} 不能按圆括号的“长度三、填充值五”来读;列表初始化还会执行自己的窄化检查。阅读类接口时,应把两种括号当作不同的调用形状,而不是只比较实参个数。

当构造结果出乎意料时,先检查是否存在 initializer_list 重载以及是否触发窄化限制,而不是只调整元素类型。

常见陷阱

初始化形式和推导语法各自解决一部分问题,不能替代对转换、类型与生命周期的检查:

  • 花括号能阻止不允许的窄化,却不会自动给信息丢失定义业务规则;用强制转换压掉诊断前,仍要明确截断、舍入和范围边界。
  • const auto& view = makeValue(); 可以把临时对象的生命周期延长到这个本地引用的生命周期,但不能把该借用从函数返回。也不要返回指向局部对象、临时子对象或已经结束生命周期对象的引用。
  • decltype(auto) 会保留引用语义,也可能保留一个错误的引用;使用推导结果前,必须确认被引用对象活得足够久。
  • std::array{1, 2, 3} 能完成 CTAD,不代表所有类模板都能从任意构造表达式推导;仍要检查构造函数、推导指引、元素类型和实参合法性。

遇到推导结果时,先写出初始化器的类型和值类别,再确认对象所有权和生命周期;这比凭变量拼写猜测类型可靠。