10-4 窄化转换、列表初始化与constexpr初始化器
在上节课(10.3——数值转换)中,我们探讨了数值转换,涵盖了基础类型之间多种多样的类型转换。
窄化转换
在 C++ 中,窄化转换是一种潜在不安全的数值转换,目标类型可能无法容纳源类型的全部值。
以下转换被定义为窄化转换:
- 从浮点类型转换为整数类型。
- 从浮点类型转换为更窄或更低阶的浮点类型,除非被转换值为constexpr且在目标类型范围内(即使目标类型精度不足以存储该数的全部有效位)。
- 从整数类型转换为浮点类型,除非被转换值为constexpr且其值可精确存储于目标类型中。
- 从整数类型转换为无法表示原始类型全部值的另一整数类型,除非被转换的值是 constexpr 且其值能精确存储在目标类型中。这涵盖了从宽到窄的整数转换,以及整数符号转换(有符号到无符号,或反之)。
在多数情况下,隐式窄化转换会引发编译器警告,但有符号/无符号转换除外(是否产生警告取决于编译器配置)。
应尽可能避免窄化转换,因其存在潜在安全隐患,可能引发错误。
最佳实践
鉴于其潜在风险与错误源,请尽可能避免窄化转换。
将有意窄化转换显式化
窄化转换并非总能避免——尤其在函数调用中,当函数形参与实参类型不匹配时往往需要进行窄化转换。
此时建议使用static_cast将隐式窄化转换显式化。此举既能明确标注转换意图,又能抑制编译器警告或错误。
例如:
void someFcn(int i)
{
}
int main()
{
double d{ 5.0 };
someFcn(d); // bad: implicit narrowing conversion will generate compiler warning
// good: we're explicitly telling the compiler this narrowing conversion is intentional
someFcn(static_cast<int>(d)); // no warning generated
return 0;
}

最佳实践
若需执行窄化转换,请使用static_cast将其显式化。
大括号初始化禁止窄化转换
使用大括号初始化时禁止窄化转换(这也是推荐此初始化形式的主要原因之一),尝试执行将导致编译错误。
例如:
int main()
{
int i { 3.5 }; // won't compile
return 0;
}
Visual Studio 报错如下:
error C2397: conversion from 'double' to 'int' requires a narrowing conversion
若需在花括号初始化中执行窄化转换,请使用 static_cast 将窄化转换显式化:
int main()
{
double d { 3.5 };
// static_cast<int> converts double to int, initializes i with int result
int i { static_cast<int>(d) };
return 0;
}
某些 constexpr 转换不被视为窄化转换
当窄化转换的源值在运行时才确定时,转换结果同样无法在运行前确定。此时,该窄化转换是否能保留原始值也需运行时才能验证。例如:
#include <iostream>
void print(unsigned int u) // note: unsigned
{
std::cout << u << '\n';
}
int main()
{
std::cout << "Enter an integral value: ";
int n{};
std::cin >> n; // enter 5 or -5
print(n); // conversion to unsigned may or may not preserve value
return 0;
}

在上述程序中,编译器无法预知n的具体值。当调用print(n)时,int到unsigned int的转换才实际发生,其结果是否保留原值取决于n的输入值。因此启用有符号/无符号警告的编译器会对此发出警告。
不过您可能注意到,多数缩窄转换定义都包含以“除非被转换值是constexpr且...”开头的例外条款。例如当转换满足“从整数的类型转换到无法表示原始类型全部值的另一整数的类型,除非被转换值是constexpr且其值能精确存储于目标类型”时,即属于缩窄转换。
当窄化转换的源值为 constexpr 时,编译器必须已知具体待转换的值。此类情况下,编译器可自行执行转换,随后检查值是否被保留。若值未被保留,编译器可因错误终止编译。若值得以保留,则该转换不被视为窄化转换(此时编译器可安全地用转换结果替换整个转换过程)。
例如:
#include <iostream>
int main()
{
constexpr int n1{ 5 }; // note: constexpr
unsigned int u1 { n1 }; // okay: conversion is not narrowing due to exclusion clause
constexpr int n2 { -5 }; // note: constexpr
unsigned int u2 { n2 }; // compile error: conversion is narrowing due to value change
return 0;
}

让我们将规则“从一个整数类型转换到另一个无法表示原始类型所有值的整数类型,除非被转换的值是constexpr且其值能精确存储在目标类型中”应用于上述两种转换。
对于 n1 和 u1 的情况,n1 是 int 类型而 u1 是 unsigned int 类型,因此这是从一种整数类型转换到另一种无法表示原始类型全部值的整数类型。然而,n1 是 constexpr,且其值 5 可精确存储在目标类型中(作为无符号值 5)。因此该转换不被视为窄化转换,允许使用 n1 对 u1 进行列表初始化。
对于 n2 和 u2 的情况,情形类似。尽管 n2 是 constexpr,但其值 -5 无法在目标类型中精确表示,因此这被视为窄化转换。由于我们正在进行列表初始化,编译器将报错并终止编译。
奇怪的是,浮点类型到整数类型的转换没有constexpr排除条款,因此这些转换始终被视为缩窄转换——即使待转换的值是constexpr且符合目标类型的范围:
int n { 5.0 }; // compile error: narrowing conversion

更奇怪的是,即使会导致精度损失,从常量表达式浮点类型转换为更窄浮点类型的操作也不被视为缩窄转换!
constexpr double d { 0.1 };
float f { d }; // not narrowing, even though loss of precision results

警告
即使导致精度损失,从 constexpr 浮点类型转换为更窄浮点类型也不视为窄化转换。
使用 constexpr 初始化器进行列表初始化
这些 constexpr 例外条款在初始化非整型/非双精度对象时极其有用,因为我们可以使用 int 或 double 字面量(或 constexpr 对象)作为初始化值。
这使我们能够避免:
- 在多数情况下使用字面量后缀
- 在初始化中添加冗余的static_cast
例如:
int main()
{
// We can avoid literals with suffixes
unsigned int u { 5 }; // okay (we don't need to use `5u`)
float f { 1.5 }; // okay (we don't need to use `1.5f`)
// We can avoid static_casts
constexpr int n{ 5 };
double d { n }; // okay (we don't need a static_cast here)
short s { 5 }; // okay (there is no suffix for short, we don't need a static_cast here)
return 0;
}
此方法同样适用于复制初始化和直接初始化。
值得注意的是:使用constexpr值初始化更窄或更低级别的浮点类型是允许的,只要该值在目标类型的取值范围内,即使目标类型精度不足以精确存储该值!
关键洞察
浮点类型按以下顺序排列(由大到小):
- Long double(长双精度)
- Double(双精度)
- Float
因此,类似以下代码是合法的且不会报错:
int main()
{
float f { 1.23456789 }; // not a narrowing conversion, even though precision lost!
return 0;
}
然而,在此情况下编译器仍可能发出警告(若使用 -Wconversion 编译标志,GCC 和 Clang 都会发出警告)。

浙公网安备 33010602011771号