CppCon 2020 学习:C++20: String Formatting Library An Overview and Use with Custom Types
·
目前 C++ 中有两种格式化文本的方式:
1. C 风格的 printf()
- 不推荐使用的原因:
- 类型安全性差:
printf不会检查传入参数的类型是否和格式符匹配,容易出错。 - 不可扩展:只支持固定数量和类型的数据格式化,比如整数、浮点数、字符串等,不能方便地扩展支持自定义类型。
- 类型安全性差:
- 优点:
- 代码易读,格式字符串和参数分开,直观清晰。
- 示例代码:
这行代码表示:打印出“x 的值是 11,y 的值是 22。”int x{11}; int y{22}; printf("x has value %d and y has value %d.\n", x, y); - 优点补充:
- 格式字符串和参数是分开的,这样有利于(本地化),只需要格式字符串,而不需要改代码。
- 本地化友好:
- 你可以只格式字符串,比如将
"x has value %d and y has value %d.\n"成格式字符串,程序代码不变,方便国际化。
- 你可以只格式字符串,比如将
为什么?
目前 C++ 中有两种格式化文本的方式:
- C 风格的
printf() - C++ 的 I/O 流(iostream)
推荐使用的方式是 C++ 的 I/O 流:
- 优点:
- 类型安全:编译器会检查类型匹配,避免类型错误。
- 可扩展:可以通过重载
operator<<来支持自定义类型的输出。
- 缺点:
- 可读性较差:
这段代码比起int x{11}; int y{22}; cout << "x has value " << x << " and y has value " << y << endl;printf("x has value %d and y has value %d.\n", x, y);要长,且字符串和参数是交织在一起的。
- 可读性较差:
- 本地化困难:
- 字符串和变量是混合在一起的,不像
printf的格式字符串那样可以单独提取,时比较麻烦。
总结:
虽然cout更安全、灵活,但代码看起来更杂乱,且不利于单独字符串;而printf虽然不安全,但格式字符串清晰,易于本地化。
这段内容的理解如下:
- 字符串和变量是混合在一起的,不像
C++20 新特性:std::format()(在 <format> 头文件中)
- 类型安全
std::format在编译时检查格式字符串和参数类型,避免类型错误。 - 支持自定义类型扩展
可以为自定义类型重载格式化接口,实现灵活格式化。 - 易于阅读
格式字符串清晰,变量用{}占位,参数位置对应。 - 格式字符串和参数分离
格式字符串与参数独立,便于维护和本地化。 - 便于本地化/
格式字符串可以单独提取、,不受代码影响。 - 格式字符串后面跟任意数量的参数
基本示例:
cout << format("Hello CppCon {}!", 2020); // 输出: Hello CppCon 2020!
这里,{} 是格式占位符,会被后面的参数 2020 替换。
总结:std::format 结合了 printf 的格式化灵活性和类型安全,同时解决了 cout 可读性差和本地化困难的问题,是 C++20 推荐的格式化方式。
如果你需要,我可以帮你写一个更完整的说明或示例代码。
#include <format>
#include <iostream>
int main(int argc, char *argv[]) {
std::cout << std::format("Hello CppCon {}!", 2020); // 输出: Hello CppCon 2020!
return 0;
}
cmake
set(CMAKE_CXX_STANDARD 20)
# 你的可执行文件
add_executable(test_halide main.cpp)
这段内容讲的是 C++20 std::format(或者类似格式化库)中格式字符串的占位符和用法,理解如下:
占位符(Placeholders)
- 格式字符串中包含占位符
{},用于表示后续参数的位置,比如:
这里cout << format("Read {} bytes from {}", n, "file.txt");{}会依次被n和"file.txt"替换。 - 占位符可以使用参数索引(也叫位置占位符),明确指定对应参数,例如:
cout << format("Read {0} bytes from {1}", n, "file.txt");{0}表示第一个参数n,{1}表示第二个参数"file.txt"。 - 这种带位置的占位符特别适合本地化(Localization),可以灵活调整参数顺序。例如:
句子中“file.txt”和数字cout << format("从{1}中读取{0}个字节。", n, "file.txt");n的顺序不同,通过{1}和{0}来对应正确参数。 - 如果格式字符串里需要输出花括号
{或},必须用双花括号转义:cout << format("Test escaping {{ and }}"); // 输出: Test escaping { and }
总结:
{}是占位符,按顺序替换参数{0},{1}可以指定参数位置,方便不同语言时调整参数顺序- 输出大括号用
{{和}}来转义
#include <format>
#include <iostream>
int main() {
int n = 42;
const char* filename = "file.txt";
// 1. 简单占位符,按顺序替换
std::cout << std::format("Read {} bytes from {}\n", n, filename);
// 输出: Read 42 bytes from file.txt
// 2. 使用参数索引指定顺序
std::cout << std::format("Read {0} bytes from {1}\n", n, filename);
// 输出: Read 42 bytes from file.txt
// 3. 本地化示例,参数顺序改变
std::cout << std::format("从{1}中读取{0}个字节。\n", n, filename);
// 输出: 从file.txt中读取42个字节。
// 4. 输出花括号,需使用双花括号转义
std::cout << std::format("测试转义 {{ 和 }} 符号\n");
// 输出: 测试转义 { 和 } 符号
return 0;
}
这部分内容主要讲解了 C++20 std::format 的格式说明符(Format Specifiers)、本地化支持、错误处理、性能表现,以及如何支持自定义类型的格式化。下面给你做详细的理解总结:
1. 格式说明符 (Format Specifiers)
格式说明符定义了如何格式化输出内容,格式为:
[[fill]align][sign][#][0][width][.precision][type]
- fill:填充字符,默认为空格。
- align:对齐方式,取值:
<左对齐(默认非数字类型)>右对齐(默认数字类型)^居中对齐
- sign:符号显示方式
-只显示负号(默认)+显示正负号- 空格 显示负号,正数显示空格
- #:开启替代格式,比如十六进制前加
0x。 - 0:用
0填充(只对数字有效,且忽略对齐)。 - width:字段宽度,可以是数字或动态参数
{}。 - .precision:小数位数或字符串长度,可以是数字或动态参数
{}。 - type:类型,常见的有:
d十进制整数b二进制(#时前缀为0b)o八进制x小写十六进制(#时前缀0x)X大写十六进制(#时前缀0X)e,E科学计数法f,F固定小数点g,G一般格式s字符串c字符p指针
示例
int i{42};
std::cout << std::format("|{:7}|", i) << "\n"; // 右对齐,宽度7
std::cout << std::format("|{:<7}|", i) << "\n"; // 左对齐,宽度7
std::cout << std::format("|{:_>7}|", i) << "\n"; // 用`_`填充,右对齐,宽度7
std::cout << std::format("|{:_^7}|", i) << "\n"; // 用`_`填充,居中,宽度7
2. 本地化 (Localization)
std::format 支持 std::locale,比如:
std::locale enUS{"en_US"};
std::cout << std::format(enUS, "{}", 1024) << "\n"; // 输出 1,024(带千分位逗号)
3. 错误处理 (Handling Errors)
格式错误会抛出 std::format_error 异常,比如:
try {
std::cout << std::format("{:.}", 5);
} catch (const std::format_error& e) {
std::cout << e.what(); // 输出 "missing precision specifier"
}
4. 性能 (Performance)
std::format性能优于传统的sprintf、ostringstream和to_string,且更灵活、安全。- 例如某平台测试结果(优化开启):
sprintf: 882,311 nsostringstream: 2,892,035 nsto_string: 1,167,422 nsstd::format: 675,636 nsformat_to: 499,376 ns (更快)
5. 支持自定义类型 (Supporting Custom Types)
- 通过特化
std::formatter<T>模板,可以为用户自定义类型实现格式化支持。 - 需要实现两个成员函数:
parse():解析格式说明符format():格式化对象
6. 自定义类型示例 — KeyValue 类
class KeyValue {
public:
KeyValue(std::string key, int value) : m_key{std::move(key)}, m_value{value} {}
const std::string& getKey() const { return m_key; }
int getValue() const { return m_value; }
private:
std::string m_key;
int m_value;
};
自定义格式化器:
template<>
class std::formatter<KeyValue> {
public:
enum class OutputType { KeyOnly, ValueOnly, KeyAndValue };
constexpr auto parse(format_parse_context& ctx) {
auto it = ctx.begin();
auto end = ctx.end();
if (it == end || *it == '}') {
m_outputType = OutputType::KeyAndValue;
return it;
}
switch (*it) {
case 'a': m_outputType = OutputType::KeyOnly; break;
case 'b': m_outputType = OutputType::ValueOnly; break;
case 'c': m_outputType = OutputType::KeyAndValue; break;
default: throw std::format_error("Invalid KeyValue format specifier.");
}
++it;
if (it != end && *it != '}') {
throw std::format_error("Invalid KeyValue format specifier.");
}
return it;
}
auto format(const KeyValue& kv, format_context& ctx) {
switch (m_outputType) {
case OutputType::KeyOnly:
return std::format_to(ctx.out(), "{}", kv.getKey());
case OutputType::ValueOnly:
return std::format_to(ctx.out(), "{}", kv.getValue());
case OutputType::KeyAndValue:
default:
return std::format_to(ctx.out(), "{} - {}", kv.getKey(), kv.getValue());
}
}
private:
OutputType m_outputType{OutputType::KeyAndValue};
};
使用示例:
KeyValue kv{"Key1", 11};
std::cout << std::format("{}", kv) << "\n"; // Key1 - 11
std::cout << std::format("{:a}", kv) << "\n"; // Key1
std::cout << std::format("{:b}", kv) << "\n"); // 11
std::cout << std::format("{:c}", kv) << "\n"; // Key1 - 11
try {
std::cout << std::format("{:cd}", kv) << "\n";
} catch (const std::format_error& e) {
std::cout << e.what() << "\n"; // Invalid KeyValue format specifier.
}
总结
std::format提供了丰富的格式说明符,支持动态宽度和精度,灵活控制输出样式。- 支持国际化本地化格式。
- 格式错误会抛异常,方便调试。
- 性能优越于传统格式化手段。
- 可以自定义类型格式化,增强代码可读性和复用性。
#include <format>
#include <iostream>
#include <string>
#include <locale>
#include <chrono>
#include <sstream>
#include <cstdio>
#include <stdexcept>
// 自定义 KeyValue 类
class KeyValue {
public:
KeyValue(std::string key, int value) : m_key{std::move(key)}, m_value{value} {}
const std::string& getKey() const { return m_key; }
int getValue() const { return m_value; }
private:
std::string m_key;
int m_value;
};
// 自定义 KeyValue 类型的格式化器
template <>
class std::formatter<KeyValue> {
public:
enum class OutputType { KeyOnly, ValueOnly, KeyAndValue };
// 解析格式说明符
constexpr auto parse(std::format_parse_context& ctx) {
auto it = ctx.begin();
auto end = ctx.end();
if (it == end || *it == '}') {
m_outputType = OutputType::KeyAndValue; // 默认格式,输出键和值
return it;
}
switch (*it) {
case 'a':
m_outputType = OutputType::KeyOnly; // 只输出键
break;
case 'b':
m_outputType = OutputType::ValueOnly; // 只输出值
break;
case 'c':
m_outputType = OutputType::KeyAndValue; // 输出键和值
break;
default:
// 为了 constexpr 兼容,解析阶段不抛异常,遇到非法格式设置错误标志
m_outputType = OutputType::KeyAndValue; // 默认输出键和值
m_error = true; // 标记格式符错误,后续格式化时处理
break;
}
++it;
if (it != end && *it != '}') {
m_error = true; // 标记格式符错误
}
return it;
}
// 格式化输出
auto format(const KeyValue& kv, std::format_context& ctx) const {
if (m_error) {
// 格式说明符非法时,输出错误提示
return std::format_to(ctx.out(), "[格式说明符错误]");
}
switch (m_outputType) {
case OutputType::KeyOnly:
return std::format_to(ctx.out(), "{}", kv.getKey());
case OutputType::ValueOnly:
return std::format_to(ctx.out(), "{}", kv.getValue());
case OutputType::KeyAndValue:
default:
return std::format_to(ctx.out(), "{} - {}", kv.getKey(), kv.getValue());
}
}
private:
OutputType m_outputType{OutputType::KeyAndValue};
bool m_error{false}; // 是否出现格式说明符错误的标志
};
// 性能测试函数,测试不同格式化方法的耗时
void benchmark_formatting(int iterations) {
int value = 123456;
// std::format 性能测试
auto format_time = std::chrono::high_resolution_clock::now();
std::string result;
for (int i = 0; i < iterations; ++i) {
result = std::format("{:d}", value);
}
auto format_end = std::chrono::high_resolution_clock::now();
auto format_duration =
std::chrono::duration<double, std::nano>(format_end - format_time).count() / iterations;
// sprintf 性能测试
auto sprintf_time = std::chrono::high_resolution_clock::now();
char buffer[32];
for (int i = 0; i < iterations; ++i) {
snprintf(buffer, sizeof(buffer), "%d", value);
result = buffer;
}
auto sprintf_end = std::chrono::high_resolution_clock::now();
auto sprintf_duration =
std::chrono::duration<double, std::nano>(sprintf_end - sprintf_time).count() / iterations;
// std::ostringstream 性能测试
auto ostringstream_time = std::chrono::high_resolution_clock::now();
for (int i = 0; i < iterations; ++i) {
std::ostringstream oss;
oss << value;
result = oss.str();
}
auto ostringstream_end = std::chrono::high_resolution_clock::now();
auto ostringstream_duration =
std::chrono::duration<double, std::nano>(ostringstream_end - ostringstream_time).count() /
iterations;
// std::to_string 性能测试
auto to_string_time = std::chrono::high_resolution_clock::now();
for (int i = 0; i < iterations; ++i) {
result = std::to_string(value);
}
auto to_string_end = std::chrono::high_resolution_clock::now();
auto to_string_duration =
std::chrono::duration<double, std::nano>(to_string_end - to_string_time).count() /
iterations;
// 输出各方法平均耗时(纳秒)
std::cout << std::format("性能测试(平均每次耗时,{}次迭代):\n", iterations);
std::cout << std::format(" std::format: {:>8.2f} ns\n", format_duration);
std::cout << std::format(" sprintf: {:>8.2f} ns\n", sprintf_duration);
std::cout << std::format(" ostringstream: {:>8.2f} ns\n", ostringstream_duration);
std::cout << std::format(" to_string: {:>8.2f} ns\n", to_string_duration);
}
int main() {
try {
// 1. 格式说明符示例
std::cout << std::format("\n=== 格式说明符示例 ===\n");
int i = 42;
std::cout << std::format("|{:7}|\n", i); // 右对齐,宽度7
std::cout << std::format("|{:<7}|\n", i); // 左对齐,宽度7
std::cout << std::format("|{:_>7}|\n", i); // 右对齐,宽度7,填充字符'_'
std::cout << std::format("|{:_^7}|\n", i); // 居中,宽度7,填充字符'_'
// 更多格式说明符示例
double pi = 3.14159;
std::cout << std::format("Pi(固定小数点,2位小数):{:.2f}\n", pi);
std::cout << std::format("Pi(科学计数法):{:e}\n", pi);
std::cout << std::format("十六进制格式:{:#x}\n", i);
std::cout << std::format("二进制格式:{:#b}\n", i);
std::cout << std::format("带符号数:{:+}\n", i);
std::cout << std::format("带符号负数:{:+}\n", -i);
std::cout << std::format("正数前空格:{: }\n", i);
// 2. 本地化示例
std::cout << std::format("\n=== 本地化示例 ===\n");
try {
std::locale enUS("en_US.UTF-8");
std::cout << std::format(enUS, "带千分位分隔符的数字:{}\n", 1234567);
} catch (const std::runtime_error& e) {
std::cout << std::format("本地化错误:{}\n", e.what());
}
// 3. 错误处理示例
std::cout << std::format("\n=== 错误处理示例 ===\n");
try {
// 用有效格式说明符替代了之前错误的 "{:.}"
std::cout << std::format("{:d}", 5) << "\n";
} catch (const std::format_error& e) {
std::cout << std::format("捕获异常:{}\n", e.what());
}
// 4. 自定义类型格式化示例
std::cout << std::format("\n=== 自定义类型格式化示例 ===\n");
KeyValue kv("Key1", 11);
std::cout << std::format("默认格式:{}\n", kv);
std::cout << std::format("只输出键:{:a}\n", kv);
std::cout << std::format("只输出值:{:b}\n", kv);
std::cout << std::format("键和值:{:c}\n", kv);
try {
// 使用非法格式说明符时,输出错误提示
std::cout << std::format("{:d}", kv) << "\n";
} catch (const std::format_error& e) {
std::cout << std::format("捕获异常:{}\n", e.what());
}
// 5. 性能基准测试
std::cout << std::format("\n=== 性能基准测试 ===\n");
benchmark_formatting(1000000); // 运行100万次迭代
} catch (const std::exception& e) {
std::cout << std::format("出现异常:{}\n", e.what());
return 1;
}
return 0;
}
std::locale 是 C++ 标准库中表示区域设置(locale)的一种类,用来定义程序中与地区、语言、文化相关的各种规则和习惯。它控制程序如何处理数字格式化、日期时间格式、货币符号、字符分类(如字母、数字、空白等)、字符串排序等。
简单来说,std::locale 的作用是让程序能按照特定国家或语言环境的习惯来显示和处理文本和数字。
主要用途举例:
- 数字格式化
比如不同地区千分位分隔符不同:- 美国习惯用逗号
1,234,567.89 - 德国习惯用点和逗号反过来
1.234.567,89
- 美国习惯用逗号
- 日期时间格式
不同地区日期格式不同:- 美国:
MM/DD/YYYY - 中国:
YYYY年MM月DD日
- 美国:
- 货币符号
- 美国用美元符号
$ - 英国用英镑符号
£
- 美国用美元符号
- 字符分类
判断一个字符是否是字母、数字、空白等,根据语言不同可能有所区别。
std::locale 的用法示例:
#include <iostream>
#include <locale>
#include <iomanip>
int main() {
long long money = 123456789; // 单位是最小货币单位,比如美分,德分等
const char* locales[] = {
"en_US.UTF-8", // 美国
"de_DE.UTF-8", // 德国
"fr_FR.UTF-8", // 法国
"ja_JP.UTF-8", // 日本
"zh_CN.UTF-8" // 中国
};
for (auto loc_name : locales) {
try {
std::locale loc(loc_name);
// 使用该 locale 格式化数字
std::cout.imbue(loc); // 将标准输出绑定到该 locale
std::cout << std::showbase << std::put_money(money) << std::endl; // 输出 $1234.56
} catch (const std::runtime_error& e) {
std::cout << loc_name << " 不支持: " << e.what() << "\n";
}
}
return 0;
}
这里通过传入一个 std::locale 实例给 std::format,告诉它按照美国的习惯输出数字,自动加上千分位分隔符。
总结
std::locale 是 C++ 标准库提供的一个封装,用于支持国际化和本地化,使程序能够根据不同地区的习惯正确格式化和处理文本、数字、日期等信息。
更多推荐


所有评论(0)