사용자 정의 리터럴(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-suffixX인 사용자 정의 리터럴을 만나면, 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가 제공하는 시간 단위 접미사.