Chapter 8: Formatting and Scanning (v0.1.1)
이 장은 fmt.h와 scan.h에 대한 상세 레퍼런스다. Chapter 3은 더 짧은 개요와 일상적인 예제를 제공한다. 이 장은 정확한 문법, 파라미터 형태, 반환값, 그리고 호출자가 흔히 실수하는 지점에 초점을 맞춘다.
목차
- Design model
- Formatter data model
- Formatter constructors and selectors
- Format string grammar
- Formatting APIs
5.1. Formatting a type of your own
- Console print helpers
- Scanner data model
- Scanner primitive APIs
- Scan argument model
- Structural scan grammar
- Scan formatting APIs
11.1. Scan error code guide and recovery
1. 설계 모델
타입을 두 번 쓰지 않는 이유
이 장의 두 절반은 모두 한 가지를 없애기 위해 존재한다. 프로그래머가 타입을 말하는데 컴파일러가 그것을 값과 대조해 볼 수 없는 자리다.
printf("%d", x)는 타입을 두 번 말한다 — 한 번은 %d로, 한 번은 x를 넘기면서 — 그리고 varargs가 두 번째를 지워버리므로 아무것도 둘을 비교할 수 없다. scanf("%d", &x)는 더 나쁘다. 포맷이 어떻게 파싱할지 그리고 포인터를 통해 무엇을 쓸지를 둘 다 결정하므로, 불일치는 헛소리를 출력하는 대신 메모리를 망가뜨린다. 둘은 반대 방향에서 온 같은 결함이고, 둘 다 조용히 컴파일된다.
여기서 플레이스홀더는 타입을 전혀 담지 않는다. {}는 위치를 표시할 뿐이고, 타입은 포매팅 쪽에서는 PROVEN_ARG(x), 스캐닝 쪽에서는 PROVEN_SCAN_ARG(&x)에서 오며, 둘 다 인자의 정적 타입에 대해 컴파일 시점에 _Generic으로 결정된다. : 뒤의 spec은 표현만 제어한다 — width, fill, 정렬, 정밀도, 진법 — 결코 해석을 제어하지 않는다. 문자열에 타입을 쓴 적이 없으므로 %d냐 double이냐를 틀릴 여지가 없다.
두 번째 설계 결정은 어느 쪽도 버퍼를 소유하지 않는다는 것이다. 포매팅은 여러분이 제공한 목적지에 덧붙이고 들어가지 않으면 거부한다. 스캐닝은 여러분이 제공한 뷰(view)에서 읽고 여러분이 읽을 수 있는 커서를 옮긴다. 할당자(allocator)를 건네지 않는 한 여기서는 아무것도 할당하지 않으며, 바로 그 점이 이 장 전체가 프리스탠딩(freestanding) 빌드에서 동작하게 만든다(§13).
포매팅 측면과 스캐닝 측면은 정반대의 문제를 해결한다.
- 포매팅은 타입이 있는 값을 받아 텍스트로 렌더링한다.
- 스캐닝은 텍스트를 받아 타입이 있는 값을 기록한다.
이 프로젝트는 양쪽 모두 의도적으로 작게 유지한다.
- 포매팅은 간결한 플레이스홀더 언어, positional 재사용, 단순한 정렬(alignment), width, 그리고 숫자 값에 대한 hex 렌더링을 지원한다.
- 스캐닝은 타입이 있는 목적지 포인터, 엄격한 플레이스홀더 개수 검사, 그리고 공백 축약(whitespace collapsing)을 포함한 리터럴 매칭을 지원한다.
- 어느 쪽도 완전한
printf나scanf복제를 목표로 하지 않는다.
실용적인 결과로, 이 API들은 대규모 범용 포맷 엔진보다 추론하기 쉬우면서도, 문법은 일반적인 시스템 코드 작업에 충분히 표현력이 있다.
2. 포매터 데이터 모델
proven_fmt_result_t
typedef struct {
proven_err_t err;
proven_size_t written;
proven_size_t required;
} proven_fmt_result_t;의미:
err: 연산의 상태 코드.written: 목적지에 실제로 기록된 바이트 수.required: 전체 포맷 출력에 필요한 총 바이트 수.
먼저 err를 사용하라. 나머지 필드는 잘림(truncating)이나 부분적으로 성공한 연산에서 가장 유용하다.
성공한 result는 다음과 같은 모습이다.
proven_result_u8str_t rs = proven_u8str_create(alloc, 64);
if (proven_is_ok(rs.err)) {
proven_u8str_t s = rs.value;
proven_fmt_result_t r = proven_u8str_append_fmt_trunc(
&s,
"hello {}",
PROVEN_ARG("world")
);
if (proven_is_ok(r.err)) {
proven_println("{}", PROVEN_ARG(proven_u8str_as_view(&s)));
}
proven_u8str_destroy(alloc, &s);
}잘림이 발생한 result도 얼마나 더 많은 공간이 필요했는지는 여전히 알려줄 수 있다. 여기서는 목적지를 일부러 너무 작게 만들었으므로 written이 required에 못 미치게 되며, 그럼에도 문자열은 여전히 유효하다.
proven_result_u8str_t rs = proven_u8str_create(alloc, 8); /* on purpose: too small */
if (proven_is_ok(rs.err)) {
proven_u8str_t s = rs.value;
proven_fmt_result_t r = proven_u8str_append_fmt_trunc(
&s,
"name={} score={}",
PROVEN_ARG("ada"),
PROVEN_ARG(42)
);
/* r.written is what fit; r.required is what the whole output needed. */
proven_println("wrote {} of {} bytes",
PROVEN_ARG(r.written), PROVEN_ARG(r.required));
proven_u8str_destroy(alloc, &s);
}proven_arg_type_t
typedef enum {
PROVEN_ARG_NONE,
PROVEN_ARG_I32,
PROVEN_ARG_U32,
PROVEN_ARG_I64,
PROVEN_ARG_U64,
#ifndef PROVEN_FMT_NO_FLOAT
PROVEN_ARG_F64,
#endif
PROVEN_ARG_CSTR,
PROVEN_ARG_STR_VIEW,
PROVEN_ARG_DATETIME,
PROVEN_ARG_PTR,
PROVEN_ARG_FN,
} proven_arg_type_t;포매터는 현재 다음 값 클래스들을 인식한다.
- signed 32비트 정수
- unsigned 32비트 정수
- signed 64비트 정수
- unsigned 64비트 정수
- 부동소수점 값 (
PROVEN_FMT_NO_FLOAT가 정의되지 않은 경우) - 신뢰할 수 있는 C 문자열
- 빌려 쓰는(borrowed) U8 문자열 뷰
- datetime
- 객체 포인터
- 함수 포인터
proven_arg_t
typedef struct {
proven_arg_type_t type;
union {
proven_i32 i32;
proven_u32 u32;
proven_i64 i64;
proven_u64 u64;
double f64;
const char *cstr;
proven_u8str_view_t str_view;
proven_datetime_t datetime;
const void *ptr;
void (*fn)(void);
} value;
} proven_arg_t;union 필드는 선택된 type과 일치해야 한다. union 필드를 잘못 채워 놓고 포매터가 알아서 추측해 주기를 바라며 proven_arg_t를 억지로 만들지 마라.
잘못된 예:
proven_arg_t arg = {0};
arg.type = PROVEN_ARG_I64;
arg.value.u64 = 123; /* wrong: type and union field do not match */올바른 예:
proven_arg_t arg = proven_arg_i64(123);
(void)arg; /* pass it to a formatting macro; PROVEN_ARG(arg) accepts it as-is */3. 포매터 생성자와 선택자
생성자 요약
| API | 파라미터 | 반환 | 의도 |
proven_arg_none(void) | 없음 | proven_arg_t | 내부 sentinel 값. |
proven_arg_i32(int v) | signed 정수 | proven_arg_t | 32비트 signed 정수로 렌더링. |
proven_arg_u32(unsigned int v) | unsigned 정수 | proven_arg_t | 32비트 unsigned 정수로 렌더링. |
proven_arg_i64(long long v) | 넓은 signed 정수 | proven_arg_t | 64비트 signed 정수로 렌더링. |
proven_arg_u64(unsigned long long v) | 넓은 unsigned 정수 | proven_arg_t | 64비트 unsigned 정수로 렌더링. |
proven_arg_f64(double v) | 부동소수점 값 | proven_arg_t | float 포매팅이 비활성화되지 않은 한, 부동소수점 텍스트로 렌더링. |
proven_arg_cstr(const char *v) | 신뢰할 수 있는 live C 문자열 | proven_arg_t | NUL로 종료되는 C 문자열을 렌더링. |
proven_arg_cstr_n(const char *v, proven_size_t max_len) | 경계가 있을 수 있는 C 문자열 | proven_arg_t | NUL을 찾되 max_len까지만 렌더링. |
proven_arg_str_view(proven_u8str_view_t v) | 빌려온 U8 뷰 | proven_arg_t | NUL 종료를 가정하지 않고 빌려온 뷰를 렌더링. |
proven_arg_datetime(proven_datetime_t v) | datetime 값 | proven_arg_t | 포매터의 datetime 규칙으로 datetime을 렌더링. |
proven_arg_ptr(const void *v) | 객체 포인터 | proven_arg_t | 포인터 값을 렌더링. |
proven_arg_fn(void (*v)(void)) | 함수 포인터 | proven_arg_t | 원시 함수 포인터 표현을 렌더링. |
proven_arg_ucstr(const unsigned char *v) | unsigned-char 문자열 | proven_arg_t | proven_arg_cstr에 대한 편의 래퍼. |
proven_arg_identity(proven_arg_t v) | 기존 argument 객체 | proven_arg_t | 통과(pass-through) 헬퍼. |
proven_arg_bool(bool v) | boolean | proven_arg_t | 1 / 0이 아니라 true / false를 단어로 렌더링. |
proven_arg_char(char v) | 문자 하나 | proven_arg_t | 문자를 렌더링. 이것이 바로 char 변수는 문자로 렌더링되는데 리터럴 PROVEN_ARG('Z')는 여전히 90으로 렌더링되는 이유다. C에서 'Z'는 int 타입이며, 어떤 _Generic으로도 이것을 숫자 90과 구별할 수 없다. |
proven_arg_custom(const void *v, proven_fmt_custom_fn fn) | 어떤 타입이든 | proven_arg_t | 라이브러리가 전혀 알지 못하는 타입을, 당신이 제공하는 함수를 통해 렌더링. Formatting a user-defined type 참조. |
PROVEN_ARG(x)
PROVEN_ARG(x)는 통상적인 진입점이다. _Generic을 사용하여 컴파일러가 x의 타입으로부터 생성자를 고른다.
현재 매핑은 다음과 같다.
_Bool,char,signed char,short,int->proven_arg_i32unsigned char,unsigned short,unsigned int->proven_arg_u32long,long long->proven_arg_i64unsigned long,unsigned long long->proven_arg_u64double,float->proven_arg_f64(PROVEN_FMT_NO_FLOAT가 정의되지 않은 경우)const char *,char *->proven_arg_cstrunsigned char *,const unsigned char *->proven_arg_ucstrvoid *,const void *->proven_arg_ptrproven_u8str_view_t->proven_arg_str_viewproven_datetime_t->proven_arg_datetimeproven_arg_t->proven_arg_identity
중요한 결과:
PROVEN_ARG는 함수 포인터를 선택하지 않는다.- 함수 포인터에는
PROVEN_ARG_FN(f)를 사용하라.
잘못된 예:
void helper(void) {}
proven_u8str_append_fmt_grow(alloc, &s, "{}", PROVEN_ARG(helper)); /* wrong */올바른 예:
void helper(void); /* whatever function you want to print the address of */
proven_result_u8str_t rs = proven_u8str_create(alloc, 64);
if (proven_is_ok(rs.err)) {
proven_fmt_result_t r = proven_u8str_append_fmt_grow(alloc, &rs.value, "{}",
PROVEN_ARG_FN(helper));
(void)r;
proven_u8str_destroy(alloc, &rs.value);
}PROVEN_ARG_FN(f)
이 매크로는 호출자가 함수 포인터를 void *로 캐스팅하지 않고도 넘길 수 있도록 존재한다. proven_arg_fn을 감싼 작은 안전 래퍼다.
예:
void helper(void);
proven_result_u8str_t rs = proven_u8str_create(alloc, 64);
if (proven_is_ok(rs.err)) {
proven_u8str_t s = rs.value;
proven_fmt_result_t r = proven_u8str_append_fmt_grow(
alloc,
&s,
"callback = {}",
PROVEN_ARG_FN(helper)
);
if (!PROVEN_FMT_IS_OK(r)) {
proven_eprintln("formatting the callback failed");
}
proven_u8str_destroy(alloc, &s);
}PROVEN_ARG_CSTR_N(v, max_len)
이 매크로는 경계가 있는 문자열 헬퍼다. 소스가 완전히 신뢰할 수 있는 C 문자열이 아닐 수 있지만 여전히 C 문자열과 유사한 입력 처리를 원할 때 사용하라.
이 매크로는 max_len까지만 NUL을 탐색한 뒤 경계 지어진 접두부를 뷰로서 포매팅한다.
좋은 사용 사례 - buf가 통신선(wire)에서 들어왔으므로 NUL 종료가 전혀 보장되지 않는 경우:
char buf[128]; /* filled from an untrusted source */
(void)proven_mem_copy(buf, sizeof buf, proven_mem_view_from_u8(PROVEN_LIT("payload")));
proven_result_u8str_t rs = proven_u8str_create(alloc, 64);
if (proven_is_ok(rs.err)) {
proven_fmt_result_t r = proven_u8str_append_fmt_grow(
alloc,
&rs.value,
"payload={}",
PROVEN_ARG_CSTR_N(buf, sizeof buf) /* looks for NUL only within 128 bytes */
);
(void)r;
proven_u8str_destroy(alloc, &rs.value);
}나쁜 사용 사례:
const char *buf = get_network_buffer();
proven_u8str_append_fmt_grow(alloc, &s, "{}", PROVEN_ARG(buf)); /* wrong if buf is not trusted */부동소수점 형식화 참고
PROVEN_FMT_NO_FLOAT가 정의되면 float 지원은 제네릭 셀렉터에서 제거되고 float 생성자는 사용할 수 없다. 이는 런타임 토글이 아니라 컴파일 시점의 구성 선택이다.
double의 기본 {} 렌더링은 유한 값에 대해 소수점 이하 6자리 고정 형식을 만들며, 크기가 간결한 형식에 비해 너무 크거나 너무 작으면 과학적 표기(scientific)로 전환된다. 이전 버전과 달리, 이 출력은 이제 정확하고 정수만 사용하는 엔진으로 계산된다(double/long double 근사 없음). 자릿수는 정확히 반올림되므로(round-half-to-even), {}는 같은 값에 대해 printf("%.6f") / %.6e가 출력하는 것과 일치한다. 최단(shortest) 라운드트립 형식이나 6이 아닌 다른 precision이 필요하면 아래에 설명하는 proven_float_format_* 정책 API를 사용하라.
정확도와 한계
- 기본
{}float 출력은 소수점 이하 6자리를 사용하며, 가장 가까운 값으로 정확히 반올림하되 동점(tie)은 짝수로 처리한다(glibc%.6f/%.6e와 일치). 어떤 크기에서도 정확하며, 구성 가능한 큰 정수(big-integer) 용량을 넘어서는 precision/크기 한계는 없다. - 라운드트립 직렬화에는 최단(shortest) 정책 (
proven_float_format_options_shortest())을 사용하라. 이는 다시 파싱하면 정확히 같은 값이 되는 가장 짧은 십진 문자열을 방출한다. - 십진수-double 스캐닝은 IEEE-754 binary64로, 가장 가까운 값으로 반올림하되 동점은 짝수로 처리하도록 정확히 반올림되며, 호스트의
strtod와 비트 단위로 일치한다. 가장 작은 subnormal에 대한 중간 임계값 아래의 값들은 입력 부호를 보존한 채 부호 있는 0으로 반올림된다. - 파서는
Clinger fast path -> Eisel-Lemire -> 정확한 big-integer 폴백으로 계층화되어 있다. 모든 계층이 정확하고 폴백이 최종 판정자이므로, 결과는 모든 입력에 대해 정확히 반올림된다. 캐시된 거듭제곱 테이블은 생성된 소스다 (scripts/generate_float_decimal_tables.py). - 정확 폴백 big-integer 용량은 임베디드 타겟을 위해
PROVEN_FLOAT_BIGINT_LIMBS(include/proven/float_config.h참조)로 조정할 수 있다. fast path는 big integer를 절대 건드리지 않는다. - 검증: 포매터와 파서는 40.8억 개의 모든 유한
binary32값과 25.6억 개의 무작위binary64값에 대해 호스트 C 라이브러리와 남김없이 대조 검사되었으며, 불일치가 하나도 없었다. 알고리즘, 방법론, glibc와의 벤치마크는docs/float-correctness-and-performance.md를 참조하라.
엔진 내부(개념적으로)
API를 사용하는 데 이 내용이 전혀 필요하지 않다. 이는 출력이 왜 신뢰할 수 있는지 알고 싶은 독자를 위한 것이다. 전체 내용은 docs/float-correctness-and-performance.md에 있다.
파싱(십진수 → binary64), 세 계층, 빠른 것 우선. 결과는 항상 정확히 반올림된다. 계층은 순전히 속도의 계단일 뿐이며, 각 계층은 정확한 답을 보장할 수 있을 때만 선택된다.
- Clinger fast path. 값의 유효 자릿수가 적고 지수가 작을 때, 유효숫자(significand)와
10^exp모두double로 정확히 표현 가능하므로, 단 한 번의 반올림된 곱셈/나눗셈이 증명 가능하게 올바르다. 일상적인 대부분의 숫자를 커버한다. - Eisel-Lemire. 캐시된 10의 거듭제곱과의 64×128비트 고정소수점 곱셈이며, 결과가 반올림 경계로부터 충분히 멀어 확실한지를 검사한다. 검사가 불확실하면(값이 중간의 동점 지점에 놓이면) 다음 계층으로 넘어간다.
- 정확한 big-integer 폴백. 값을 큰 정수들의 비(
significand와5^q/2^q)로 구성하고, 후보double및 그 이웃과 정확히 비교한다. 이것이 동점과 subnormal을 올바르게 만드는 최종 판정자다. 시드된 ±16-ULP 윈도우가 탐색을 몇 번의 비교로 유지한다. big-integer 용량은PROVEN_FLOAT_BIGINT_LIMBS로 제한된다. 이 계층만이 limb를 (스택에) 할당하며, fast path는 여기에 절대 도달하지 않는다.
포매팅(binary64/32 → 십진수). 두 엔진, 그 어디에도 long double 없음:
- 최단(shortest) (
proven_float_format_options_shortest()): Grisu3 fast path가 거의 모든 입력에 대해 최소한의 라운드트립 자릿수를 ~90 ns에 만든다. Grisu3가 최소성을 증명할 수 없는 드문 경우에는 정확한 Dragon4(Burger–Dybvig, round-to-even) 코어로 폴백한다. 결과는 같은 비트로 다시 파싱되는 유일한 최단 십진수다. - 고정
%f/ 과학적%e(기본{}와 고정 옵션): 정확한 정수 엔진이 값을 big-integermul_pow5/시프트로10^precision만큼 스케일링하고, 정수divmod를 수행한 뒤, round-half-to-even으로 반올림한다. 따라서 어떤 precision과 크기에서도 glibc와 일치하며,2^64/precision 한계가 없다. 극단적인 지수는 실제 임의정밀도(arbitrary-precision) 연산을 하며 그만큼 느리다(실무에서는 드물다).
공개 부동소수점 파싱 API
세 진입점이 하나의 정확히 반올림되는 백엔드를 공유한다.
proven_scan_f64(scan)—proven_scan_t커서로부터 파싱하며, 실패 시 커서를 복원한다. 네이티브하고 길이로 경계 지어진 경로(NUL 종료 불필요).proven_parse_double_ascii(view)—proven_u8str_view_t로부터 로케일 독립적인 ASCII 토큰 하나를 파싱하고 소비한 길이를 보고한다.proven_strtod(nptr, endptr)— C 문자열 위의strtod스타일 편의 래퍼로, 선행 ASCII 공백을 건너뛰고endptr을 보고한다.
실전 예제: 파싱
#include "proven/scan.h"
#include "proven/float_parse.h"
/* (1) Native, view-based parsing through a scanner cursor. */
proven_scan_t sc = proven_scan_init(proven_u8str_view_from_cstr("3.14159e2 rest"));
proven_result_f64_t r = proven_scan_f64(&sc);
if (r.err == PROVEN_OK) {
/* r.val == 314.159; the cursor now sits at " rest". */
proven_println("parsed {}", PROVEN_ARG(r.val));
}
/* (2) strtod-style wrapper for C strings. endptr reports where parsing stopped. */
char *end = NULL;
double v = proven_strtod(" -0.5\t", &end); /* v == -0.5, *end == '\t' */
(void)v;
/* A trailing exponent marker with no digits stops like strtod: "1e" parses 1,
leaving endptr at 'e'. Inputs with hundreds of significant digits and extreme
exponents are still rounded correctly via the exact fallback. */실전 예제: 정책 API로 형식화하기
proven_float_format_f64_policy / _f32_policy는 호출자 버퍼에 직접 기록하고 기록한 바이트 수를 보고한다. 이들은 절대 할당하지 않는다.
#include "proven/float_format.h"
char buf[64];
proven_size_t n = 0;
/* Shortest round-trippable form: 0.1 -> "0.1" (not "0.10000000000000001"). */
(void)proven_float_format_f64_policy(buf, sizeof buf, 0.1,
PROVEN_FLOAT_FORMAT_POLICY_RYU,
proven_float_format_options_shortest(), &n);
/* buf == "0.1", n == 3 */
/* Fixed precision (correctly rounded, round-half-to-even). */
proven_float_format_options_t opt = proven_float_format_options_fixed_default();
opt.precision = 2;
(void)proven_float_format_f64_policy(buf, sizeof buf, 3.14159,
PROVEN_FLOAT_FORMAT_POLICY_DEFAULT, opt, &n);
/* buf == "3.14" */
/* Always scientific - this is what {:e} selects. Six fractional digits by default,
a signed two-digit-minimum exponent: exactly what printf's %e prints. */
proven_float_format_options_t sci = proven_float_format_options_scientific();
sci.precision = 2;
(void)proven_float_format_f64_policy(buf, sizeof buf, 42.0,
PROVEN_FLOAT_FORMAT_POLICY_DEFAULT, sci, &n);
/* buf == "4.20e+01" - where fixed would give "42.00" and shortest "42" */PROVEN_FLOAT_FORMAT_POLICY_RYU는 최단 출력을 선택한다.DEFAULT/SIMPLE은 정확한 고정 precision 경로(%f, 아주 크거나 작은 크기에서는%e로 전환)를 선택한다.- 버퍼가 너무 작으면
PROVEN_ERR_OUT_OF_BOUNDS를 반환하며(값은 절대 소리 없이 잘리지 않는다), 지원되지 않는 정책에는PROVEN_ERR_INVALID_ARG를 반환한다. - 제네릭
{}포매터(proven_u8str_append_fmt*)는 내부적으로DEFAULT정책을 사용하므로, 일상적인 로깅에서는 이 API를 직접 호출할 필요가 없다.
4. 형식 문자열 문법
포매터는 의도적으로 작은 문법을 받아들인다.
치환 필드
지원되는 형식:
{}: 다음 positional argument{0}: 첫 번째 사용자 argument{1}: 두 번째 사용자 argument{2}: 세 번째 사용자 argument- 이하 계속
번호 매김은 사용자 대상이며 0부터 시작한다. 구현은 인덱스 0에 숨겨진 sentinel을 저장하고, 사용자 인덱스 0을 내부 argument 슬롯 1로 매핑한다.
중괄호 이스케이프
{{는 리터럴{가 된다}}는 리터럴}가 된다
레이아웃 명세
{:[[fill]align][sign][#][0][width][.precision][type]}모든 부분은 선택적이며, 순서는 나머지 세계가 사용하는 것과 같다. 따라서 Python이나 Rust에서 복사한 스펙은 여기서도 그곳과 같은 의미를 가진다.
| 부분 | 값 | 하는 일 |
align | < > ^ | 왼쪽, 오른쪽, 가운데. 기본값 >. |
fill | align 앞에 오는 임의의 문자 | 패딩 문자. 기본값은 공백. |
sign | + 또는 공백 | 음수가 아닌 수에 부호를 강제하거나, 부호 자리를 하나 예약한다. |
# | 대체 형식(alternate form): 0x, 0X, 0o, 0b 접두어. 정수 전용. | |
0 | 0 채움. 42에 대한 {:08}은 00000042. | |
width | 숫자, 최대 10000 | 최소 필드 width. |
.precision | .N, 최대 60 | 소수 자릿수. float 전용. |
type | x X o b d(int), f g e(float) | 기수(base)와 대소문자. f 고정, g 최단 라운드트립, e 과학적 표기(printf %e). |
과거에 거짓이었던 만큼 알아둘 가치가 있는 두 가지가 있다.
- 선행
0은 0 채움이지 width의 첫 자리가 아니다.{:08}은 v26.07.12f 전까지" 42"를 만들었다(공백 패딩, 오류 없음). 명시적 fill은 여전히 우선한다:{:*>08}은*로 채운다. - 0 채움은 부호와 자릿수 사이에 들어간다.
42에 대한{:+08}은+0000042이며,0000+42가 아니다. 패딩은 숫자의 일부이고, 숫자의 부호가 먼저 온다.
부동소수점
{}는 정확히 반올림된 소수점 이하 6자리를 준다. {:.3}은 3자리, {:.0}은 0자리를 주고, {:e}는 과학적 표기를 강제한다 - 가수(mantissa), precision 소수 자릿수(기본 6자리), 부호 있는 최소 두 자리 지수, 정확히 반올림, 정확히 printf의 %e와 같다 - 이는 {:f}와 {:g}가 도달하지 못하는 형식이다: {:f}는 지수를 절대 보여주지 않고, {:g}는 더 짧을 때만 지수를 사용한다. {:f}는 고정 형식을 강제하고, {:g}는 라운드트립되는 가장 짧은 표현을 준다.
v26.07.12i 전까지 이 중 어느 것도 존재하지 않았다: 모든 float은 영원히 정확히 6자리 소수로만 나왔다. 정확 엔진은 언제나 이 모든 것을 할 수 있었으나, {} 문법이 그것에 도달하지 못했을 뿐이다. 눈에 보이는 대가는 float 컬럼을 정렬할 수 없다는 것이었다. 12.5는 9자 폭으로, 100.0은 10자 폭으로 렌더링되었기 때문이다.
proven_byte_t buf[64];
proven_u8str_t line = proven_u8str_borrow(buf, sizeof buf);
(void)proven_u8str_append_fmt(&line, "{:>9.2}", PROVEN_ARG(12.5)); /* " 12.50" */인자가 지킬 수 없는 명세는 에러다
double에 대한 {:x}, 정수에 대한 {:.2}, 정수에 대한 {:f}, 문자열에 대한 {:#}: 모두 PROVEN_ERR_INVALID_FORMAT.
이들은 과거에 *무시*되었다 - double에 대한 {:x}는 3.500000을 출력하고 성공을 보고했다. 호출자는 무언가를 요청하고, 다른 것을 받았으며, 그것이 제대로 되었다는 말을 들었다. 그것은 있을 수 있는 최악의 결과이며, 거부하는 것보다 나쁘다.
char and bool
PROVEN_ARG('Z')는 Z로 렌더링되고, bool은 true 또는 false로 렌더링된다. 둘 다 과거에는 정수 경로를 거쳤으므로, 문자가 90으로 출력되었고 문자 하나를 방출할 방법이 아예 없었다 - hex 덤프의 ASCII 컬럼은 별도 버퍼에서 손으로 만들어 문자열로 넘겨야 했다. 이제 대문자 hex 덤프는 한 개의 루프다.
proven_byte_t hexbuf[64];
proven_u8str_t hexline = proven_u8str_borrow(hexbuf, sizeof hexbuf);
unsigned char byte = 0xde;
(void)proven_u8str_append_fmt(&hexline, "{:02X} ", PROVEN_ARG((unsigned)byte));
(void)proven_u8str_append_fmt(&hexline, "{}", PROVEN_ARG((char)'.'));5. 형식화 API
proven_u8str_fmt_internal(...)
proven_fmt_result_t proven_u8str_fmt_internal(
proven_allocator_t alloc,
proven_u8str_t *str,
bool trunc,
const char *fmt,
proven_allocator_t scratch,
const proven_arg_t *args,
proven_size_t args_count
);이것은 내부 포매팅 엔진이다. 사용자 코드는 보통 대신 공개 매크로를 호출해야 한다.
파라미터:
alloc: 문자열이 커져야 할 때 사용되는 할당자str: 목적지 U8 문자열trunc: true이면 best-effort 잘림을 허용하고, false이면 atomic(원자적) 동작을 유지fmt: 포맷 텍스트scratch: 필요할 때 임시 alias 패칭에 사용되는 할당자args: 인덱스 0의 숨겨진 sentinel을 포함한 포맷 argument 배열args_count: sentinel을 포함한args의 총 길이
반환값:
proven_fmt_result_t
중요한 규칙:
args_count는 플레이스홀더 개수에 숨겨진 sentinel을 더한 값과 일치해야 한다- 사용되지 않는 여분의 argument는 오류다
- 누락된 argument는 오류다
- 엔진이 목적지 문자열과 빌려온 뷰 argument 사이의 aliasing을 감지하면, 실패 atomicity를 보존하기 위해 임시 작업용(scratch) 할당자를 사용할 수 있다
proven_u8str_append_fmt(str, fmt, ...)
고정 용량 문자열로의 atomic(원자적) 포매팅. 결과가 들어맞지 않으면, 함수는 실패를 보고하고 목적지를 변경하지 않는다.
전부 아니면 전무(all-or-nothing) 동작을 원할 때 사용하라.
proven_u8str_append_fmt_trunc(str, fmt, ...)
Best-effort 포매팅. 들어맞는 만큼 기록하고, 얼마나 기록되었는지와 얼마나 필요했는지를 보고한다.
부분 출력이 허용될 때 사용하라.
proven_u8str_append_fmt_grow(alloc, str, fmt, ...)
성장 가능한(growable) 포매팅. 제공된 할당자를 통해 목적지 문자열을 재할당할 수 있다. 할당 실패 시, 기존 문자열은 유효하게 유지된다.
수동 용량 계획 없이 출력을 들어맞게 하고 싶을 때 사용하라.
proven_u8str_append_fmt_with_scratch(alloc, str, fmt, scratch, ...)
별도의 scratch 할당자를 사용하는 성장 가능한 포매팅. argument 목록에 목적지 버퍼와 alias할 수 있는 문자열 뷰가 있어 임시 패칭이 필요할 때 유용하다.
alloc과 scratch 모두에 실제 할당자를 사용하라. 호출에 충분할 만큼 수명이 길지 않은 한, 죽은 아레나(arena)나 일회용 임시 버퍼를 넘기지 마라.
부동소수점 형식화 정책 이음매
공개 float 정책 헤더는 float 포매팅을 위한 명시적 정책 레이어를 제공한다. 의도적으로 작으며, 정확한 고정 precision 포매터를 기본 경로로 유지한다.
주요 진입점은 다음과 같다.
proven_float_format_f64_policy(...)proven_float_format_f32_policy(...)proven_float_format_options_fixed_default()proven_float_format_options_shortest()
정책 참고 사항:
PROVEN_FLOAT_FORMAT_POLICY_DEFAULT와PROVEN_FLOAT_FORMAT_POLICY_SIMPLE은 정확한 고정 precision 출력(정확히 반올림, round-half-to-even)을 선택한다.PROVEN_FLOAT_FORMAT_POLICY_RYU는 최단 출력 정책 분기다.- 정책 API는 지원되지 않는 enum 값에 대해
PROVEN_ERR_INVALID_ARG를 반환한다. - 정책 API는 호출자가 제공한 버퍼가 너무 작을 때
PROVEN_ERR_OUT_OF_BOUNDS를 반환한다.
예:
char buf[128];
proven_size_t written = 0;
proven_err_t err = proven_float_format_f64_policy(
buf,
sizeof buf,
0.1,
PROVEN_FLOAT_FORMAT_POLICY_RYU,
proven_float_format_options_shortest(),
&written
);
if (proven_is_ok(err)) {
/* buf holds the shortest form that parses back to exactly 0.1 */
proven_println("{}", PROVEN_ARG_CSTR_N(buf, written));
}PROVEN_FMT_IS_OK(res)
proven_fmt_result_t를 검사하는 작은 헬퍼 매크로. 의도를 간결하게 유지하고 싶을 때 사용하라.
예:
proven_result_u8str_t rs = proven_u8str_create(alloc, 32);
if (proven_is_ok(rs.err)) {
proven_u8str_t s = rs.value;
proven_fmt_result_t r = proven_u8str_append_fmt_grow(
alloc,
&s,
"name={} score={:0>4}",
PROVEN_ARG("ada"),
PROVEN_ARG(42)
);
if (!PROVEN_FMT_IS_OK(r)) {
/* the string is untouched: grow-mode formatting is failure-atomic */
proven_eprintln("formatting failed");
}
proven_u8str_destroy(alloc, &s);
}콘솔식 헬퍼
sysio 레이어는 같은 포매터 기계를 사용하는 print 헬퍼들을 제공한다.
proven_print(fmt, ...)proven_println(fmt, ...)proven_eprint(fmt, ...)proven_eprintln(fmt, ...)
이들은 stdout이나 stderr로 직접 포맷 출력을 원할 때 편리하다. 여전히 proven_err_t를 반환하므로, 출력이 중요할 때는 결과를 검사하라.
예:
if (!proven_is_ok(proven_println("hello {}", PROVEN_ARG("world")))) {
/* the write to stdout failed - a closed pipe, a full disk */
proven_eprintln("stdout is not writable");
}5.1. 내가 만든 타입 형식화하기
PROVEN_ARG는 _Generic 위에 세워졌고, _Generic은 컴파일 시점에 알려준 타입에 대해서만 디스패치할 수 있다. 당신의 타입은 알려줄 수 없다. 그래서 포매터의 argument 집합 — 정수, float, 문자열, 포인터, datetime — 은 닫힌 집합이었다: rect_t, uuid_t, vec3_t는 {}에 전혀 넘길 수 없었다.
이를 우회하는 두 방법은 모두 나빴다. 값을 scratch 문자열로 미리 포매팅해서 *그것*을 넘기는 방법: 값마다 할당과 복사가 로깅 경로에서 발생하는데, 로깅 경로야말로 할당이 바로 실패한 상황에서도 계속 동작해야 하는 유일한 경로다. 아니면 필드를 하나씩 출력하고 컬럼 정렬은 포기하는 방법.
PROVEN_ARG_OF(&obj, render)가 그 문이다.
proven_err_t render(proven_fmt_sink_t out, const void *obj);그 시그니처의 형태로부터 세 가지가 따라 나온다.
- 렌더러는 버퍼가 아니라 sink를 받는다. 렌더러는 공간이 얼마나 있는지 알 필요가 없고, 무엇도 오버플로할 수 없다.
proven_fmt_put으로 방출한다. - 합성(compose)된다. 렌더러는 포매터를 다시 호출할 수 있으며 — 스택 버퍼로, 할당자 없이 — 그 결과를 sink에 넘길 수 있다. 필드 자체가 사용자 타입인 타입도 자연스럽게 중첩된다.
- Width, fill, alignment가 동작한다. 포매터는 렌더러를 두 번 실행한다: 먼저 counting sink에 대해 출력 폭을 알아내고, 그다음 실제로, 그 주위에 패딩을 적용하여 실행한다. 이것이
{:>10}이 당신의 타입 컬럼을 int 컬럼을 정렬하는 것과 똑같이 정렬하는 이유이며, 렌더러가 결정적(deterministic)이어야 하고obj를 변경해서는 안 되는 이유다. 두 패스가 불일치하면, 포매터는 정렬된 컬럼에 잘못된 폭의 필드를 방출하고 나중에 알아차리게 하는 대신PROVEN_ERR_INVALID_ARG를 반환한다.
포매터가 하지 않을 일은 추측이다. 사각형에 대한 {:x}, UUID에 대한 {:.2}, 행렬에 대한 {:+} — 라이브러리는 그것들이 당신의 타입에 무엇을 의미하는지 전혀 모르므로, PROVEN_ERR_INVALID_FORMAT으로 거부한다. 그럴듯한 답을 지어내고 성공을 보고하는 것이야말로 포매터가 거짓말을 시작하는 방식이다. 당신이 요청하지 않은 타입 글자는 오류보다 낫지 않다.
테스트 스위트가 컴파일하고 실행함:
/*
* 라이브러리가 들어 본 적 없는 타입을 형식화하기.
*
* `PROVEN_ARG` 는 `_Generic` 위에 서 있고, 그것은 미리 알려 준 타입에만 갈래를 태울 수
* 있다 - 그리고 여러분의 타입을 알려 줄 방법이 없다. 그래서 `PROVEN_ARG_OF` 가 생기기
* 전에는 `rect_t` 를 찍을 방법이 아예 없었다. 임시 문자열에 미리 형식화해서 그것을
* 건네거나(값마다 할당 한 번과 복사 한 번, 그것도 할당하면 안 되는 그 로그 경로에서),
* 필드를 하나씩 찍고 열 맞추기는 포기하거나 둘 중 하나였다.
*
* 렌더러는 버퍼가 아니라 *그릇*을 받는다. 그것이 이 방식을 조립 가능하게 만든다. 렌더러는
* 형식화기를 다시 부를 수 있고, 그 출력은 그저 어딘가로 가는 바이트다. 그리고 형식화기가
* 내보내기 전에 렌더러의 출력을 재기 때문에 - 세는 그릇에 대고 한 번 돌려 본다 - 폭과
* 채움과 정렬이 사용자 타입에서도 int 에서와 똑같이 먹는다.
*/
typedef struct { int w, h; } rect_t;
static proven_err_t render_rect(proven_fmt_sink_t out, const void *obj) {
const rect_t *r = (const rect_t *)obj;
/* 조립: 형식화기를 스택 버퍼로. 할당자는 어디에도 없다. */
proven_byte_t tmp[64];
proven_u8str_t s = proven_u8str_borrow(tmp, sizeof tmp);
proven_fmt_result_t f = proven_u8str_append_fmt(&s, "{}x{}",
PROVEN_ARG(r->w), PROVEN_ARG(r->h));
if (!PROVEN_FMT_IS_OK(f)) return f.err;
return proven_fmt_put(out, proven_u8str_as_view(&s));
}
int main(void) {
rect_t a = { .w = 1920, .h = 1080 };
rect_t b = { .w = 640, .h = 480 };
proven_byte_t buf[128];
proven_u8str_t line = proven_u8str_borrow(buf, sizeof buf);
/* 다른 인자와 똑같다. */
proven_fmt_result_t r = proven_u8str_append_fmt(&line, "mode={}", PROVEN_ARG_OF(&a, render_rect));
EXAMPLE_REQUIRE(PROVEN_FMT_IS_OK(r), "a user type should format");
EXAMPLE_REQUIRE(proven_u8str_view_eq(proven_u8str_as_view(&line), PROVEN_LIT("mode=1920x1080")),
"the renderer's bytes should be what came out");
/* 그리고 정렬된다. 형식화기가 먼저 재는 이유가 바로 이것이다 - 사용자 정의 값의 열이
* 다른 무엇의 열과 똑같이 줄을 맞춘다. */
(void)proven_u8str_reset(&line);
r = proven_u8str_append_fmt(&line, "[{:>10}]\n[{:>10}]",
PROVEN_ARG_OF(&a, render_rect),
PROVEN_ARG_OF(&b, render_rect));
EXAMPLE_REQUIRE(PROVEN_FMT_IS_OK(r), "two user types should format");
EXAMPLE_REQUIRE(proven_u8str_view_eq(proven_u8str_as_view(&line),
PROVEN_LIT("[ 1920x1080]\n[ 640x480]")),
"both rows should be right-aligned to the same width");
/* 여러분의 타입에 대해 라이브러리가 해석할 수 없는 지정자는 추측하지 않고 거부된다.
* 사각형에 `{:x}` 는 뜻이 없고, 성공했다고 알리면서 그럴듯한 무언가로 답하는 것이
* 형식화기가 여러분에게 거짓말을 시작하는 방식이다. */
(void)proven_u8str_reset(&line);
r = proven_u8str_append_fmt(&line, "{:x}", PROVEN_ARG_OF(&a, render_rect));
EXAMPLE_REQUIRE(r.err == PROVEN_ERR_INVALID_FORMAT,
"a type letter on a user type should be an error");
return EXAMPLE_OK();
}6. 콘솔 출력 헬퍼
proven_println은 무엇이고, 비용은 얼마인가
proven_println("{}", PROVEN_ARG(x))는 프로그램에서 텍스트를 내보내는 가장 짧은 방법이며, 진단 출력이나 일회성 도구, 출력이 몇 줄뿐인 프로그램에는 알맞은 도구다.
루프에는 잘못된 도구인데, 그 이유는 호출 지점에서 보이지 않으므로 명시할 가치가 있다. 호출 하나가 각각 하나의 write 시스템 콜이다. proven_println 만 번 호출은 시스템 콜 만 번이고, 이는 포매팅 자체보다 대략 두 자릿수만큼 비싸다. printf는 libc가 대신 flush해 주는 버퍼 뒤에 이것을 숨긴다. 이 라이브러리는 여러분 모르게 버퍼링하지 않는데, 요청하지 않은 버퍼는 종료 시점에, 크래시 때, 또는 두 기록자가 뒤섞일 때 여러분을 놀라게 하는 버퍼이기 때문이다.
할당에 관한 주의 하나. 줄은 512바이트 스택 버퍼에 포매팅되므로 일반적인 줄은 할당 비용이 0이다. 들어가지 않는 줄은 거부되는 대신 그 호출에 한해 전역 힙(heap)으로 물러난다 — 길다는 이유로 출력을 거부하는 편이 더 나쁘기 때문이다. 이는 allocator 매개변수가 없는 호출이 그래도 할당할 수 있는 라이브러리 내 유일한 지점이며, 과도하게 긴 경우로 한정된다. 아무것도 할당하지 않는다는 보장이 필요하다면 직접 버퍼에 포매팅한 뒤 그것을 기록할 것.
출력량이 문제가 될 때는 proven_sysio_stdout_buffered에서 버퍼드 쓰기 스트림(writer)를 받아 거기에 포매팅한다 — argument 규칙은 동일하고, 줄마다 한 번이 아니라 flush마다 한 번의 시스템 콜이 된다. Chapter 5가 스트림 계층을 다룬다.
잘못된 예 — 출력하면서 스캐닝용 argument 생성자를 집어들기:
proven_println("{}", PROVEN_SCAN_ARG(&x)); /* wrong: that builds a scan destination */이 섹션은 상세 I/O API가 Chapter 5에 있기 때문에 의도적으로 짧다. 포매터 사용자에게 중요한 점은 콘솔 헬퍼들이 문자열 append API와 같은 argument 규칙을 공유한다는 것이다.
흔한 실수:
proven_println에PROVEN_SCAN_ARG를 사용하기- 모든 포맷 문자열에
PROVEN_LIT이 필요하다고 가정하기 - 출력 함수도 여전히 실패할 수 있다는 것을 잊기
7. 스캐너 데이터 모델
스캐너는 당신이 이미 가지고 있는 바이트 위의 커서다. 텍스트를 소유하지 않고, 복사하지 않으며, 어디에서도 읽지 않는다 - proven_scan_t는 뷰에 오프셋을 더한 것이며, 그것이 전부다.
typedef struct {
proven_u8str_view_t view; /* the bytes being read; not owned */
proven_size_t cursor; /* how far in we are */
} proven_scan_t;이것이 scanf와 다른 이유이므로 언급할 가치가 있는 두 가지 결과:
- 스캐너는 절대 할당하지 않고 당신의 입력에 절대 기록하지 않는다. 스캔된 단어는 원본 바이트 *안*을 가리키는
proven_u8str_view_t로 반환된다. 그것은 정확히 그 바이트가 유효한 동안만 유효하며, 그 이상은 아니다. 그것들보다 오래 살아야 한다면,proven_u8str_create_from_view()로 복사하라. - 커서는 당신의 것이다. 그것은 평범한 필드다. 저장하거나, 복원하거나, 손으로 진행시킬 수 있다(§12는
proven_scan_skip_until이후 정확히 그렇게 한다). 스캐너의 어떤 것도 당신에게 숨겨져 있지 않으므로, 당신을 위해 되돌려야 할 것도 없다.
proven_scan_init()은 잘못된 형식의 뷰(size > 0인데 null 포인터)를 신뢰하는 대신 빈 뷰로 정규화한다. 따라서 쓰레기로부터 만들어진 스캐너는 역참조 대신 입력 끝(EOF)으로 읽힌다.
각 primitive는 값을 그것을 지키는 오류와 짝지은 result 구조체를 반환한다: proven_result_i64_t, proven_result_u64_t, proven_result_f64_t, proven_result_u8str_view_t. 오류가 PROVEN_OK가 아닌 한 값은 무의미하다 - 스캐너는 실패를 뜻하는 sentinel 값을 사용하지 않는데, 모든 sentinel은 동시에 정당한 입력이기도 하기 때문이다.
8. 스캐너 기본 API
void proven_scan_skip_whitespace(proven_scan_t *scan);
proven_result_i64_t proven_scan_i64(proven_scan_t *scan);
proven_result_u64_t proven_scan_u64(proven_scan_t *scan);
proven_result_f64_t proven_scan_f64(proven_scan_t *scan);
proven_result_u8str_view_t proven_scan_str(proven_scan_t *scan);
proven_err_t proven_scan_skip_until(proven_scan_t *scan, proven_u8str_view_t target);
void proven_scan_skip_until_number(proven_scan_t *scan);값을 반환하는 것들은 [[nodiscard]]다: 결과를 버리는 스캔은 애초에 할 필요가 없던 스캔이다.
공통 동작
- 선행 공백은 건너뛴다 — 모든 값 스캐너가 그렇게 한다. 먼저
proven_scan_skip_whitespace()를 호출할 필요가 없다. 그 함수는 커서를 직접 위치시키고 싶을 때를 위해 존재한다. - 스캐닝은 값에 속할 수 없는 첫 바이트에서 멈춘다.
"12abc"는12를 내고 커서를a에 남긴다. 그것은 오류가 아니다 - 스캐너는 당신이 물은 질문에 답했고 나머지는 다음에 묻는 이를 위해 남겼다. - 실패 시 커서는 복원된다. 따라서 실패한 스캔은 비사건(non-event)이다: 돌아서서 같은 위치를 다른 것으로 파싱할 수 있다. §12가 이렇게 한다.
정수 스캐너
| 입력 | proven_scan_i64 | 이유 |
"42", "+42", "-42" | OK - 42, 42, -42 | 부호는 숫자의 일부다 |
"9223372036854775808" | PROVEN_ERR_OVERFLOW | INT64_MAX보다 하나 큼. 랩(wrap)하지 않는다 |
"abc", "" | PROVEN_ERR_INVALID_ARG | 여기에 숫자가 없다 |
"0x10" | OK - 0, 커서는 1에 | 십진수만: 0, 그 뒤에 텍스트 |
마지막 행이 사람들을 놀라게 하는 것이다. proven_scan_i64와 proven_scan_u64는 십진수를 읽는다. hex도, 8진수도, 기수 접두어도 없다. 0x10은 정수 0이고, x10은 여전히 입력에 남아 있다.
proven_scan_u64는 unsigned를 뜻한다: "-1"은 18446744073709551615로 랩하는 것이 아니라 PROVEN_ERR_INVALID_ARG다. 음수를 조용히 거대한 양수로 바꾸는 스캐너는 경계 검사가 무력화되는 방식이다.
부동소수점 스캐너
proven_scan_f64는 라이브러리의 나머지와 같은 정확히 반올림되는 십진 엔진을 거친다: 가장 가까운 값으로 반올림, 동점은 짝수로, 그 어디에도 long double 없음. nan과 inf를 받아들인다.
두 경계 동작은 의도적으로 비대칭이며, 그 비대칭이 요점이다.
"1e309"는PROVEN_ERR_OVERFLOW를 준다. 올바른 유한 답이 없으므로, 요청하지 않은 무한대를 건네는 대신 거부한다."1e-400"은PROVEN_OK와0.0을 주며, 부호는 보존된다. 0으로의 언더플로는 정확히 반올림된 답 *그 자체*다. 그것을 오류로 보고하는 것은 올바른 산술을 실패로 보고하는 것이 될 것이다.
낱말과 커서 이동
proven_scan_str은 다음 공백으로 구분된 구간을 입력 안을 가리키는 뷰로 반환한다. 공백 외에 아무것도 남지 않은 경우는 PROVEN_ERR_INVALID_ARG다.
완전한 뷰에서는 "입력이 떨어졌다"와 "입력이 틀렸다"가 같은 사실이다 - 더 이상 입력이 없으므로, 끝에서 잘린 숫자는 정말로 잘못된 형식이다. 스트림 위에서는 이 둘이 반대되는 사실이며, 그 차이가 기다릴지 오류를 보고할지를 결정한다. 파싱이 가진 것의 끝을 넘어 달렸을 때 스캐너는 proven_scan_t::needs_more를 설정하며, 버퍼드 스캐너는 정확히 그것을 사용해 다시 채우고 재시도한다: -를 전달한 뒤 잠시 후 12를 전달하는 파이프는 -12로 스캔된다. 이전에는 잘못된 형식의 숫자였다. 실제로 존재하는 잘못된 바이트 - 숫자가 와야 할 자리의 글자 - 는 여전히 오류이며, 이후 어떤 입력도 그것을 바꾸지 못한다. 스캐너는 그것을 기다리지 않는다.
proven_scan_skip_until(scan, target)은 커서를 target을 지나서가 아니라 target으로 옮긴다 - 그것을 얼마나 소비할지는 당신이 결정한다. target이 거기 없으면 결과는 PROVEN_ERR_NOT_FOUND이고 커서는 움직이지 않는다: 스캐너는 탐색에 실패한 입력을 소비하지 않는다.
proven_scan_skip_until_number는 첫 숫자에서, 또는 바로 뒤에 숫자가 따라오는 부호에서 멈춘다. 숫자가 없으면 커서를 입력의 끝까지 몰아간다 - 따라서 읽을 것이 있다고 가정하기 전에 커서(cursor), 곧 스캐너가 지금까지 읽어 온 위치를 scan.cursor < scan.view.size로 검사하라.
9. 스캔 인자 모델
스캔 argument는 컴파일러가 선택하는 당신의 목적지를 가리키는 타입 있는 포인터다.
PROVEN_SCAN_ARG(&x) /* _Generic on the pointer type */이것이 스캐너가 scanf와 가장 첨예하게 다른 지점이다. 잘못 쓸 포맷 글자가 없는데, 포맷 글자가 아예 없기 때문이다. long에 대한 %d나, 너무 작은 버퍼에 대한 %s는 여기서 가능한 실수가 아니다: 목적지의 타입 *자체*가 명세이며, 불일치는 손상된 스택이 아니라 컴파일 오류다.
지원되는 목적지: short, unsigned short, int, unsigned int, long, unsigned long, long long, unsigned long long, double, 그리고 proven_u8str_view_t.
PROVEN_SCAN_ARG_LONG(&x)와 PROVEN_SCAN_ARG_ULONG(&x)는 호출 지점에서 명시적이길 원하는 호출자를 위해 존재한다. PROVEN_SCAN_ARG는 이미 long*와 unsigned long*를 처리한다.
좁은 목적지는 범위 검사된다. "70000"을 short로 스캔하는 것은 잘린 4464가 아니라 PROVEN_ERR_OVERFLOW다. 값은 64비트로 파싱되어, 무언가를 저장하기 전에 목적지의 범위와 대조 검사된다.
proven_scan_arg_* 생성자들은 argument 배열을 손으로 만들어야 할 때를 위해 공개되어 있지만, 호출자가 사용하는 것은 매크로다.
10. 구조적 스캔 문법
포맷 문자열로 파싱하는 것이 출력하는 것보다 위험한 이유
잘못된 포맷 문자열로 포매팅하면 잘못된 출력이 나온다. 그것으로 파싱하면 메모리가 망가진다. 포맷이 여러분이 넘긴 포인터를 통해 무엇을 쓸지 결정하기 때문이다. c가 char인데 sscanf("%d", &c)를 하면 1바이트 객체에 4바이트를 쓰는데, 호출문 어디에도 그런 말이 없다.
이 비대칭이 이 섹션의 모양을 결정한다. 구조적 스캔은 포매터와 같은 {} 문법을 쓰지만, 목적지 타입은 PROVEN_SCAN_ARG(&x)에서 온다 — 같은 _Generic 디스패치이므로 기록되는 폭은 객체의 폭이고, 거기 담기에 너무 큰 값은 이웃한 세 바이트가 아니라 PROVEN_ERR_OVERFLOW가 된다.
쓰기 전에 반드시 체득해야 할 성질 하나. 구조적 스캔은 플레이스홀더 전체에 걸쳐 트랜잭션이 아니다. 세 번째 {}가 실패하면 앞의 두 목적지는 이미 기록된 뒤다. 이는 의도한 절충이다 — 줄 전체가 파싱될 때까지 모든 목적지를 버퍼링하려면 할당이 필요한데, 이 스캐너는 아무것도 할당하지 않는다 — 그러나 그 결과로 실패한 proven_scan_fmt는 변수들을 일부만 갱신된 상태로 남긴다. 실패 시 그것들을 쓰레기로 취급하거나, 지역 변수에 스캔한 뒤 성공했을 때만 복사해 낼 것. §11.1이 두 패턴을 모두 보여준다.
패턴 속 리터럴은 문법의 나머지 절반이며 사람들이 잘 활용하지 않는 부분이다. 플레이스홀더가 아닌 모든 것은 입력과 정확히 일치해야 하므로, "12-34"에 대한 "{}:{}"는 한 필드를 조용히 돌려주는 대신 리터럴에서 실패한다.
스캔 포맷 문자열은 포매터의 것을 거꾸로 읽은 것이다.
- 플레이스홀더는 argument 하나를 순서대로 소비한다.
- 그 외의 모든 것은 입력과 정확히 일치해야 하는 리터럴이다.
스캐닝 측에서는 플레이스홀더 안에 스펙이 없다. Width, fill, alignment는 포매팅의 관심사이며, 스캐너는 거기 있는 것을 읽는다.
포맷 안의 공백은 특별하지 않다. 값 스캐너들이 스스로 선행 공백을 건너뛰므로, 두 플레이스홀더 사이에 공백이 있는 포맷과 없는 포맷은 "7 8"을 동일하게 파싱한다 - 포맷의 공백은 입력의 공백과 일치하고, 그것이 없었더라도 두 번째 스캐너가 어차피 건너뛰었을 것이다.
플레이스홀더의 수는 argument의 수와 같아야 한다. 입력의 값이 너무 적은 것은 오류다. 너무 많은 것은 오류가 아니다(§11.1).
11. 스캔 형식화 API
proven_scan_fmt(view, fmt, ...) /* scan a view from the beginning */
proven_scan_fmt_cursor(&scan, fmt, ...) /* continue from an existing cursor */
proven_err_t proven_scan_fmt_internal(...) /* what the macros expand to */자기완결적인 한 줄에는 proven_scan_fmt를 사용하라. 스캔이 같은 입력 위를 걷는 더 긴 여정의 한 단계일 때는 proven_scan_fmt_cursor를 사용하라: 이것은 당신이 소유한 커서를 진행시키므로, §8의 primitive들과 자유롭게 섞인다.
11.1. 스캔 에러 코드 안내와 복구
| 코드 | 실제로 일어난 일 | 할 일 |
PROVEN_OK | 모든 플레이스홀더가 채워졌고 모든 리터럴이 일치했다. | 값들을 발행(publish)하라. |
PROVEN_ERR_INVALID_ARG | 입력이 당신이 요청한 형태가 아니다 - 플레이스홀더가 읽을 값이 없었거나, 입력이 떨어졌다. | 그 줄은 일치하지 않는다. 보고하되, 같은 형태로 재시도하지 마라. |
PROVEN_ERR_NEED_MORE | 버퍼드 스캐너 전용. 토큰이 읽기 경계에서 반으로 잘렸다: 그 나머지가 아직 도착하지 않았다. 보통은 이것을 보지 않는다 - proven_sysio_scanner_scan이 당신을 위해 다시 채우고 재시도한다 - 이는 스캐너가 스스로에게 하는 말이다. | 아무것도. 이미 처리되었다. |
PROVEN_ERR_NOT_FOUND | 포맷 안의 리터럴이 일치하지 않았다. | 그 줄은 예상과 다른 형태다. 저장된 커서에서 다른 포맷을 시도하라. |
PROVEN_ERR_OVERFLOW | 숫자는 제대로 된 형식이었지만 목적지에 들어맞지 않는다. | 입력이 유효한데 목적지가 너무 좁은 것일 수도, 입력이 적대적인 것일 수도 있다. 그 둘은 매우 다른 상황이다 - 타입을 넓히기 전에 구별하라. |
PROVEN_ERR_INVALID_FORMAT | 포맷 문자열 자체가 잘못된 형식이다. | 입력이 아니라 당신 코드의 버그다. |
구조적 스캐너는 트랜잭션이 아니며, 이것이 물어뜯는 그 지점이다.
리터럴이 일치에 실패했을 때, 불일치 *이전*의 플레이스홀더들은 이미 기록되어 있다. 호출은 PROVEN_ERR_NOT_FOUND를 반환하는데 당신의 목적지는 그래도 값을 담고 있다 - 아래의 id는 실패한 호출에서 온 7이다.
int id = -1;
double ratio = -1.0;
proven_err_t err = proven_scan_fmt(line, "id={} XXX={}",
PROVEN_SCAN_ARG(&id), PROVEN_SCAN_ARG(&ratio));
/* err == PROVEN_ERR_NOT_FOUND, and id == 7: it was written before the failure. */그러므로: 실패 시 모든 목적지를 오염된(clobbered) 것으로 취급하라. 전부 아니면 전무가 필요하면, 지역 변수로 스캔하고 호출이 성공한 뒤에만 발행하라 - §12의 예제가 그 형태를 보여준다. 대안으로, 호출 전에 scan.cursor를 저장하고 이후에 복원하라. 커서는 평범한 필드이며, 그것은 의도적이다.
후행 입력은 오류가 아니다. "7 8"에 대해 플레이스홀더 하나를 스캔하면 값 7로 성공하고 8은 소비되지 않은 채 남는다. 스캐너는 당신이 요청한 것을 일치시키고 멈췄다. 당신이 묻지 않은 것을 단속하지 않는다. 줄 전체가 소비되어야 한다면, 커서를 직접 검사하라.
if (scan.cursor != scan.view.size) { /* there is unparsed input left */ }12. 예제와 오용 사례
실전 예제: 한 줄을 형식화하고 다시 읽어 들이기
테스트 스위트가 컴파일하고 실행함. 스택에서 빌린 문자열(오버플로 시 atomic)과 할당자 기반 문자열(성장함)로 포매팅하고, 신뢰할 수 없는 바이트에 경계 argument를 사용한 뒤, 그 줄을 다시 파싱한다 - float은 정확히 라운드트립된다.
/*
* 형식화와 파싱은 같은 생각의 두 반쪽이다. `{}` 는 타입 있는 값을 글로 그려 내고,
* 스캐너는 글을 타입 있는 목적지로 되읽는다. 둘 다 호출 자리에서 타입이 검사되므로
* (_Generic 이 생성자를 고른다), 실행 중에 어긋날 서식 문자열/인자 짝이 없다.
*
* 중요한 선택은 *바이트가 어디로 가는가* 다.
*
* append_fmt - 용량 고정, 원자적. 너무 길면? 아무것도 쓰이지 않고
* PROVEN_ERR_OUT_OF_BOUNDS 를 받는다. 할당자가 끼지 않으므로
* 스택 버퍼에서도 돈다.
* append_fmt_grow - 할당자를 등에 업는다. 들어가도록 늘리고, 할당이 실패하면
* 문자열은 있던 그대로 남는다.
*/
int main(void) {
proven_allocator_t alloc = proven_heap_allocator();
/* --- 용량 고정: 할당자도, 할당도 없다 ---------------------------------- */
/* borrow 는 부르는 쪽의 기억을 감싼다. 그래서 이 문자열은 통째로 스택에 산다. `cap`
* 은 NUL 을 포함하므로 32 바이트는 내용 31 바이트를 담는다. 지울 것도 없다. */
proven_byte_t stack_buf[32];
proven_u8str_t fixed = proven_u8str_borrow(stack_buf, sizeof stack_buf);
proven_fmt_result_t r = proven_u8str_append_fmt(&fixed, "port={}", PROVEN_ARG(8080));
EXAMPLE_REQUIRE(PROVEN_FMT_IS_OK(r), "a short line should fit in 32 bytes");
EXAMPLE_REQUIRE(proven_u8str_view_eq(proven_u8str_as_view(&fixed), PROVEN_LIT("port=8080")),
"the fixed-capacity append should have rendered the port");
/* 원자적이라는 말은 원자적이라는 뜻이다. 들어가지 않는 append 는 아무것도 바꾸지
* 않는다. 문자열은 여전히 온전하고 전에 담고 있던 것을 그대로 담고 있다 - 치울 잘린
* 꼬리가 없다. (잘린 꼬리가 원하는 것이라면 append_fmt_trunc 를 쓸 것.) */
proven_fmt_result_t too_long = proven_u8str_append_fmt(
&fixed, " and a great deal more text than will ever fit here {}", PROVEN_ARG(1));
EXAMPLE_REQUIRE(too_long.err == PROVEN_ERR_OUT_OF_BOUNDS, "the overlong append must fail");
EXAMPLE_REQUIRE(too_long.required > too_long.written, "it reports what it would have needed");
EXAMPLE_REQUIRE(proven_u8str_view_eq(proven_u8str_as_view(&fixed), PROVEN_LIT("port=8080")),
"a failed atomic append must leave the string untouched");
/* --- 지정자: 채움, 정렬, 폭, 16진 ------------------------------------- */
proven_result_u8str_t created = proven_u8str_create(alloc, 8); /* 일부러 작게 */
EXAMPLE_REQUIRE(proven_is_ok(created.err), "creating the output string should succeed");
if (!proven_is_ok(created.err)) return 1;
proven_u8str_t out = created.value;
/* grow 는 필요한 만큼 재할당하므로, 처음 용량은 한계가 아니라 힌트다.
* `{:0>4}` = 채움 '0', 오른쪽 정렬, 폭 4. `{:x}` = 소문자 16진, 0x 없음. */
r = proven_u8str_append_fmt_grow(alloc, &out, "id={:0>4} tag={:*^9} addr=0x{:x}",
PROVEN_ARG(7),
PROVEN_ARG(PROVEN_LIT("ok")),
PROVEN_ARG(48879));
EXAMPLE_REQUIRE(PROVEN_FMT_IS_OK(r), "the growing append should succeed");
EXAMPLE_REQUIRE(proven_u8str_view_eq(proven_u8str_as_view(&out),
PROVEN_LIT("id=0007 tag=***ok**** addr=0xbeef")),
"fill/align/width/hex should render exactly this");
printf("%s\n", proven_u8str_as_cstr(&out));
/* --- 믿을 수 없는 글은 경계가 있고, NUL 로 끝난다고 믿지 않는다 -------- */
/* char* 에 PROVEN_ARG 를 쓰면 "NUL 이 나올 때까지 걸어라" 는 뜻이다 - 리터럴에는
* 괜찮지만 소켓에서 온 것에는 버퍼 넘어 읽기다. 이 버퍼에는 NUL 이 아예 없다.
* PROVEN_ARG_CSTR_N 은 대신 길이에서 멈추므로 실제로 있는 것만 읽는다. 여러분이 직접
* 만들지 않은 것에는 이것을 쓸 것. */
const char untrusted[4] = {'a', 'b', 'c', 'd'}; /* 일부러 종결자를 두지 않았다 */
EXAMPLE_REQUIRE(proven_is_ok(proven_u8str_reset(&out)), "reset should keep the buffer");
r = proven_u8str_append_fmt_grow(alloc, &out, "payload={}",
PROVEN_ARG_CSTR_N(untrusted, sizeof untrusted));
EXAMPLE_REQUIRE(PROVEN_FMT_IS_OK(r), "the bounded append should succeed");
EXAMPLE_REQUIRE(proven_u8str_view_eq(proven_u8str_as_view(&out), PROVEN_LIT("payload=abcd")),
"the bounded argument should render its whole 4 bytes and stop");
/* --- 레코드를 형식화하고, 도로 파싱하기 -------------------------------- */
proven_i64 sensor_id = 42;
double reading = 3.14159;
EXAMPLE_REQUIRE(proven_is_ok(proven_u8str_reset(&out)), "reset should keep the buffer");
r = proven_u8str_append_fmt_grow(alloc, &out, "{} {} {}",
PROVEN_ARG(sensor_id),
PROVEN_ARG(PROVEN_LIT("boiler")),
PROVEN_ARG(reading));
EXAMPLE_REQUIRE(PROVEN_FMT_IS_OK(r), "formatting the record should succeed");
printf("record: %s\n", proven_u8str_as_cstr(&out));
/* 뷰 하나 위의 스캐너 하나. 호출마다 커서를 자기가 삼킨 만큼 앞으로 옮기므로 호출이
* 왼쪽에서 오른쪽으로 이어진다 - 그리고 하나하나가 따로 실패할 수 있는데, 그것이
* 파서와 짐작을 가르는 차이다. */
proven_scan_t sc = proven_scan_init(proven_u8str_as_view(&out));
proven_result_i64_t id = proven_scan_i64(&sc);
EXAMPLE_REQUIRE(proven_is_ok(id.err), "the first field should parse as an integer");
EXAMPLE_REQUIRE(id.val == sensor_id, "the integer should round-trip");
/* scan_str 은 *파싱 중인 문자열 안*을 가리키는 뷰를 돌려준다 - 복사도 소유도 하지
* 않으므로 `out` 이 살아 있는 동안에만 쓸 수 있다. */
proven_result_u8str_view_t name = proven_scan_str(&sc);
EXAMPLE_REQUIRE(proven_is_ok(name.err), "the second field should parse as a word");
EXAMPLE_REQUIRE(proven_u8str_view_eq(name.val, PROVEN_LIT("boiler")), "the name should round-trip");
proven_result_f64_t temp = proven_scan_f64(&sc);
EXAMPLE_REQUIRE(proven_is_ok(temp.err), "the third field should parse as a float");
/* 근사하게가 아니라 정확히 같다. 스캐너는 올바르게 반올림하므로 글자에 가장 가까운
* double 을 돌려주고 - 형식화기가 내놓은 글자(소수 여섯 자리)는 이 값을 모호함 없이
* 지목한다. 비트 하나까지 우리가 시작한 그 double 이다. 소수 여섯 자리보다 더 필요한
* 값이라면 가장 짧은 정책(proven_float_format_options_shortest)으로 형식화하면 같은
* 왕복이 성립한다. */
EXAMPLE_REQUIRE(temp.val == reading, "the float must round-trip exactly, not approximately");
/* 입력을 남김없이 삼켰다. 조용히 남겨 둔 것이 없다. */
proven_result_i64_t extra = proven_scan_i64(&sc);
EXAMPLE_REQUIRE(!proven_is_ok(extra.err), "there should be nothing left to scan");
proven_u8str_destroy(alloc, &out);
return EXAMPLE_OK();
}실전 예제: 스캐너의 에러 코드와 그로부터의 복구
테스트 스위트가 컴파일하고 실행함. 위 표의 모든 코드를 여기서 일부러 유발한다. 비-트랜잭션 실패까지 포함하는데 - 읽기만 한 계약은 배우지 못한 계약이기 때문이다.
/*
* 스캐너의 오류 코드들, 그리고 거기서 되살아나는 법.
*
* 이 스캐너는 scanf 가 아니다. 받지 않은 포인터를 통해 쓰는 일이 없고, 폭을 짐작하지
* 않으며, 여러 가지 중 무엇이 잘못됐는지를 말해 준다. 마지막 것은 그 코드들이 무슨
* 뜻인지 알아야 도움이 되므로, 이 프로그램은 하나하나를 일부러 일으켜 본다.
*/
static proven_u8str_view_t v(const char *s) {
return proven_u8str_view_from_cstr(s);
}
int main(void) {
/* --- 기본 호출들은 실패하면 커서를 되돌린다 ---------------------------- */
/* 실패한 파싱은 없던 일이다. 커서는 있던 자리에 있으므로, 같은 자리를 다른 것으로
* 읽어 볼 수 있다. */
{
proven_scan_t sc = proven_scan_init(v("abc"));
proven_result_i64_t n = proven_scan_i64(&sc);
EXAMPLE_REQUIRE(n.err == PROVEN_ERR_INVALID_ARG, "'abc' is not an integer");
EXAMPLE_REQUIRE(sc.cursor == 0, "a failed integer scan leaves the cursor alone");
/* 그래서 같은 자리를 낱말로 읽을 수 있다. */
proven_result_u8str_view_t w = proven_scan_str(&sc);
EXAMPLE_REQUIRE(proven_is_ok(w.err) && proven_u8str_view_eq(w.val, PROVEN_LIT("abc")),
"the same bytes parse fine as a word");
}
/* --- 들어가지 않는 수는 감긴 값이 아니라 OVERFLOW 다 ------------------ */
{
proven_scan_t sc = proven_scan_init(v("9223372036854775808")); /* INT64_MAX + 1 */
proven_result_i64_t n = proven_scan_i64(&sc);
EXAMPLE_REQUIRE(n.err == PROVEN_ERR_OVERFLOW, "one past INT64_MAX must not wrap");
EXAMPLE_REQUIRE(sc.cursor == 0, "the cursor is restored on overflow too");
}
/* --- 그러나 아래로 넘치는 실수는 오류가 *아니다* ---------------------- */
/* 너무 크면 OVERFLOW 이고, 너무 작으면 부호를 지킨 0 이다. 그 비대칭은 일부러다.
* 0 으로 내려앉는 것은 올바르게 반올림한 답이지만, 위로 넘치는 데는 올바른 유한한
* 답이 아예 없다. */
{
proven_scan_t big = proven_scan_init(v("1e309"));
proven_result_f64_t b = proven_scan_f64(&big);
EXAMPLE_REQUIRE(b.err == PROVEN_ERR_OVERFLOW, "1e309 does not fit a double");
proven_scan_t tiny = proven_scan_init(v("-1e-400"));
proven_result_f64_t t = proven_scan_f64(&tiny);
EXAMPLE_REQUIRE(proven_is_ok(t.err), "1e-400 underflows, which is not an error");
EXAMPLE_REQUIRE(t.val == 0.0, "it rounds to zero");
}
/* --- 정수 스캐너는 십진만 읽는다 -------------------------------------- */
/* "0x10" 은 열여섯이 아니다. 0 하나이고, 그 뒤는 스캐너가 보라고 하지 않은 글이다.
* 사람들이 놀라는 자리라 알아 둘 값어치가 있다. */
{
proven_scan_t sc = proven_scan_init(v("0x10"));
proven_result_i64_t n = proven_scan_i64(&sc);
EXAMPLE_REQUIRE(proven_is_ok(n.err) && n.val == 0, "0x10 scans as the integer 0");
EXAMPLE_REQUIRE(sc.cursor == 1, "and the cursor stops before the 'x'");
}
/* --- 파싱은 그 값에 속할 수 없는 첫 바이트에서 멈춘다 ----------------- */
{
proven_scan_t sc = proven_scan_init(v("12abc"));
proven_result_i64_t n = proven_scan_i64(&sc);
EXAMPLE_REQUIRE(proven_is_ok(n.err) && n.val == 12, "12abc yields 12");
EXAMPLE_REQUIRE(sc.cursor == 2, "and leaves 'abc' for whoever asks next");
}
/* --- 부호 없음은 부호 없음이라는 뜻이다 ------------------------------- */
{
proven_scan_t sc = proven_scan_init(v("-1"));
proven_result_u64_t n = proven_scan_u64(&sc);
EXAMPLE_REQUIRE(n.err == PROVEN_ERR_INVALID_ARG,
"-1 is rejected rather than wrapping to a huge unsigned value");
}
/* --- 값까지 찾아가기: skip_until -------------------------------------- */
/* skip_until 은 커서를 목표 *위*에 두지, 지나쳐 두지 않는다. 그것을 얼마나 삼킬지는
* 여러분이 정한다. */
{
proven_scan_t sc = proven_scan_init(v("port=8080"));
proven_err_t err = proven_scan_skip_until(&sc, PROVEN_LIT("="));
EXAMPLE_REQUIRE(proven_is_ok(err), "the '=' is there");
EXAMPLE_REQUIRE(sc.cursor == 4, "the cursor sits on the '=' itself");
++sc.cursor; /* 그것을 넘어선다 */
proven_result_i64_t port = proven_scan_i64(&sc);
EXAMPLE_REQUIRE(proven_is_ok(port.err) && port.val == 8080, "the port parses");
/* 못 찾으면 NOT_FOUND 이고 커서는 움직이지 않는다 - 스캐너는 찾아가지 못한
* 입력을 삼키지 않는다. */
proven_scan_t sc2 = proven_scan_init(v("port=8080"));
proven_err_t missing = proven_scan_skip_until(&sc2, PROVEN_LIT("#"));
EXAMPLE_REQUIRE(missing == PROVEN_ERR_NOT_FOUND, "there is no '#'");
EXAMPLE_REQUIRE(sc2.cursor == 0, "and the cursor stayed put");
}
/* --- 구조를 읽는 스캐너 ------------------------------------------------ */
{
int id = 0;
double ratio = 0.0;
proven_u8str_view_t name = {0};
proven_err_t err = proven_scan_fmt(v("id=7 ratio=0.5 name=ada"),
"id={} ratio={} name={}",
PROVEN_SCAN_ARG(&id),
PROVEN_SCAN_ARG(&ratio),
PROVEN_SCAN_ARG(&name));
EXAMPLE_REQUIRE(proven_is_ok(err), "the line matches the shape");
EXAMPLE_REQUIRE(id == 7 && ratio == 0.5, "the values land in the right places");
EXAMPLE_REQUIRE(proven_u8str_view_eq(name, PROVEN_LIT("ada")), "including the word");
}
/* --- 구조를 읽는 스캐너는 트랜잭션이 *아니다* ------------------------- */
/*
* 이것이 사람을 무는 자리다. 리터럴이 맞지 않으면 파싱은 오류를 돌려주는데 - 그
* 어긋남 *앞*의 자리표들은 이미 목적지에 쓰여 버렸다. 호출이 실패했는데도 `id` 는
* 7 이다.
*
* 그러니 실패했을 때는 모든 목적지가 더럽혀졌다고 여길 것. 전부 아니면 전무가
* 필요하면 지역 변수로 파싱하고 호출이 성공한 뒤에만 공표할 것. 아래 코드가 하는
* 일이 그것이다.
*/
{
int id = -1;
double ratio = -1.0;
proven_err_t err = proven_scan_fmt(v("id=7 ratio=0.5"),
"id={} XXX={}", /* 리터럴이 틀렸다 */
PROVEN_SCAN_ARG(&id),
PROVEN_SCAN_ARG(&ratio));
EXAMPLE_REQUIRE(err == PROVEN_ERR_NOT_FOUND, "the literal 'XXX=' is not in the input");
EXAMPLE_REQUIRE(id == 7, "and yet id was already written: the scan is not atomic");
/* 안전한 모양: 지역 변수로 파싱하고 성공했을 때 공표한다. */
int good_id = 0;
double good_ratio = 0.0;
int published_id = -1;
proven_err_t ok = proven_scan_fmt(v("id=7 ratio=0.5"), "id={} ratio={}",
PROVEN_SCAN_ARG(&good_id), PROVEN_SCAN_ARG(&good_ratio));
if (proven_is_ok(ok)) published_id = good_id;
EXAMPLE_REQUIRE(published_id == 7, "publish only what a successful scan produced");
}
/* --- 입력이 모자랄 때, 그리고 남을 때 ---------------------------------- */
{
int a = 0, b = 0;
proven_err_t short_input = proven_scan_fmt(v("5"), "{} {}",
PROVEN_SCAN_ARG(&a), PROVEN_SCAN_ARG(&b));
EXAMPLE_REQUIRE(!proven_is_ok(short_input), "two placeholders, one value: that fails");
/* 뒤에 남은 입력은 오류가 *아니다*. 스캐너는 청한 것을 맞추고 멈췄다. 청하지
* 않은 것까지 단속하지는 않는다. 줄 전체를 삼켜야 한다면 그것은 여러분이
* 확인할 일이다. */
int only = 0;
proven_scan_t sc = proven_scan_init(v("7 8"));
proven_err_t err = proven_scan_fmt_cursor(&sc, "{}", PROVEN_SCAN_ARG(&only));
EXAMPLE_REQUIRE(proven_is_ok(err) && only == 7, "the first value scans");
EXAMPLE_REQUIRE(sc.cursor < sc.view.size, "and '8' is still sitting there, unconsumed");
}
/* --- 좁은 목적지는 범위가 검사된다 ------------------------------------ */
{
short small = 0;
proven_err_t err = proven_scan_fmt(v("70000"), "{}", PROVEN_SCAN_ARG(&small));
EXAMPLE_REQUIRE(err == PROVEN_ERR_OVERFLOW,
"70000 does not fit a short, and the scanner says so rather than truncating");
}
return EXAMPLE_OK();
}오용: 0x10이 16이라고 가정하기
그것은 0이다. 정수 스캐너들은 십진수 전용이며, x10은 여전히 입력에 남아 있다. hex가 필요하면, 그 자릿수 루프는 당신이 직접 작성하는 것이다.
오용: 남은 입력을 에러로 다루기
그것은 오류가 아니다. "7 8"에 대한 플레이스홀더 하나는 성공한다. 신경 쓰인다면 커서를 검사하라.
오용: 실패한 스캔 뒤의 목적지를 믿기
그것들은 오염되었다. §11.1을 참조하라.
오용: 입력이 사라진 뒤에도 스캔한 낱말을 들고 있기
proven_scan_str은 복사본이 아니라 입력 안을 가리키는 뷰를 반환한다. 버퍼가 사라지면, 그 단어도 사라진다. 그것이 온 바이트보다 오래 살아야 한다면 proven_u8str_create_from_view()로 복사하라.
13. 프리스탠딩과 빌드 모드 참고
float만 컴파일에서 빠지는 이유
이 장의 모든 것은 이식 가능한 계산이지만 한 부분만 예외이고, 그 부분은 유난히 비싸다.
double을 올바르게 포매팅하는 것 — 왕복(round-trip)하는 가장 짧은 십진 표현을, subnormal을 포함한 모든 입력에 대해 얻는 것 — 에는 큰 정수 연산과 조회 테이블이 필요하다. 포매터에서 단일 코드 조각으로는 가장 크며, 대부분의 펌웨어는 double을 아예 출력하지 않는다. 그래서 freestanding 프로파일은 PROVEN_FMT_NO_FLOAT를 설정해 그것을 덜어내고, 마이크로컨트롤러용 빌드는 결코 호출하지 않을 수 킬로바이트의 십진 변환 테이블을 지고 다니지 않는다.
이것이 런타임 결정이 아니라 빌드 타임 결정인 것은 의도적이다. 코드가 단지 도달 불가능한 것이 아니라 *부재*하므로, 링커를 설득해 남겨둘 수 없다. 작은 타깃에서도 float를 원하는 빌드를 위한 관련 손잡이 — 큰 정수 용량 — 는 freestanding 가이드 §8a가 다룬다.
스캐너는 코어다: I/O를 하지 않고, 아무것도 할당하지 않으며, 어떤 플랫폼 레이어도 건드리지 않는다. 따라서 freestanding 빌드에서도 호스티드(hosted) 빌드에서와 정확히 똑같이 사용할 수 있다.
유일한 빌드 모드 의존성은 float다. Freestanding 빌드는 PROVEN_FMT_NO_FLOAT(float *포매터*를 컴파일에서 제외)와 PROVEN_NO_U16STR를 설정한다. proven_scan_f64는 십진 파싱 엔진(float_parse.c와 float_decimal.c)을 끌어오는데, 이는 정수 전용이며
long double없음, libm 없음, 소프트 float 헬퍼 호출 없음 - 코드 크기 면에서
공짜는 아니다. 그것이 중요한 타겟이면서 float을 읽을 필요가 없다면, 그저 호출하지 마라: 스캐너의 다른 어떤 것도 float 경로를 참조하지 않는다.
proven_sysio_scanner_*(Chapter 5)는 다른 것이다: 파일 위의 버퍼드 스캐너로, I/O를 하기 때문에 hosted 전용이다. 이 장에서 설명한 스캐너는 당신이 이미 가지고 있는 바이트를 읽는다.
실전 예제: 더 넓은 수, 자유로운 텍스트, 부동소수점
앞의 예제들은 정해진 서식을 proven_i32로 읽어 들인다. 입력이 실제 데이터가 되는 순간 세 가지가 따라오며, 이 예제는 그것들을 한꺼번에 다룬다.
목적지 타입이 곧 계약이다. proven_scan_arg_i64(), proven_scan_arg_u32(), proven_scan_arg_u64()는 값이 들어갈 타입을 이름으로 말하고, 스캐너는 감기는 대신 넘침을 보고한다. 32비트로는 모자란 바이트 수와, 결코 음수일 수 없는 값 — 이 둘이 목적지 타입을 잘못 고른 것이 숫자가 커지기 전까지 아무도 모르는 버그가 되는 경우다. 일반 매크로 PROVEN_SCAN_ARG()는 넘겨받은 포인터로 이들 중 하나를 골라 주며, 이름 있는 형태는 그 매크로가 고르는 것을 손으로 적은 것이다.
모든 입력이 정해진 서식인 것은 아니다. proven_scan_skip_whitespace()는 공백·탭·줄바꿈을 지나가고, proven_scan_skip_until_number()는 다음 숫자 자리까지, 또는 바로 뒤에 숫자가 붙은 부호 자리까지 커서를 옮긴다. 음수가 부호를 잃지 않는 이유다. 둘 다 실패할 수 없다. "건너뛸 것이 없었다"는 에러가 아니며, 입력의 끝에서 커서는 넘어가지 않고 멈춘다.
부동소수점에는 로케일(locale) 문제가 있고, 이 라이브러리에는 없다. strtod()는 "3,5"를 어떤 로케일에서는 3.5로, 다른 로케일에서는 3으로 읽으므로 같은 프로그램이 기계마다 다른 말을 한다. proven_parse_double_ascii()는 구조적으로 로케일에서 자유롭다. 쉼표는 결코 소수점이 아니다. 이 함수는 숫자가 쓴 바이트 수를 알려 주어 호출자가 그 지점부터 이어 갈 수 있게 하고, 해석할 수 없는 텍스트를 진짜 0과 구분되지 않는 0이 아니라 에러로 보고한다. proven_parse_f64_ascii()는 같은 함수의 예전 이름이며, 기존 호출 자리가 그대로 읽히도록 남아 있다.
내보내는 쪽에서는 proven_arg_f64()가 부동소수점 값이 형식화기로 들어가는 통로다. float도 double도 이것을 지나므로, 출력이 그 값을 담아 둔 변수의 폭에 좌우되지 않는다. 정확한 표기가 중요할 때는 proven_float_format_f32_policy()와 그 f64 짝이 정책과 옵션을 명시적으로 받는다. 최단(shortest) 모드는 다시 읽었을 때 정확히 같은 값이 되는 가장 짧은 자릿수를 요구한다. 직렬화기가 원하는 성질이 이것이다. 필요 없는 수에까지 열일곱 자리를 찍지 않으면서 왕복을 정확하게 만든다.
#include <string.h>
/*
* 글에서 수를 읽어 내고, 다시 써 넣기.
*
* 여기 쓰인 글은 프로그램이 실제로 만나는 종류다. 폭과 부호가 뒤섞인 로그 줄, 그리고
* 관심 있는 수가 산문 속에 묻혀 있는 측정값 파일. 그래서 앞의 8장 예제들이 다루지 않는
* 세 가지 일이 나온다.
*
* - int 보다 *넓은* 타입, 그리고 부호 없는 타입으로 파싱하기. 여기서는 목적지 타입이
* 물음의 전부다 - 32비트에 들어가지 않는 바이트 수야말로 파일이 5 GB 가 될 때까지
* 아무도 알아채지 못하는 고전적인 넘침이다.
* - 입력이 빡빡한 형식이 아닐 때 커서를 손으로 옮기기 - 공백을 건너뛰거나, 다음 수가
* 시작하는 자리까지 건너뛰기.
* - 십진 문자열을 double 로, 그리고 다시 글로, C 라이브러리의 로케일에 매인 변환을
* 거치지 않고 정확하게 옮기기.
*
* 부동소수점 쪽은 들리는 것보다 중요하다. strtod 는 어느 로케일에서는 "3,5" 를 3.5 로,
* 다른 로케일에서는 3 으로 읽고, 그러면 같은 프로그램이 기계마다 서로 다른 소리를 한다.
* 여기 쓰인 파서는 만들어질 때부터 로케일이 없다. 환경이 무어라 하든 쉼표가 소수점이
* 되는 일은 없다.
*/
int main(void) {
/* --- 1. 값이 필요로 하는 타입으로 파싱하기 ---------------------------- */
/* 로그 줄 하나: 64비트가 필요한 요청 번호, 결코 음수가 아니고 4 GB 를 넘을 수 있는
* 바이트 수, 작은 상태 코드, 그리고 음수 오프셋. */
proven_scan_t scan = proven_scan_init(
PROVEN_LIT("id=9007199254740993 bytes=5368709120 status=404 delta=-17"));
proven_i64 id = 0;
proven_u64 bytes = 0;
proven_u32 status = 0;
proven_i32 delta = 0;
/* 인자 생성자마다 목적지 타입을 이름으로 밝히므로, 스캐너는 옳은 폭으로 쓰고 감기는
* 대신 넘침을 알린다. 일반 매크로 PROVEN_SCAN_ARG() 는 포인터의 타입에서 이것들을
* 대신 골라 준다. 이름 붙은 꼴은 그것이 고르는 것 자체이고, 그 선택을 눈에 보이게
* 하고 싶을 때 여러분이 적는 것이다. */
proven_err_t err = proven_scan_fmt_cursor(&scan, "id={} bytes={} status={} delta={}",
proven_scan_arg_i64(&id),
proven_scan_arg_u64(&bytes),
proven_scan_arg_u32(&status),
proven_scan_arg_i32(&delta));
EXAMPLE_REQUIRE(proven_is_ok(err), "scanning the log line must succeed");
EXAMPLE_REQUIRE(id == 9007199254740993LL, "a 64-bit id survives, which a 32-bit destination could not");
EXAMPLE_REQUIRE(bytes == 5368709120ULL, "and so does a byte count larger than 4 GiB");
EXAMPLE_REQUIRE(status == 404u, "the small unsigned value reads normally");
EXAMPLE_REQUIRE(delta == -17, "and a signed destination accepts the minus sign");
/* 목적지 타입은 힌트가 아니라 계약이다. 음수에는 부호 없는 표현이 없고, 스캐너는
* 그것을 어마어마한 값으로 감아 버리는 대신 거부한다. */
proven_scan_t neg = proven_scan_init(PROVEN_LIT("-17"));
proven_u32 nowhere = 12345;
err = proven_scan_fmt_cursor(&neg, "{}", proven_scan_arg_u32(&nowhere));
EXAMPLE_REQUIRE(err != PROVEN_OK, "a negative value cannot be scanned into an unsigned destination");
EXAMPLE_REQUIRE(nowhere == 12345, "and the destination is left as it was");
/* --- 2. 입력이 빡빡하지 않을 때 커서 옮기기 --------------------------- */
/* 자유로운 글: 수가 중요하고 그 사이의 낱말은 아니다. */
proven_scan_t notes = proven_scan_init(
PROVEN_LIT(" sample A measured 42 units; sample B measured -8 units"));
/* skip_whitespace 는 공백과 탭과 줄바꿈을 지나 나아간다. 손으로 몰고 가는 형식의 필드
* 사이에서 부르는 것이고, 실패하는 일이 없다. "공백이 없었다" 는 오류가 아니기
* 때문이다. */
proven_scan_skip_whitespace(¬es);
EXAMPLE_REQUIRE(notes.cursor == 3, "the three leading spaces are consumed");
/* skip_until_number 는 커서를 첫 숫자, 또는 곧바로 숫자가 따라오는 부호까지 몰고
* 간다. "이 줄에서 수를 찾아라" 를 손으로 쓴 반복문에서 한 문장으로 바꿔 주는
* 호출이다. */
proven_scan_skip_until_number(¬es);
proven_i32 first = 0;
err = proven_scan_fmt_cursor(¬es, "{}", proven_scan_arg_i32(&first));
EXAMPLE_REQUIRE(proven_is_ok(err) && first == 42, "the first number in the line is 42");
proven_scan_skip_until_number(¬es);
proven_i32 second = 0;
err = proven_scan_fmt_cursor(¬es, "{}", proven_scan_arg_i32(&second));
EXAMPLE_REQUIRE(proven_is_ok(err) && second == -8,
"and the next one keeps its sign, because the sign is part of the number");
/* 끝에 이르면 더 찾을 것이 없고, 커서는 달려 나가는 대신 멈춘다. 파싱이 망가진 것이
* 아니라 끝난 것이다. */
proven_scan_skip_until_number(¬es);
EXAMPLE_REQUIRE(notes.cursor == notes.view.size, "with no number left, the cursor lands at the end");
/* --- 3. 십진 글을 double 로, 로케일 없이 ----------------------------- */
proven_parse_double_result_t d = proven_parse_double_ascii(PROVEN_LIT("3.14159 rest"));
EXAMPLE_REQUIRE(proven_is_ok(d.err), "parsing a decimal number must succeed");
EXAMPLE_REQUIRE(d.val > 3.14158 && d.val < 3.14160, "and produce the value the digits spell");
/* consumed 는 수가 어디서 끝났는지 알려 주므로 부르는 쪽이 거기서 이어 갈 수 있다 -
* 목록을 토막으로 복사하지 않고 파싱하는 방법이 그것이다. */
EXAMPLE_REQUIRE(d.consumed == 7, "consumed reports exactly the bytes the number used");
/* 여기서 쉼표는 소수점이 아니고 앞으로도 아니다. 프로그램이 어느 로케일에서 돌든
* 마찬가지다. 수는 쉼표에서 끝난다. */
proven_parse_double_result_t comma = proven_parse_double_ascii(PROVEN_LIT("3,5"));
EXAMPLE_REQUIRE(proven_is_ok(comma.err) && comma.consumed == 1 && comma.val == 3.0,
"a comma ends the number: the parser is locale-free by construction");
/* proven_parse_f64_ascii 는 예전 이름을 단 같은 함수다. 지금 이름이 생기기 전에 쓰인
* 코드를 위해 남겨 두었다. 새 코드는 proven_parse_double_ascii 를 쓸 것. 옛 호출
* 자리도 그대로 읽히도록 둘 다 여기 있다. */
proven_parse_f64_result_t same = proven_parse_f64_ascii(PROVEN_LIT("3.14159 rest"));
EXAMPLE_REQUIRE(same.val == d.val && same.consumed == d.consumed,
"the compatibility name is the same parser");
/* 아예 수가 아닌 글은 조용한 0 이 아니라 오류다 - atof() 였다면 진짜 "0" 과 구별할
* 방법 없이 그것을 돌려주었을 것이다. */
proven_parse_double_result_t junk = proven_parse_double_ascii(PROVEN_LIT("not a number"));
EXAMPLE_REQUIRE(!proven_is_ok(junk.err), "unparsable text is reported, not turned into 0");
EXAMPLE_REQUIRE(junk.consumed == 0, "and nothing was consumed");
/* --- 4. float 를 다시 글로 ------------------------------------------- */
/* proven_arg_f64 가 부동소수점 값이 형식화기로 들어가는 길이다. float 도 double 도
* 그것을 지나므로, 프로그램이 찍는 자릿수가 그 값이 어쩌다 담겨 있던 변수의 폭에
* 좌우되지 않는다. */
proven_allocator_t alloc = proven_heap_allocator();
proven_result_u8str_t line = proven_u8str_create(alloc, 64);
EXAMPLE_REQUIRE(proven_is_ok(line.err), "creating the output string must succeed");
proven_fmt_result_t out = proven_u8str_append_fmt(&line.value, "measured {} units",
proven_arg_f64(d.val));
EXAMPLE_REQUIRE(proven_is_ok(out.err), "formatting the parsed value must succeed");
EXAMPLE_REQUIRE(proven_u8str_view_starts_with(proven_u8str_as_view(&line.value),
PROVEN_LIT("measured 3.14159")),
"and spell the number it was given");
/* 정책을 주는 꼴이 명시적인 쪽이다. 기본값이 아니라 특정한 표기가 필요할 때 쓴다.
* SHORTEST 는 다시 읽었을 때 정확히 이 값이 되는 가장 적은 자릿수를 청한다 -
* 직렬화기가 원하는 성질이 그것이다. 필요 없는 수까지 열일곱 자리로 찍지 않고도
* 왕복을 정확하게 만들어 주기 때문이다. */
char shortest[64];
proven_size_t wrote = 0;
float measured = 0.1f;
proven_err_t ferr = proven_float_format_f32_policy(shortest, sizeof shortest, measured,
PROVEN_FLOAT_FORMAT_POLICY_RYU,
proven_float_format_options_shortest(),
&wrote);
EXAMPLE_REQUIRE(proven_is_ok(ferr), "formatting a float in shortest mode must succeed");
EXAMPLE_REQUIRE(wrote > 0 && shortest[wrote] == '\0', "the result is written and terminated");
EXAMPLE_REQUIRE(strcmp(shortest, "0.1") == 0,
"0.1f prints as 0.1: the shortest text that reads back as the same float");
/* 그리고 왕복한다. 그 글은 자기가 나온 값으로 되읽힌다. 가장 짧은 방식이 존재하는
* 이유가 그 보장이다. */
proven_parse_double_result_t roundtrip = proven_parse_double_ascii(
(proven_u8str_view_t){ .ptr = (const proven_byte_t *)shortest, .size = wrote });
EXAMPLE_REQUIRE(proven_is_ok(roundtrip.err), "the shortest form parses back");
EXAMPLE_REQUIRE((float)roundtrip.val == measured, "as exactly the float it was printed from");
printf("scanned id=%lld bytes=%llu; formatted %s and %s\n",
(long long)id, (unsigned long long)bytes, proven_u8str_as_cstr(&line.value), shortest);
proven_u8str_destroy(alloc, &line.value);
return EXAMPLE_OK();
}반례 — 큰 값을 32비트 목적지로 읽는 경우:
proven_i32 bytes = 0;
proven_err_t e = proven_scan_fmt_cursor(&scan, "bytes={}", proven_scan_arg_i32(&bytes));
/* input says 5368709120 */ /* wrong type */값이 들어가지 않는다. 필드가 담을 수 있는 범위가 아니라 손에 익은 것으로 목적지 타입을 고르는 일은, 파일 오프셋을 int로 선언하는 것과 같은 실수다.
반례 — 기계를 건너다니는 데이터에 atof나 strtod를 쓰는 경우:
double v = strtod(text, NULL); /* wrong: the decimal point depends on the locale */게다가 atof는 실패를 보고할 방법이 아예 없다. 해석할 수 없는 텍스트가 0이 되고, 그것은 데이터 안의 진짜 0과 구분되지 않는다.
반례 — skip_until_number가 "없음"을 알려 준다고 가정하는 경우:
proven_scan_skip_until_number(&scan);
proven_i32 n = 0;
proven_err_t e = proven_scan_fmt_cursor(&scan, "{}", proven_scan_arg_i32(&n)); /* may fail */이 함수는 커서를 옮길 뿐 아무것도 돌려주지 않는다. 남은 숫자가 없으면 커서는 입력의 끝에 놓이고, 그 사실을 알려 주는 것은 뒤따르는 스캔이다. 숫자를 찾았다고 가정하는 대신 그 에러를 확인한다.
실전 예제: 인자 타입을 손으로 이름 붙이기
PROVEN_ARG(x)와 PROVEN_SCAN_ARG(&x)는 넘겨받은 것의 타입에서 인자 생성자를 골라 주며, 대부분의 코드는 무엇이 골라졌는지 알 필요가 없다. 그 선택을 도로 가져와야 하는 상황이 둘 있다.
- 매크로가 판단할 타입이 내 의도를 담고 있지 않을 때.
proven_u8str_view_t, 분해된 날짜, 진단용으로 찍는 주소 —proven_arg_str_view(),proven_arg_datetime(),proven_arg_ptr()가 있는 이유는 이들이 얼떨결에 빠지는 기본값이 아니라 의도적인 선택이기 때문이다. - 인자 목록을 실행 중에 만들 때. 매개변수가 이미
proven_arg_t인 로그 헬퍼는 그것을 다시 감쌀 수 없다. 값을 인자로 바꾸는 매크로는 이미 인자인 것과는 상관이 없다.proven_arg_identity()와proven_scan_arg_identity()가 바로 그 자리에서, 매크로로 굴러가는 같은 코드 경로가 미리 만들어진 인자를 받아들이게 해 준다.
예제는 양쪽을 이름으로 적는다. 형식화 쪽은 문자 하나, 불(bool) 값, 부호 없는 32·64비트 값, 텍스트를 넘기는 세 가지 방법(proven_arg_str_view(), proven_arg_cstr(), proven_arg_ucstr()), 날짜와 포인터. 스캔 쪽은 C의 모든 기본 정수 폭(short, int, long, long long과 그 부호 없는 형태), double, 그리고 복사 없이 붙잡는 문자열 view.
텍스트 생성자 셋은 호출자를 얼마나 믿는가에서 갈리며, 그 순서를 기억해 둘 값어치가 있다.
| 생성자 | 무엇을 받는가 | 무엇이 잘못될 수 있는가 |
proven_arg_str_view | 포인터 와 길이 | 종결자를 찾아 훑는 일이 없으니 빠질 종결자도 없다 — 기본으로 이것을 쓴다 |
proven_arg_cstr | NUL로 끝나는 C 문자열 | 종결자가 있어야 하고 메모리가 살아 있어야 한다. 아니면 형식화기가 끝을 넘어 읽는다 |
proven_arg_ucstr | 같은 것을 unsigned char *로 | 바이트 버퍼에 진짜 경고를 지우는 캐스트를 붙이지 않아도 되도록 존재한다 |
/*
* 형식화기에 건네는 모든 값은 proven_arg_t 로 도착하고, 스캐너가 써 넣는 모든 목적지는
* proven_scan_arg_t 로 도착한다. 대개는 둘 다 볼 일이 없다. PROVEN_ARG(x) 와
* PROVEN_SCAN_ARG(&x) 가 건넨 것의 타입에서 알맞은 생성자를 골라 주고, 그것이 그
* 매크로들의 존재 이유다.
*
* 두 상황에서는 그 선택을 매크로에게서 도로 가져오게 되는데, 둘 다 평범한 일이다.
*
* 1. 매크로가 알아볼 타입이 없을 때. `proven_u8str_view_t`, 쪼개 놓은 날짜, 진단용으로
* 찍는 날주소 - 이런 것들은 생성자를 이름으로 불러야 한다. 여러분이 뜻하는 바를
* 뜻하는, 매크로가 갈래를 태울 수 있는 평범한 C 타입이 없기 때문이다.
*
* 2. 인자 목록을 *실행 중에* 짓고 있을 때. "부르는 쪽이 이미 모아 둔 무엇이든" 을 받는
* 로그 도우미는 int 나 문자열이 아니라 proven_arg_t 값을 받는다 - 그리고 값을
* 인자로 바꾸는 매크로에 이미 인자인 것을 건넬 수는 없다. 항등 생성자가 그것을 위한
* 것이다. 매크로로 굴러가는 같은 코드 길이, 앞서 만들어진 인자도 받아들이게 해 준다.
*
* 그래서 이 예제는 양쪽을 다 이름으로 적는다. 목적지의 폭과 부호 유무는 장식이 아니다 -
* 70000 이 70000 으로 도착할지 4464 로 도착할지를 정하는 것이 그것이다.
*/
/* 부르는 쪽이 이미 만들어 둔 인자를 받는 로그 도우미. 매개변수가 proven_arg_t 이므로 그
* 안에서 PROVEN_ARG 는 틀린 도구다 - 그 값은 이미 인자이고, 그렇다고 말해 주는 것이
* proven_arg_identity 다. */
static proven_fmt_result_t log_pair(proven_u8str_t *out, const char *fmt,
proven_arg_t a, proven_arg_t b) {
return proven_u8str_append_fmt(out, fmt, proven_arg_identity(a), proven_arg_identity(b));
}
int main(void) {
proven_allocator_t alloc = proven_heap_allocator();
proven_result_u8str_t line = proven_u8str_create(alloc, 256);
EXAMPLE_REQUIRE(proven_is_ok(line.err), "creating the output string must succeed");
if (!proven_is_ok(line.err)) {
return 1;
}
proven_u8str_t out = line.value;
/* --- 형식화 인자를 직접 이름으로 부르기 ------------------------------- */
/* 문자 하나와 플래그 하나. bool 이 1/0 이 아니라 true/false 로 찍힌다는 것이 눈에
* 보이도록 여기서는 이름으로 적었다. */
proven_fmt_result_t r = proven_u8str_append_fmt(&out, "flag={} mark={}",
proven_arg_bool(true),
proven_arg_char('!'));
EXAMPLE_REQUIRE(proven_is_ok(r.err), "formatting a bool and a char must succeed");
EXAMPLE_REQUIRE(proven_u8str_view_eq(proven_u8str_as_view(&out), PROVEN_LIT("flag=true mark=!")),
"a bool renders as a word, not as a digit");
EXAMPLE_REQUIRE(proven_is_ok(proven_u8str_reset(&out)), "clearing the line must succeed");
/* 부호 없는 폭들. 생성자가 그 값이 무엇인지에 대한 선언이다. 부호 없는 32비트 개수와
* 부호 없는 64비트 바이트 합계는 서로 다른 사실이고, 그것을 적어 두면 자기가 가진
* 것이 어느 쪽인지 말하게 된다. */
r = proven_u8str_append_fmt(&out, "files={} bytes={}",
proven_arg_u32(1200u),
proven_arg_u64(5368709120ULL));
EXAMPLE_REQUIRE(proven_is_ok(r.err), "formatting unsigned values must succeed");
EXAMPLE_REQUIRE(proven_u8str_view_eq(proven_u8str_as_view(&out), PROVEN_LIT("files=1200 bytes=5368709120")),
"a 64-bit total is printed in full, not truncated to 32 bits");
EXAMPLE_REQUIRE(proven_is_ok(proven_u8str_reset(&out)), "clearing the line must succeed");
/* 형식화기에 글을 주는 세 가지 방법. 부르는 쪽을 얼마나 믿는지 순서대로.
*
* proven_arg_str_view - 포인터 *와* 길이. 종결자를 찾아 훑는 일이 없으므로,
* 없어서 문제가 될 종결자도 없다. 이것을 고를 것.
* proven_arg_cstr - NUL 로 끝나는 C 문자열. 형식화기가 종결자를 찾아 걸으므로
* 그것이 반드시 있어야 하고, 그 기억도 살아 있어야 한다.
* proven_arg_ucstr - 같은 것인데 `unsigned char *` 용이다. 바이트 버퍼는 보통
* 그 타입으로 적힌다. 부르는 쪽이 진짜 경고를 입막음하는
* 형변환을 쓰지 않아도 되도록 있는 것이다.
*/
const char *name = "report.txt";
const unsigned char *tag = (const unsigned char *)"draft";
proven_u8str_view_t note = PROVEN_LIT("first pass");
r = proven_u8str_append_fmt(&out, "{} [{}] {}",
proven_arg_cstr(name),
proven_arg_ucstr(tag),
proven_arg_str_view(note));
EXAMPLE_REQUIRE(proven_is_ok(r.err), "formatting the three text forms must succeed");
EXAMPLE_REQUIRE(proven_u8str_view_eq(proven_u8str_as_view(&out),
PROVEN_LIT("report.txt [draft] first pass")),
"all three produce the same kind of output from different kinds of pointer");
EXAMPLE_REQUIRE(proven_is_ok(proven_u8str_reset(&out)), "clearing the line must succeed");
/* 날짜 하나와 주소 하나. 둘 다 자기 뜻을 뜻하는 평범한 C 타입이 없다. 날짜는
* 구조체이고, 진단용으로 찍는 주소는 우연히 빠지는 것이 아니라 일부러 하는 일이다. */
proven_datetime_t when = proven_time_breakdown(0); /* 기점: 고정된, 확인할 수 있는 값 */
int local = 0;
r = proven_u8str_append_fmt(&out, "at {} object {}",
proven_arg_datetime(when),
proven_arg_ptr(&local));
EXAMPLE_REQUIRE(proven_is_ok(r.err), "formatting a date and a pointer must succeed");
EXAMPLE_REQUIRE(proven_u8str_view_starts_with(proven_u8str_as_view(&out), PROVEN_LIT("at 1970-01-01")),
"the epoch breaks down to the first of January 1970");
EXAMPLE_REQUIRE(proven_u8str_view_find(proven_u8str_as_view(&out), 0, PROVEN_LIT("0x")) != PROVEN_SIZE_MAX,
"and an address is rendered in hexadecimal");
EXAMPLE_REQUIRE(proven_is_ok(proven_u8str_reset(&out)), "clearing the line must succeed");
/* 여기서 지은 인자를, 넘겨, 저기서 형식화한다. */
r = log_pair(&out, "status={} retries={}", proven_arg_cstr("ok"), proven_arg_u32(3u));
EXAMPLE_REQUIRE(proven_is_ok(r.err), "formatting pre-built arguments must succeed");
EXAMPLE_REQUIRE(proven_u8str_view_eq(proven_u8str_as_view(&out), PROVEN_LIT("status=ok retries=3")),
"an argument built by the caller formats exactly as one built in place");
/* --- 파싱 목적지를 직접 이름으로 부르기 ------------------------------- */
/* 들어오는 쪽에서는 생성자가 목적지를 이름으로 밝히고, 목적지가 "너무 크다" 의 뜻을
* 정한다. 이것들은 평범한 C 타입들 - short, int, long, long long 과 그 부호 없는 꼴 -
* 이고, 이미 가진 변수가 고정 폭 타입이 아니라 그중 하나인 아주 흔한 경우를 위한
* 것이다. */
proven_scan_t scan = proven_scan_init(
PROVEN_LIT("h=-32000 uh=65000 i=-2000000 ui=4000000000 l=-9000000 ul=9000000 "
"ll=-9007199254740993 ull=18446744073709551615 f=2.5 word=alpha"));
short h = 0;
unsigned short uh = 0;
int i = 0;
unsigned int ui = 0;
long l = 0;
unsigned long ul = 0;
long long ll = 0;
unsigned long long ull = 0;
double f = 0.0;
proven_u8str_view_t word = {0};
proven_err_t err = proven_scan_fmt_cursor(
&scan, "h={} uh={} i={} ui={} l={} ul={} ll={} ull={} f={} word={}",
proven_scan_arg_short(&h),
proven_scan_arg_ushort(&uh),
proven_scan_arg_int(&i),
proven_scan_arg_uint(&ui),
proven_scan_arg_long(&l),
proven_scan_arg_ulong(&ul),
proven_scan_arg_llong(&ll),
proven_scan_arg_ullong(&ull),
proven_scan_arg_f64(&f),
proven_scan_arg_str_view(&word));
EXAMPLE_REQUIRE(proven_is_ok(err), "scanning every plain integer width must succeed");
EXAMPLE_REQUIRE(h == -32000 && uh == 65000u, "the short forms hold their values");
EXAMPLE_REQUIRE(i == -2000000 && ui == 4000000000u, "and so do the int forms");
EXAMPLE_REQUIRE(l == -9000000L && ul == 9000000UL, "and the long forms");
EXAMPLE_REQUIRE(ll == -9007199254740993LL, "a value needing 64 bits arrives whole");
EXAMPLE_REQUIRE(ull == 18446744073709551615ULL, "including the largest unsigned 64-bit value");
EXAMPLE_REQUIRE(f > 2.4999 && f < 2.5001, "the floating-point destination reads a decimal");
/* 파싱된 문자열 뷰는 파싱 중인 글 *안*을 가리킨다. 복사한 것도 할당한 것도 없으므로,
* 그 글이 사는 동안 정확히 그만큼만 쓸 수 있다 - 파싱보다 오래 살아야 한다면
* proven_u8str_t 로 복사할 것. */
EXAMPLE_REQUIRE(proven_u8str_view_eq(word, PROVEN_LIT("alpha")), "the word is captured as a view");
/* 목적지의 폭은 스캐너가 지키는 약속이다. 70000 은 short 에 들어가지 않으므로 4464 로
* 감기는 대신 거부된다 - 형변환이었다면 아무도 다시 보지 않을 파일 안에서 조용히
* 그렇게 만들어 냈을 값이다. */
proven_scan_t narrow = proven_scan_init(PROVEN_LIT("70000"));
short too_small = 7;
err = proven_scan_fmt_cursor(&narrow, "{}", proven_scan_arg_short(&too_small));
EXAMPLE_REQUIRE(err != PROVEN_OK, "a value that does not fit the destination is refused");
EXAMPLE_REQUIRE(too_small == 7, "and the destination keeps the value it had");
/* 파싱 쪽 항등 생성자. 이유는 형식화 쪽과 같다. 이미 만들어진 파싱 인자를 받는
* 도우미는 그것을 다시 감쌀 수 없다. */
proven_scan_t again = proven_scan_init(PROVEN_LIT("41"));
proven_i32 answer = 0;
proven_scan_arg_t prebuilt = proven_scan_arg_i32(&answer);
err = proven_scan_fmt_cursor(&again, "{}", proven_scan_arg_identity(prebuilt));
EXAMPLE_REQUIRE(proven_is_ok(err) && answer == 41, "a pre-built scan argument works unchanged");
printf("arguments: %s\n", proven_u8str_as_cstr(&out));
proven_u8str_destroy(alloc, &out);
return EXAMPLE_OK();
}반례 — 이미 인자인 것을 다시 감싸는 경우:
static proven_fmt_result_t log_one(proven_u8str_t *out, proven_arg_t a) {
return proven_u8str_append_fmt(out, "{}", PROVEN_ARG(a)); /* wrong */
}proven_arg_identity(a)를 쓴다. PROVEN_ARG는 값을 위한 것이고, a는 이미 인자다.
반례 — 텍스트가 사라진 뒤에도 스캔한 view를 들고 있는 경우:
proven_u8str_view_t word = {0};
{
proven_u8str_t line = read_a_line(alloc);
proven_scan_t s = proven_scan_init(proven_u8str_as_view(&line));
proven_err_t e = proven_scan_fmt_cursor(&s, "word={}", proven_scan_arg_str_view(&word));
proven_u8str_destroy(alloc, &line); /* wrong: `word` pointed into `line` */
}
use(word);스캔한 view는 스캔 대상 텍스트 안쪽을 가리킨다. 그 텍스트보다 오래 살아야 한다면 proven_u8str_t로 복사한다.