C++基础补充(03)C++20 的 std::format 函数

1. 使用C++20 std::format

需要将VisualStudio默认的标准修改为C++20
菜单“项目”-“项目属性”,打开如下对话框
在这里插入图片描述
代码中加入头文件

2. 基本用法

通过占位符{}制定格式化的位置,后面传入变量

#include<iostream>
#include<format>
#include<string>
using namespace std;

int main()
{
	int x = 68;
	double pi = 3.14159;
	string name = "Alice";

	//格式化字符串
	string rst = format("Hello,{}! The answer is {} and pi is {:.2f}.", name, x, pi);

	cout << rst << endl;

	return 0;
}

输出
Hello,Alice! The answer is 68 and pi is 3.14.

使用 {} 作为占位符,参数顺序插入
可以指定格式。{:.2f} 表示保留2位小数

3. 格式说明

详细可查阅手册
整数格式化

#include<iostream>
#include<format>
#include<string>
using namespace std;

int main()
{
	int x = 68;
	cout << format("Decimal:{}\n", x);//十进制输出
	cout << format("Hex:{:#x}\n", x);//十六进制输出
	cout << format("Binary:{:#b}\n", x);//二进制输出
	return 0;
}

浮点数格式化

#include<iostream>
#include<format>
#include<string>
using namespace std;

int main()
{
	double val = 3.14159;
	cout << format("Default: {}\n", val);  // 默认 3.14159
	cout << format("Fixed:{:.2f}\n", val); // 固定2位小数 3.14
	cout << format("Scientific:{:.2e}\n", val);//科学计数法 3.14e+00
	return 0;
}

字符串格式化

#include <iostream>
#include <format>
using namespace std;
int main() 
{
    string str = "Hello";
    cout << format("Default: {}\n", str);    // 默认输出: Hello
    cout << format("Padded: {:>10}\n", str); // 右对齐,宽度 10:      Hello
    cout << format("Left Padded: {:<10}\n", str);  // 左对齐,宽度 10: Hello     
    return 0;
}

