사용자 정의 리터럴(User-defined literals)
사용자 정의 리터럴(User-defined literals)
123이나 3.14 같은 리터럴은 그냥 숫자일 뿐이죠. 그런데 90.0_deg_to_rad처럼, 리터럴에 내가 만든 접미사를 붙여서 사용자 정의 타입의 객체를 만들고 싶다면? C++11부터는 **사용자 정의 리터럴(user-defined literal)**로 가능해요. 사용자 정의 접미사(user-defined suffix)를 정의해서 정수·부동소수점·문자·문자열 리터럴이 사용자 정의 타입의 객체를 만들게 할 수 있어요.
출처: cppreference
본문
문법
사용자 정의 리터럴은 다음 형태 중 하나의 표현식이에요.
| decimal-literal ud-suffix | (1) | |
| octal-literal ud-suffix | (2) | |
| hex-literal ud-suffix | (3) | |
| binary-literal ud-suffix | (4) | |
| fractional-constant exponent-part (optional) ud-suffix | (5) | |
| digit-sequence exponent-part ud-suffix | (6) | |
| character-literal ud-suffix | (7) | |
| string-literal ud-suffix | (8) |
| 자리 | 의미 |
|---|---|
| decimal-literal | 정수 리터럴과 동일. 0이 아닌 십진수 하나 뒤에 십진수 0개 이상 |
| octal-literal | 정수 리터럴과 동일. 0 뒤에 8진수 0개 이상 |
| hex-literal | 정수 리터럴과 동일. 0x 또는 0X 뒤에 16진수 1개 이상 |
| binary-literal | 정수 리터럴과 동일. 0b 또는 0B 뒤에 2진수 1개 이상 |
| digit-sequence | 부동소수점 리터럴과 동일. 십진수 숫자들의 시퀀스 |
| fractional-constant | 부동소수점 리터럴과 동일. 숫자 시퀀스 뒤에 점(123.), 또는 선택적인 숫자 시퀀스 뒤에 점과 다른 숫자 시퀀스(1.0 또는 .12) |
| exponent-part | 부동소수점 리터럴과 동일. 문자 e 또는 E 뒤에 선택적 부호, 그 뒤에 숫자 시퀀스 |
| character-literal | 문자 리터럴과 동일 |
| string-literal | 문자열 리터럴과 동일. 원시 문자열 리터럴(raw string literal) 포함 |
| ud-suffix | 리터럴 연산자나 리터럴 연산자 템플릿 선언이 도입한 식별자 (아래 참고) |
정수·부동소수점 숫자 시퀀스에서는 어느 두 숫자 사이에든 선택적 구분 문자 '를 넣을 수 있어요. (C++14부터)
토큰이 사용자 정의 리터럴 문법과 일반 리터럴 문법 둘 다에 맞으면, 일반 리터럴로 간주돼요. (즉 123LL에서 LL을 오버로드하는 것은 불가능해요.)
컴파일러가 ud-suffix가 X인 사용자 정의 리터럴을 만나면, operator""X라는 이름의 함수를 찾는 비한정 이름 검색을 수행해요. 검색이 선언을 찾지 못하면 프로그램은 잘못된 형태(ill-formed)예요. 그렇지 않으면,
- a) 오버로드 집합이
str을 잘 형성된 템플릿 인자로 받는 비타입 템플릿 매개변수를 가진 문자열 리터럴 연산자 템플릿을 포함하면, 사용자 정의 리터럴 표현식은 함수 호출operator ""X<str>()로 취급돼요. (C++20부터)
아래 예는 리터럴 연산자 오버로드가 어떻게 선택되는지 보여줘요.
long double operator ""_w(long double);
std::string operator ""_w(const char16_t*, size_t);
unsigned operator ""_w(const char*);
int main()
{
1.2_w; // operator ""_w(1.2L) 호출
u"one"_w; // operator ""_w(u"one", 3) 호출
12_w; // operator ""_w("12") 호출
"two"_w; // 오류: 적용 가능한 리터럴 연산자가 없음
}
번역 6단계에서 문자열 리터럴 연결이 일어날 때, 사용자 정의 문자열 리터럴도 함께 연결되고, ud-suffix는 연결 목적으로 무시돼요. 단, 연결된 모든 리터럴에 접미사가 하나만 나타나야 해요.
int main()
{
L"A" "B" "C"_x; // OK: L"ABC"_x와 같음
"P"_x "Q" "R"_y; // 오류: 두 개의 다른 ud-suffix (_x와 _y)
}
리터럴 연산자(Literal operators)
사용자 정의 리터럴이 호출하는 함수를 **리터럴 연산자(literal operator)**라고 불러요. (템플릿이면 리터럴 연산자 템플릿) 네임스페이스 스코프의 다른 함수나 함수 템플릿처럼 선언돼요. (friend 함수, 함수 템플릿의 명시적 인스턴스화나 특수화, using 선언으로 도입된 함수여도 돼요.) 단, 다음 제약이 있어요.
이 함수의 이름은 두 형태 중 하나를 가질 수 있어요.
operator "" identifier |
(1) | (비권장) |
operator user-defined-string-literal |
(2) |
| 자리 | 의미 |
|---|---|
| identifier | 이 함수를 호출할 사용자 정의 리터럴의 ud-suffix로 쓸 식별자 |
| user-defined-string-literal | 공백 없이 "" 뒤에 ud-suffix가 되는 문자 시퀀스 |
ud-suffix는 밑줄 _로 시작해야 해요. 밑줄로 시작하지 않는 접미사는 표준 라이브러리가 제공하는 리터럴 연산자를 위해 예약돼요. 또한 이중 밑줄 __을 포함할 수 없어요. 그런 접미사도 예약돼요.
리터럴 연산자가 템플릿이면, 빈 매개변수 목록을 가져야 하고 템플릿 매개변수를 하나만 가질 수 있어요. 그것은 요소 타입이 char인 비타입 템플릿 매개변수 팩이어야 하는데, (이 경우 **숫자 리터럴 연산자 템플릿(numeric literal operator template)**이라고 불러요).
template<char...>
double operator ""_x();
또는 클래스 타입의 비타입 템플릿 매개변수여야 해요. (이 경우 **문자열 리터럴 연산자 템플릿(string literal operator template)**이라고 불러요.)
struct A { constexpr A(const char*); };
template<A a>
A operator ""_a();
(C++20부터)
리터럴 연산자에는 다음 매개변수 목록만 허용돼요.
( const char* ) |
(1) | |
( unsigned long long int ) |
(2) | |
( long double ) |
(3) | |
( char ) |
(4) | |
( wchar_t ) |
(5) | |
( char8_t ) |
(6) | (C++20부터) |
( char16_t ) |
(7) | |
( char32_t ) |
(8) | |
( const char*, std::size_t ) |
(9) | |
( const wchar_t*, std::size_t ) |
(10) | |
( const char8_t*, std::size_t ) |
(11) | (C++20부터) |
( const char16_t*, std::size_t ) |
(12) | |
( const char32_t*, std::size_t ) |
(13) |
기본 인자(default argument)는 허용되지 않아요.
C 언어 연결(C language linkage)은 허용되지 않아요.
위 제약 외에는, 리터럴 연산자와 리터럴 연산자 템플릿은 일반 함수(와 함수 템플릿)예요. inline이나 constexpr로 선언될 수 있고, 내부 또는 외부 연결을 가질 수 있으며, 명시적으로 호출될 수 있고, 주소를 얻을 수 있는 등 그렇지요.
#include <string>
void operator ""_km(long double); // OK, 1.0_km에 대해 호출됨
void operator "" _km(long double); // 위와 동일, 비권장
std::string operator ""_i18n(const char*, std::size_t); // OK
template<char...>
double operator ""_pi(); // OK
float operator ""_e(const char*); // OK
// 오류: 접미사는 밑줄로 시작해야 함
float operator ""Z(const char*);
// 오류: 밑줄 뒤 대문자로 시작하는 모든 이름은 예약됨
// (주의: ""와 _ 사이에 공백)
double operator"" _Z(long double);
// OK. (주의: ""와 _ 사이에 공백 없음)
double operator""_Z(long double);
// OK: 리터럴 연산자는 오버로드될 수 있음
double operator ""_Z(const char* args);
int main() {}
주의할 점(Notes)
사용자 정의 리터럴이 도입된 이후, 앞의 문자열 리터럴 뒤에 공백 없이 고정 폭 정수 타입용 형식 매크로 상수를 쓰는 코드는 유효하지 않게 됐어요. std::printf("%"PRId64"\n",INT64_MIN);는 std::printf("%" PRId64"\n",INT64_MIN);로 바꿔야 해요.
최대 뭉침(maximal munch) 때문에, p, P,(C++17부터) e, E로 끝나는 사용자 정의 정수·부동소수점 리터럴은 그 뒤의 +나 - 연산자와 소스에서 공백이나 괄호로 분리해야 해요.
long double operator""_E(long double);
long double operator""_a(long double);
int operator""_p(unsigned long long);
auto x = 1.0_E+2.0; // 오류
auto y = 1.0_a+2.0; // OK
auto z = 1.0_E +2.0; // OK
auto q = (1.0_E)+2.0; // OK
auto w = 1_p+2; // 오류
auto u = 1_p +2; // OK
정수·부동소수점 사용자 정의 리터럴 뒤에 오는 점 연산자에도 같은 규칙이 적용돼요.
#include <chrono>
using namespace std::literals;
auto a = 4s.count(); // 오류
auto b = 4s .count(); // OK
auto c = (4s).count(); // OK
그 외의 경우, 하나의 유효하지 않은 전처리 숫자 토큰(예: 1.0_E+2.0 또는 4s.count)이 형성되어 컴파일이 실패해요.
| 기능 테스트 매크로 | 값 | 표준 | 기능 |
|---|---|---|---|
__cpp_user_defined_literals |
200809L | (C++11) | 사용자 정의 리터럴 |
키워드
operator
예제
여기서는 리터럴 연산자를 세 가지 용도로 쓰는 걸 볼게요. 각도→라디안 변환, 사용자 정의 타입 생성, 그리고 출력 같은 부작용 수행까지요.
#include <algorithm>
#include <cstddef>
#include <iostream>
#include <numbers>
#include <string>
// 각도(입력)를 라디안(출력)으로 변환하는 데 사용
constexpr long double operator""_deg_to_rad(long double deg)
{
long double radians = deg * std::numbers::pi_v<long double> / 180;
return radians;
}
// 사용자 정의 타입과 함께 사용
struct mytype
{
unsigned long long m;
};
constexpr mytype operator""_mytype(unsigned long long n)
{
return mytype{n};
}
// 부작용 수행에 사용
void operator""_print(const char* str)
{
std::cout << str << '\n';
}
#if __cpp_nontype_template_args < 201911
std::string operator""_x2 (const char* str, std::size_t)
{
return std::string{str} + str;
}
#else // C++20 문자열 리터럴 연산자 템플릿
template<std::size_t N>
struct DoubleString
{
char p[N + N - 1]{};
constexpr DoubleString(char const(&pp)[N])
{
std::ranges::copy(pp, p);
std::ranges::copy(pp, p + N - 1);
}
};
template<DoubleString A>
constexpr auto operator""_x2()
{
return A.p;
}
#endif // C++20
int main()
{
double x_rad = 90.0_deg_to_rad;
std::cout << std::fixed << x_rad << '\n';
mytype y = 123_mytype;
std::cout << y.m << '\n';
0x123ABC_print;
std::cout << "abc"_x2 << '\n';
}
출력:
1.570796
123
0x123ABC
abcabc
표준 라이브러리(Standard library)
표준 라이브러리는 다음 리터럴 연산자를 정의해요.
std::literals::complex_literals 인라인 네임스페이스에 정의됨 |
|
|---|---|
operator""if, operator""i, operator""il(C++14) |
순수 허수를 나타내는 std::complex 리터럴 |
std::literals::chrono_literals 인라인 네임스페이스에 정의됨 |
|
operator""h(C++14) |
시간을 나타내는 std::chrono::duration 리터럴 |
operator""min(C++14) |
분을 나타내는 std::chrono::duration 리터럴 |
operator""s(C++14) |
초를 나타내는 std::chrono::duration 리터럴 |
operator""ms(C++14) |
밀리초를 나타내는 std::chrono::duration 리터럴 |
operator""us(C++14) |
마이크로초를 나타내는 std::chrono::duration 리터럴 |
operator""ns(C++14) |
나노초를 나타내는 std::chrono::duration 리터럴 |
operator""y(C++20) |
특정 연도를 나타내는 std::chrono::year 리터럴 |
operator""d(C++20) |
날(day)을 나타내는 std::chrono::day 리터럴 |
std::literals::string_literals 인라인 네임스페이스에 정의됨 |
|
operator""s(C++14) |
문자 배열 리터럴을 basic_string으로 변환 |
std::literals::string_view_literals 인라인 네임스페이스에 정의됨 |
|
operator""sv(C++17) |
문자 배열 리터럴의 문자열 뷰를 만듦 |
결함 보고(Defect reports)
이전에 발표된 C++ 표준들에 소급 적용된 동작 변경 결함 보고들이 있어요.
| DR | 적용 대상 | 발표 당시 동작 | 올바른 동작 |
|---|---|---|---|
| CWG 1473 | C++11 | 리터럴 연산자 선언에서 ""와 ud-suffix 사이 공백이 요구됐음 |
선택 사항으로 만듦 |
| CWG 1479 | C++11 | 리터럴 연산자가 기본 인자를 가질 수 있었음 | 금지됨 |
| CWG 2521 | C++11 | operator"" _Bq는 예약 식별자 _Bq를 써서 (진단 없이) 잘못된 형태였음 |
""와 ud-suffix 사이 공백이 있는 리터럴 연산자 문법을 비권장 처리 |
더 알아보기
- 리터럴: 정수, 부동소수점, 문자, 문자열 리터럴의 기본 형태.
- 사용자 정의 변환: 타입 사이의 변환을 정의하는 방법.
<chrono>리터럴:std::chrono가 제공하는 시간 단위 접미사.