C++ 은 언어에 텍스트 형식을 가져오기 위해 예전부터 여러 시도를 했었습니다.
첫 번째로는, C에서 상속된 printf 입니다.
std::printf("hello %s\n", "world!");
std::printf 는 간결하고, 빠르며, 완벽하게 작동합니다.
요즘은 디버거들과 IDE 들이 printf()같은 API를 통합하고 있을 정도로 보편적이고 성공적이였습니다.
현재 2022년에도 아직도 printf로 디버깅 (변수 값을 찍어본다거나..) 하는 분들 계시잖아요?
그러나 너무 간결한게 문제였습니다.
std::printf 는 built-in type 들 (int, const char* 등..) 에 대해서만 작동했고 포매팅 (%d, %c) 은 안전하지 않았으며 varargs 같은 가변 인자를 사용했었습니다.
몇 년 후 사람들은 타입 안정성과 사용자 정의 타입의 통합을 염두에 두고, 새로운 Input-Output API에 대한 작업을 시작했습니다. 이 작업이 현재 우리가 C++에서 자주 사용하는 표준 <iostream> 입니다.
std::string world{"world"};
std::cout << "hello" << world << std::endl;
확실히 타입 안정성과 확장성 측면에서는 개선되었습니다.
그러나 성능저하 잘못된 설계 너무 많은 'operator<<' 때문에 말이 많았습니다.
std::cout << "oh yeah, "
<< "this is "
<< definitely()
<< " not verbose "
<< *at_all
<< std::endl;
C++20 의 <format>은 새로운 텍스트 서식 API 입니다.
stream 형식의 문제점을 고치면서도 printf의 단순함을 챙겼습니다.
<format> 은 세 가지 간단한 원칙을 기반으로 하는 텍스트 서식 지정 라이브러리입니다.
- 색인화된 인수 및 타입 지정에 대한 지원을 포함하는 자리 표시자(placeholder) 기반 형식 구문
- 가변 인자 지원을 위한 가변 템플릿을 사용하는 타입 안전 형식
- 사용자 정의 포매터(User-defined formatter)를 통한 사용자 정의 타입 지원
주요 기능인 std::format은 주어진 인수의 타입을 지정하고 문자열을 반환합니다.
#include <format>
int main(){
const std::string message {std::format("{}, {}!", "hello", "world")};
}
자리 표시자(place holder)는 인덱싱되어 인자의 순서를 변경하거나 반복할 수도 있습니다.
다음의 호출은 모두 "hello, world!"를 반환합니다.
std::format("{1}, {0}!", "world", "hello");
std::format("he{0}{0}o, {1}!", "l", "world");
std::format 함수는 다음과 같이 정의되어 있습니다.
std::string_view와 std::wstring_view를 사용합니다.
fmr - 인자가 std::string_view 또는 std::wstring_view로 변환될 수 있는 경우에만 가능합니다.
타입 문자열은 다음과 같이 구성됩니다.
- 일반적인 문자들 ( '{' 와 '}' 는 제외 )은 복사됩니다.
- return되는 문자열에선 '{{' '}}' 같은 이스케이프 시퀀스가 '{' '}'로 대체됩니다.
- replacement fields
에서 "{}" 를 replacement fields 라고 합니다. 형식은 다음과 같습니다.("{}, {}!", "hello", "world")
1. { arg-id(optional) }
2. { arg-id(optional) : format_spec }
arg-id : 인덱스를 지정하여 args 값을 사용하게 됩니다. 생략하면 인자가 순서대로 사용됩니다.
만약 생략한 필드와 인덱싱을 추가한 필드가 둘 다 존재하다면 오류입니다.
format_spec : 해당 인자에 대한 std::formatter specification 의해 정의된 사양입니다.
(1). built-in type 과 표준 문자열 타입들은 표준 사양으로 해석됩니다.
(2). std::chrono 유형의 경우 크로노 사양으로 해석됩니다.
(3). 그 외엔 사용자 정의 formatter specification에 의해 결정됩니다.
std::format에 replacement fields 보다 더 많은 args를 제공하는 것은 오류가 아닙니다.
std::format() 이외에도 std::format_to()를 사용하여 결과 문자열을 std::string에 직접 할당하는 대신 iterator를 가져다 쓸 수 있습니다.
// file input output
std::ofstream file{"format.txt"};
std::format_to(std::ostream_iterator<char>{file}, "hello, {}!", "world");
//container
std::Vector<char> buffer{};
std::format_to(std::ostream_iterator<char>{buffer}, "hello, world");
지금까지의 예제에서는 문자열 인자만 포함되었지만 std::format은 사용할 수 있는 모든 종류의 형식을 지원합니다.
(위에서 설명한 format_spec 참조)
built-in type이나 chrono, 문자열은 사전 정의된 포매터가 있습니다.
#include <chrono>
#include <format>
const std::string str{ std::format("Just {} days left for the release, right?", std::chrono::days(42)) };
const std::string number{ std::format("Number is {}", 5) };
포매터는 값의 문자열 표현을 반환할 뿐만 아니라 Formatting specifiers 를 통해 출력을 정의할 수 있습니다.
1개 | format("{}", 1) | 1 |
2개 | format("{} {}", 1, 2) | 1 2 |
3개 | format("{} {} {}", 1, 2, 3) | 1 2 3 |
순서 지정 | format("{2} {1} {0}", 1, 2, 3) | 3 2 1 |
문자(c는 생략 가능) | format("{:c}", 'C') | C |
문자열(s는 생략 가능) | format("{:s}", "C++") | C++ |
bool | format("{}", true) | true |
bool | format("{}", false) | false |
정수(10진수,d생략 가능) | format("{:d}", 42) | 42 |
정수(8진수) | format("{:o}", 042) | 42 |
정수(16진수) | format("{:x}", 0xab) | ab |
정수(16진수) 대문자 | format("{:X}", 0xab) | AB |
정수(16진수) 0x 붙음 | format("{:#x}", 0xab) | 0xab |
정수(16진수) 0X 붙음 | format("{:#X}", 0xab) | 0XAB |
정수(2진수) | format("{:b}", 0b10101010) | 10101010 |
정수(2진수) 0b 붙음 | format("{:#b}", 0b10101010) | 0b10101010 |
정수(2진수) 0B 붙음 | format("{:#B}", 0b10101010) | 0B10101010 |
정수(정수에+) | format("{:+d}", 42) | +42 |
부동소수점수 | format("{:f}", 123.456789) | 123.456789 |
정도 지정 | format("{:6.3f}", 123.456789) | 123.457 |
지수 표기(e) | format("{:e}", 123.456789) | 1.234568e+02 |
지수 표기(E) | format("{:E}", 123.456789) | 1.234568E+02 |
최적의 표기를 자동 판정 (g는 생략 가능) | format("{:g}", 123.456789) | 123.457 |
왼쪽 정렬 | format("{:<12}", "left") | left |
오른쪽 정렬 | format("{:>12}", "right") | right |
중앙 정렬 | format("{:^12}", "center") | center |
묻힐 문자 지정 | format("{:!^12}", "hello") | !!!hello!!!! |
// 문자열을 3자리 수까지만 저장합니다.
const std::string pi = std::format("{:.3f}", 3.141592654);
만약 우리가 직접 정의한 타입이 (User-defined type) 포매팅에 사용되려면 두 가지 방법이 있습니다.
- (펑소처럼) std::ostream에 operator<< 재정의 해서 사용하기
- UDT에 대한 사용자 정의 포매터 (User-defined formatter) 작성
(펑소처럼) std::ostream에 operator<< 재정의 해서 사용하기
아주 간단합니다.
<format>은 출력 스트림과 호환되는 모든 유형이 포맷 라이브러리와 호환되도록 Stream 라이브러리와 상호 운용됩니다.
#include <ostream>
#include <format>
enum class State{
On,
Off
};
std::ostream& operator<<(std::ostream& os, const State state){
switch(state){
case State::On:
return os << "On";
case State::Off:
return os << "Off";
}
return os;
}
const std::string current_mode = std::format("current mode is {}", Mode::On);
이렇게 하면 format에 stream 오버헤드가 추가된다는 단점이 있지만 기존 유형들이 operator<<를 지원했다면 <format>으로 마이그레이션 하는 것이 엄청 쉬워집니다.
UDT에 대한 사용자 정의 포매터 (User-defined formatter) 작성
사용자 정의 포매터를 작성하려면 유형에 맞는 std::formatter 를 특수화 해야 합니다.
template<>
struct std::formatter<State>{
std::format_parse_context::iterator parse(std::format_parse_context& context){
// ...
}
std::format_parse_contexT::iterator format(
const State state,
std::format_context& context){
// ...
}
};
특수화에는 두 개의 멤버 함수가 포함되어야 합니다.
- parse(context) : replacement field (placeholder)가 있는 경우 타입을 구문 분석하는 역할을 합니다.
즉, 타입 문자열의 "{}" 내부에 있는 내용을 구문 분석하는 함수입니다. 만약 replacement field가 발견되면 std::formatter 객체에 저장해야 합니다. ( this 함수 context 에서 ) - format(value, context) : parse()에서 찾은 타입 사양을 적용하여 formatting context에 제공(내보내야) 합니다.
std::format_to()에서 제공하는 반복자를 사용하여 간단히 추가할 수도 있습니다.
#include <format>
struct point {
double x, y;
};
template <> struct std::formatter<point>{
// f - 부동 수숫점형, e - 지수
char presentation { 'f' };
// 'f' 또는 'e'를 분석합니다.
constexpr auto parse(format_parse_context& ctx) -> decltype(ctx.begin()){
// [ctx.begin(), ctx.end())는 형식 문자열의 일부를 포함하는 문자 범위입니다.
// 예를 들어 std::format("{:f} - point of interest", point{1, 2}); 라면
// 범위에는 "f} - point of interest" 가 포함됩니다.
// 포매터는 '}' 또는 범위의 끝까지 지정자를 구문 분석해야 합니다.
// 이 예에서 포매터는 'f' 지정자를 구문 분석하고 '}'를 가리키는 반복자를 반환해야 합니다.
auto it{ ctx.begin() };
auto end { ctx.end() };
if(it != end && (*it == 'f' || *it == 'e')) presentation = *it++;
if(it != end && *it != '}') throw std::format_error("invalid format");
return it;
}
// 구분 분석 특수화를 사용하여 p의 타입을 지정합니다.
// 이 포매터에 저장됩니다.
template <typename FormatContext>
auto format(const point& p, FormatContext& ctx) -> decltype(ctx.out()) {
// ctx.out() 는 쓰기(write)에 사용된 output iterator 입니다.
return presentation == 'f'
? format_to(ctx.out(), "({:.1f}, {:.1f})", p.x, p.y)
: format_to(ctx.out(), "({:.1e}, {:.1e})", p.x, p.y);
}
};
이것을 std::format에 넘기게 되면 다음과 같은 결과가 나옵니다.
point p {1, 2};
std::string s { std::format("{:f}", p) };
// s == "(1.0, 2.0)"
상속 또는 구성(composition)을 통해 기존 포매터를 재사용할 수도 있습니다. 예를 들어
enum class color {red, green, blue};
template <> struct std::formatter<color>: formatter<string_view> {
// formatter<string_view>를 상속받아 분석합니다.
template <typename FormatContext>
auto format(color c, FormatContext& ctx) {
string_view name = "unknown";
switch (c) {
case color::red: name = "red"; break;
case color::green: name = "green"; break;
case color::blue: name = "blue"; break;
}
return formatter<string_view>::format(name, ctx);
}
};
parse는 상속됐기 때문에 formatter<string_view>로 인해 모든 문자열 타입 사양을 인식합니다.
std::format("{:>10}", color::blue)
//" blue"
클래스 계층에 대한 포매터를 작성할 수도 있습니다.
#include <type_traits>
#include <fmt/format.h>
struct A {
virtual ~A() {}
virtual std::string name() const { return "A"; }
};
struct B : A {
virtual std::string name() const { return "B"; }
};
template <typename T>
struct std::formatter<T, std::enable_if_t<std::is_base_of<A, T>::value, char>> :
std::formatter<std::string> {
template <typename FormatCtx>
auto format(const A& a, FormatCtx& ctx) {
return std::formatter<std::string>::format(a.name(), ctx);
}
};
int main() {
B b;
A& a = b;
std::format("{}", a); // "B"
}
formatter가 분석 가능한 타입에 대한 특수화 및 암시적 변환을 모두 제공할 경우, 특수화가 우선시 됩니다.
'C++ > Modern' 카테고리의 다른 글
C++20) 우주선 연산자를 이용한 3방향 비교 (three-way comparison, spaceship operator) (2) | 2021.07.25 |
---|---|
C++20) Atomic Smart Pointers / std::atomic_flag (2) | 2021.07.23 |
C++20) Ranges - 2 (0) | 2021.07.19 |
C++20) Ranges - 1 (0) | 2021.06.15 |
C++20) Modules ( 모듈 ) - 2 (5) | 2021.03.14 |