输出
在这里插入图片描述
{:#x}:表示带有前缀的十六进制输出,前缀为 0x。
{:#b}:表示带有前缀的二进制输出,前缀为 0b。
{:05}:表示使用 5 位宽度输出,并用零进行左侧填充。
{:.2f}:表示浮点数保留两位小数。
{:.2e}:表示浮点数使用科学计数法输出,保留两位小数。

<think>我们被要求用C++17实现C++20std::format功能。由于C++17标准库中没有std::format,我们需要自己实现一个简化版本。 主要思路: 1. 实现一个format函数,它接受一个格式字符串和一系列参数,然后返回格式化后的字符串。 2. 格式字符串中使用大括号{}作为占位符,可以包含格式说明符,例如{:d}、{:s}等(但为了简化,我们可以先支持基本类型,并忽略格式说明符,或者支持部分常用格式)。 3. 由于C++17没有变参模板的编译期字符串解析(如C++20的constexpr能力),我们可以在运行时解析格式字符串。 步骤: a) 我们将遍历格式字符串,当遇到'{'时,检查下一个字符是否为'}',或者中间有格式说明符(如{:d})。 b) 提取占位符中的格式说明(如果有),然后根据说明将对应的参数转换为字符串。 c) 如果没有格式说明,则按照默认方式转换(类似于std::to_string或使用流输出)。 我们将使用变参模板来接收任意数量的参数。 注意:由于是简化版,我们不会实现完整的格式规范(如对齐、填充、精度等),但可以支持基本类型(整数、浮点数、字符串)和简单的格式说明(如d, f, s等)。 实现方式: 方法1:使用递归展开参数包,每次处理一个占位符,同时更新参数包和格式字符串的解析位置。 方法2:使用整数序列和tuple,将参数包存入tuple,然后遍历格式字符串,遇到占位符就从tuple中取出一个参数并转换为字符串。 我们将使用方法2,因为它更直观。 具体步骤: 1. 编写一个格式化函数,它接受格式字符串和参数包。 2. 将参数包存入一个tuple。 3. 解析格式字符串:遍历每一个字符,当遇到'{'时,检查下一个字符: - 如果是'{',则转义,输出一个'{'。 - 否则,解析直到'}',中间的内容就是格式说明符(可能为空)。 4. 根据格式说明符,将tuple中的当前参数转换为字符串(使用一个索引,每遇到一个占位符,索引加一)。 5. 将转换后的字符串插入到结果中。 6. 其他字符直接添加到结果。 注意:需要处理转义情况,即连续两个'{'或'}'表示输出单个大括号。 转换单个参数为字符串的函数: - 我们可以根据格式说明符,使用std::ostringstream来格式化输出,因为这样可以控制整数、浮点数的格式。 由于C++17没有std::format,我们只能自己实现。 简化假设: - 我们只支持以下格式说明符: 'd':整数按十进制输出(默认也是整数) 'f':浮点数按固定小数输出 's':字符串 'x':十六进制整数(小写) 'X':十六进制整数(大写) - 不支持宽度、精度、对齐等(但可以扩展,这里为了简化先不实现)。 步骤: 1. 定义一个函数,将给定值按照格式说明符转换为字符串。 2. 在解析格式字符串时,提取格式说明符(即大括号内的内容,去掉冒号,例如"{:d}"中的"d")。 3. 根据格式说明符,调用转换函数。 代码结构: 首先,定义一个转换函数模板,它接受一个值和格式说明符字符串,返回字符串。 然后,主format函数使用一个索引来遍历参数元组。 由于元组的索引需要在编译时确定,我们在解析格式字符串时,需要运行时计数,但我们可以使用一个循环,每次遇到占位符就处理一个参数。 具体实现: 我们实现一个format_impl函数,它遍历格式字符串,同时维护一个当前参数索引(从0开始)。当遇到占位符时,就根据索引从元组中取出参数,然后根据格式说明符转换,并将结果追加到输出字符串。 注意:转义的处理:连续两个'{'输出一个'{',连续两个'}'输出一个'}'。 代码示例: 由于代码较长,我们逐步实现。 第一步:实现一个转换函数 ```cpp #include <iostream> #include <sstream> #include <string> #include <tuple> #include <vector> #include <cctype> #include <stdexcept> #include <iomanip> // 将单个值转换为字符串,根据格式说明符 template <typename T> std::string format_value(const T& value, const std::string& fmt_spec = "") { std::ostringstream oss; // 如果没有格式说明,则按照默认方式输出 if (fmt_spec.empty()) { oss << value; } else { // 根据格式说明符处理 if (fmt_spec == "d") { // 整数十进制 oss << static_cast<long long>(value); // 假设T是整数类型 } else if (fmt_spec == "f") { // 浮点数固定小数 oss << std::fixed << static_cast<double>(value); } else if (fmt_spec == "s") { // 字符串,对于非字符串类型,我们也可以尝试输出 oss << value; } else if (fmt_spec == "x") { // 十六进制小写 oss << std::hex << static_cast<long long>(value); } else if (fmt_spec == "X") { // 十六进制大写 oss << std::uppercase << std::hex << static_cast<long long>(value); } else { // 不支持的格式,按默认输出 oss << value; } } return oss.str(); } // 特化字符串类型,因为对于字符串,我们不需要转换 // 但上面的模板已经可以处理,不过如果格式说明符为's',我们也可以直接输出 // 所以不需要特化,但是注意:对于char*和std::string,我们同样处理 // 主format函数 template <typename... Args> std::string format(const std::string& fmt, const Args&... args) { // 将参数存入tuple std::tuple<Args...> tuple_args(args...); std::string result; size_t index = 0; // 当前参数索引 size_t pos = 0; // 当前在格式字符串中的位置 size_t len = fmt.size(); while (pos < len) { // 检查当前字符 if (fmt[pos] == '{') { // 检查下一个字符 if (pos + 1 < len) { if (fmt[pos+1] == '{') { // 转义,输出一个'{' result += '{'; pos += 2; continue; } else { // 开始解析占位符,直到找到'} size_t end = pos + 1; while (end < len && fmt[end] != '}') { ++end; } if (end >= len) { // 没有闭合,抛出异常 throw std::runtime_error("Unclosed brace in format string"); } // 提取大括号内部内容,注意可能是空,或者有格式说明符(如":d") // 占位符部分: [pos+1, end-1] std::string placeholder = fmt.substr(pos+1, end - pos - 1); // 跳过占位符 pos = end + 1; // 检查占位符是否包含冒号(格式说明符) std::string fmt_spec; // 如果placeholder为空,则使用默认格式 // 否则,如果包含冒号,则冒号后面是格式说明符 // 但为了简化,我们假设占位符要么是空,要么是冒号+格式说明符(如":d") // 注意:标准格式是{}或者{index:format},但我们这里没有index,所以只支持顺序占位 // 我们要求占位符要么是空,要么以冒号开头,然后跟格式说明符 if (!placeholder.empty()) { if (placeholder[0] == ':') { fmt_spec = placeholder.substr(1); } else { // 不支持指定索引,所以如果占位符不是以冒号开头,则报错 throw std::runtime_error("Only empty or colon with format specifier are supported"); } } // 检查参数索引是否有效 if (index >= sizeof...(Args)) { throw std::runtime_error("Too many placeholders in format string"); } // 根据索引从tuple中取出参数,并转换为字符串 // 使用一个辅助函数,通过index获取tuple中的元素 // 由于index是运行时变量,我们需要使用编译时整数序列,但这里我们只能通过递归或迭代展开参数包,但这里我们使用std::apply和编译时索引序列不太方便。 // 替代方案:我们可以写一个函数,根据索引从tuple中获取元素并调用format_value // 使用std::apply和lambda,但我们需要在lambda内根据索引获取元素?这需要编译时索引。 // 因此,我们改为使用一个辅助函数,递归地展开索引直到目标索引。 // 这里我们使用一个辅助函数,通过index获取tuple中的元素并转换为字符串 // 我们使用一个索引序列来遍历tuple,直到找到目标索引 // 但由于我们无法在运行时if中改变编译时状态,我们可以使用递归模板。 // 为了简化,我们可以使用一个运行时索引,然后使用一个静态索引序列?不,我们可以放弃使用递归,而是使用一个计数器,然后使用std::apply来遍历tuple,但这样效率不高。 // 另一种方法:使用std::apply和tuple,但需要将tuple转换为vector<string>,然后按索引取?但这样会先转换所有参数,而我们只需要按顺序取。 // 我们改为:在解析格式字符串之前,先将所有参数转换为字符串(按照顺序),然后我们只需要按照索引取即可。但这样会丢失格式说明符,因为格式说明符是在解析时确定的。 // 所以,我们改变策略:在解析格式字符串时,我们不知道当前占位符对应哪个参数(因为占位符可能指定索引,但我们这里只支持顺序,所以就是按顺序取),因此我们可以用一个计数器index,然后从tuple中取出第index个元素。 // 如何从tuple中按索引取元素?使用std::get<index>(tuple)需要编译时常量索引。但我们的index是运行时的。 // 因此,我们需要将index转换为编译时常量。我们可以使用一个编译时整数序列,然后通过递归模板实例化,直到达到index。但这样代码较复杂。 // 替代方案:使用std::apply和lambda,在lambda内部我们使用一个计数器,当计数器等于index时,我们取出该参数。但std::apply会遍历所有参数,我们可以在遍历到第index个时停止?不行,因为std::apply是顺序遍历的,但我们可以记录当前遍历的索引。 // 由于我们只需要取第index个参数,我们可以写一个函数,通过运行时索引从tuple中取出元素: // 使用递归模板,从0开始,直到达到目标索引。 // 我们写一个函数:get_from_tuple(tuple, index) // 递归终止:当index==0时,返回第一个元素 // 否则,递归调用剩余部分,index-1 // 但是,由于tuple的元素类型不同,返回类型无法统一。所以我们需要返回一个字符串,即在递归过程中就转换为字符串。 // 因此,我们写一个递归函数,它接受一个tuple和当前索引(运行时),以及一个格式说明符,然后返回转换后的字符串。 // 由于递归模板函数需要知道当前处理的索引(编译时),我们可以使用一个编译时索引序列,从0开始,然后比较编译时索引和运行时索引,相等时转换。 // 为了简化,我们放弃使用递归,而是使用std::apply将tuple转换为一个字符串数组(每个元素根据默认格式转换),然后在遇到占位符时,再根据格式说明符重新转换?这样效率低,且无法利用格式说明符。 // 因此,我们回到递归模板函数,但只处理当前索引对应的参数。 // 由于我们只需要取一个参数,我们可以这样: // 定义一个函数模板<size_t I, typename... TArgs>,如果I等于index,则返回转换后的字符串,否则递归调用<I+1, TArgs...> // 但是,递归模板需要编译时索引,而index是运行时的,所以我们无法直接比较。 // 因此,我们使用一个编译时索引序列,然后生成一个函数数组(每个函数处理tuple中对应位置的元素),然后根据index调用对应的函数。 // 由于我们只取一个参数,我们可以这样: // 创建一个函数数组,每个函数将tuple中对应位置的元素转换为字符串(使用给定的格式说明符),然后调用第index个函数。 // 但是,这需要预先知道参数包的大小,并且创建函数数组。 // 为了简化,我们使用一个std::vector<std::function<std::string(const std::string&)>>,在构造时,对tuple中的每个元素
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dotdotyy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值