printf 类型不安全,iostream 又冗又慢;C++20 的 std::format 把 {fmt} 收进标准库,Python 风格占位符 + 编译期类型检查。同一时代还有 constexpr 进阶、consteval、std::span——把格式化与计算都推向「更安全、更编译期」。

这一篇对应三个 demo:format_demo、constexpr_demo、span_demo。

这是「现代 C++ 实战」系列的第 9 篇。建议先读 第 08 篇:错误处理策略。

一、格式化三时代

方式 类型安全 语法 性能
printf ❌ 格式与参数不匹配 → UB C 风格 %d 快
iostream ✅ cout << x << y 冗长 较慢
std::format ✅ "{} + {} = {}" 简洁 接近 printf

C++23 再加 std::print / std::println——直接输出,不必先拼 string 再 cout。

编译器要求:GCC 13+ / Clang 17+ 支持 std::format;std::print 需 GCC 14+ / Clang 18+。demo 用 __has_include(<format>) 定义 HAS_STD_FORMAT,用 __has_include(<print>) && __cplusplus >= 202302L 定义 HAS_STD_PRINT,不支持时回退到 printf / 提示文字。

二、std::format 基础

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <format>
#include <string>

std::string s = std::format("Hello, {}!", "world");
std::cout << std::format("{0} + {0} = {1}\n", 21, 42);

// 对齐与填充
std::format("[{:>10}]", "hi"); // 右对齐
std::format("[{:*^10}]", "hi"); // 居中,* 填充

// 数值格式
std::format("{:x}", 255); // ff
std::format("{:.2f}", 3.14159); // 3.14
std::format("{:#b}", 5); // 0b101
占位符 含义
{} 自动格式化
{0}, {1} 按位置索引
{:d} / {:f} / {:s} 整数 / 浮点 / 字符串
{:>10} / {:<10} / {:^10} 右 / 左 / 居中
{:.2f} 精度

自定义类型:实现 formatter<T> 特化,即可用 {} 格式化你的 struct。demo 为 Point 实现了 parse() + format(),支持 {} / {:f} / {:e} 三种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
struct Point {
double x, y;
};

template <>
struct std::formatter<Point> {
char presentation = 'g'; // 默认格式

constexpr auto parse(std::format_parse_context& ctx) {
auto it = ctx.begin();
if (it != ctx.end() && (*it == 'f' || *it == 'e' || *it == 'g')) {
presentation = *it++;
}
if (it != ctx.end() && *it != '}') {
throw std::format_error("无效的 Point 格式说明符");
}
return it;
}

auto format(const Point& p, std::format_context& ctx) const {
if (presentation == 'f') {
return std::format_to(ctx.out(), "({:.2f}, {:.2f})", p.x, p.y);
} else if (presentation == 'e') {
return std::format_to(ctx.out(), "({:e}, {:e})", p.x, p.y);
}
return std::format_to(ctx.out(), "({}, {})", p.x, p.y);
}
};

std::cout << std::format("固定格式: {:f}\n", Point{3.14159, 2.71828}); // (3.14, 2.72)

三、std::print(C++23)

1
2
3
4
#include <print>

std::print("value = {}\n", 42);
std::println("done"); // 自动换行

等价于 std::format + 写入 stdout,代码更短;std::println(stderr, ...) 还能写到指定文件流。不支持时可用 {fmt} 库或继续 format + cout。

format_demo 的 CMake 是 set(CMAKE_CXX_STANDARD 23),默认构建时启动信息会显示 [std::print 支持: 是],第 4 段直接用 std::print / std::println 输出(macOS 本机与 Docker 中的 GCC 均已验证);编译器不支持 <print> 时才回退到提示文字和等效写法对比。

四、constexpr 演进

核心思想:参数为编译期常量时,函数在编译期求值——零运行时开销。

标准 constexpr 能力
C++11 函数体只能有一条 return
C++14 允许循环、局部变量、分支
C++17 if constexpr、lambda 可 constexpr
C++20 更多标准库算法/容器可 constexpr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// C++11 风格:只能有一条 return
constexpr int factorial_cpp11(int n) {
return n <= 1 ? 1 : n * factorial_cpp11(n - 1);
}

// C++14 风格:可以有循环、局部变量
constexpr int factorial_cpp14(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}

