对C++使用f-string吧!- P3412讲解

本文是对提案P3412R1的讲解(由于作者是Victor,我大胆猜测能进C++29)。如果提案后续有新的改动,请以最新结果为准。

Introduction

在Python中,虽然存在着各种格式化方式:

a = 1
print("Hello, %d" % a)
print("Hello, {}".format(a))
print(f"Hello, {a}")
print(f"Hello, {a=}") # 输出Hello, a=1

但f-string是其中最简便、最直观的。在C++中,前两种基本对应于printfformat的格式化方式;P3412想要引入f-string提供的最后两种方式。例如:

int calculate(int);
// Before this proposal
std::string stringify(std::string_view prefix, int bits) {
    return std::format("{}:{}: got {} for {:#06x}", prefix, errno, 
                       calculate(bits), bits);
}
// After this proposal
std::string stringify(std::string_view prefix, int bits) {
    return f"{prefix}-{errno}: got {calculate(bits)} for {bits:#06x}";
}

Details

Implementations

事实上,f-string的支持是直接编译器参与的。提案提出了以下的替换流程:

f"Hello, {a}";                 // #1, transformed to...
__FORMAT__("Hello, {}", (a));  // #2, transformed to...
std::format("Hello, {}", (a)); // #3

其中#1 -> #2是在预处理阶段完成的,因此写在其中的宏也可以正常替换#2 -> #3则是函数调用,在<format>中定义了新的全局函数__FORMAT__

template<typename... Args>
std::string __FORMAT__(std::format_string<Args...> lit, Args&&... args) {
    return std::format(std::move(lit), std::forward<Args>(args)...);
}

之所以引入一个新的__FORMAT__,是因为允许用户自定义这个函数。例如,如果用户可以自定义如下的函数:

// 多输出一个my:
template<typename... Args>
auto __FORMAT__(std::format_string<Args...> lit, Args&&... args)
{
    return "my: " + std::format(std::move(lit), std::forward<Args>(args)...);
}

当然这样会和<format>中原有的__FORMAT__重定义,可以使用concept来玩个trick:

template<typename... Args>
auto __FORMAT__(std::format_string<Args...> lit, Args&&... args) requires(true) {...}

这个函数是有约束的(尽管约束了个寂寞),而标准库定义的是没有约束的,在overload resolution中前者有更高的优先级。

Optimizations

通常情况下用户可能愿意这么写:

std::print(f"Hello, {a}");

然而这实际上是有问题的,因为等价于:

std::print(std::format("Hello, {}", a));

而正确的调用方法是:

std::print("Hello, {}", a);

因此,提案还提出了x-literal,直接展开成上述参数形式而非替换为__FORMAT__

std::print(x"Hello, {a}");

当然你也可以使用std::print("{}", f"Hello, {a}"),在流里也只能使用std::cout << f"Hello, {a}";,不过由于理论上创建了std::string,可能会有额外开销。

Side-effects

这种替换流程也导致了几个结果:

  • 在不可进行函数调用的位置不能使用f-string,例如#include <...>[[deprecated(...)]]中的...。同时由于现在std::format不是constexpr的,像static_assert等必须要求编译期完成的context也用不了。
  • 由于发生在string literal拼接之前,因此可以和其配合使用,看起来就比较诡异。作者给了下面的例子:

// TODO

注意一下R"abc()abc"是raw string,应该比较容易理解。但是高亮比较难受,因为毕竟还没支持这语法。。

  • 关于与user-defined literals的配合:
f"value: {x}"_uc; 

这个东西就存在二义性,_uc到底是装饰value:{}还是最后format出来的结果。提案要求是后者,即展开为:

__FORMAT__("value: {}", (x))_uc;

Debugging feature

模仿Python的debugging feature:

x = 1
print(f"{x=}") # 输出x=1

提案也提出了对其的支持:

f"{x=}"; // transformed to...
__FORMAT__("x={}", x);

唯一的二义性问题就是&MyClass::operator==结尾上,然而pointer to member function并不能有意义地输出,所以这个东西直接当成error对待就可以了。

Discussions

  1. 再给几个提案里提到的看起来有点特殊的例子:
f"Weird, but OK: {1 < 2, 2 > 1}";
// Transformed to:
__FORMAT__("Weird, but OK: {}", (1 < 2, 2 > 1));

int values[] = {3, 7, 1, 19, 2 };
f"Reversed: {std::set<int, std::greater<>>(values, values + 5)}";
// Transformed to:
__FORMAT__("Reversed: {}", (std::set<int, std::greater<>>(values, values + 5));
           
f"The result is { get_result() :{width}.3}";
// Transformed to:(这个就是std::format的runtime width)
__FORMAT__("The result is {:{}.3}", (get_result()), (width));
  1. 为什么不用C++26的反射:处理不了宏,处理不了前缀(除非搞个std::f的函数,看起来就很诡异),预计会增加更多的编译时间;目前反射支持的code injection更可能基于token sequence而不是直接的string,如果强行用还要提一个string转换到token sequence的新功能。