constexpr int fact5 = factorial_cpp11(5); // 编译期:120
static_assert(factorial_cpp14(5) == 120);

int x = std::rand() % 10;
int y = factorial_cpp14(x); // 运行期:x 非常量

用途:数组大小、模板非类型参数、static_assert 校验、查表预计算。demo 第 3 段还演示了 C++20 的 constexpr 容器操作:create_squares() 在编译期生成 std::array<int, 5>,find_max() / reverse_array() 同样在编译期完成,并用 static_assert 校验结果。

五、consteval 与 constinit(C++20)

consteval:必须在编译期求值

1
2
3
4
5
6
7
consteval int compile_time_only(int n) {
return n * n + 1;
}

constexpr int ct_result = compile_time_only(5); // OK:编译期求值
static_assert(compile_time_only(3) == 10);
// int result = compile_time_only(x); // 错误:x 不是编译期常量

与 constexpr 的区别:constexpr「可以」编译期求值;consteval「必须」编译期求值。

constinit:静态变量编译期初始化

1
2
3
constinit int global_counter = 100;       // 编译期初始化,之后仍可修改
constinit const int max_count = 1000; // 编译期初始化且不可修改
// constinit const 不等于 constexpr:max_count 不能用于 static_assert

解决静态初始化顺序问题——保证在动态初始化之前已有确定值(仍可能运行期赋值,但首次初始化在编译期完成)。

if consteval(C++23)

1
2
3
4
5
6
7
constexpr int smart_function(int n) {
if consteval {
return n * n * n; // 编译期求值:立方
} else {
return n * n; // 运行期求值:平方
}
}

同一函数内区分编译期 / 运行期实现(demo 故意让两条路径结果不同,便于观察)。demo 在 __cplusplus < 202302L 时改用 C++20 的 std::is_constant_evaluated() 实现同名函数作为回退;它的 CMake 统一 set(CMAKE_CXX_STANDARD 23),所以 macOS 的 Apple Clang 和 Docker 中的 GCC 都走 if consteval 分支,输出相同:

1
2
smart_function(3) [编译期] = 27
smart_function(3) [运行期] = 9

六、std::span:连续内存的非拥有视图

类似 string_view 之于 string,span 不拥有内存,只持有指针 + 长度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <span>
#include <vector>
#include <array>

void print_span(std::span<const int> data) {
std::cout << " [";
for (size_t i = 0; i < data.size(); ++i) {
if (i > 0) std::cout << ", ";
std::cout << data[i];
}
std::cout << "] (size=" << data.size() << ")\n";
}

int c_array[] = {1, 2, 3, 4, 5};
std::array<int, 4> std_array = {10, 20, 30, 40};
std::vector<int> vec = {100, 200, 300};

print_span(c_array); // 同一函数
print_span(std_array);
print_span(vec);
类型 大小
span<T> 动态长度,指针 + size
span<T, N> 固定长度 N,仅指针

替代裸指针 + length 参数,边界更清晰;与 Ranges 配合良好(第 10 篇)。demo 里对比了 average_new(std::span<const int>) 与传统「指针 + 长度」写法,用 double_values(std::span<int>) 演示可写视图,并演示 first(n) / last(n) / subspan(offset, count) 三个 O(1) 子视图操作。

注意:span 生命周期不能长于底层容器——不延长对象寿命。

七、三个 demo 怎么跑

1
2
3
cd ref/cpp_demo/basics/format_demo && ./build.sh --run
cd ref/cpp_demo/basics/constexpr_demo && ./build.sh --run
cd ref/cpp_demo/basics/span_demo && ./build.sh --run

format_demo 和 constexpr_demo 的 CMake 是 set(CMAKE_CXX_STANDARD 23),span_demo 仍是 set(CMAKE_CXX_STANDARD 20)。

八、小结

特性 要点
std::format 类型安全、 {} 占位符,替代 printf/iostream
std::print C++23 一行输出
constexpr 编译期计算,C++14 起可写循环
consteval 强制编译期
constinit 静态变量编译期首初始化
span 非拥有连续内存视图

现代 C++ 实战系列第 9 篇完。下一篇 Ranges 与函数式风格——filter | transform | take 管道。

系列导航

篇号 标题 状态
08 错误处理策略 ✅
09 C++20 格式化与编译期计算(本篇) ✅
10 Ranges 与函数式风格 ✅