복합 리터럴

복합 리터럴 (Compound literal)

구조체나 배열처럼 다루기 번거로운 타입의 객체를 그 자리에서 바로 만들고 싶을 때가 있어요. 복합 리터럴(compound literal) 은 지정한 타입의 이름 없는 객체를 그 자리에서 생성해 주는 문법이에요. 이 페이지에서 복합 리터럴이 어떻게 생겼고, 어떤 특성을 가지는지 살펴볼게요.

출처: cppreference

본문

구문 (Syntax)

( storage-class-specifiers (optional)(since C23) type ) { initializer-list }   (since C99)
( storage-class-specifiers (optional)(since C23) type ) { initializer-list , } (since C99)
( storage-class-specifiers (optional) type ) { }                              (since C23)

여기서

항목 설명
storage-class-specifiers (since C23) constexpr, static, register, thread_local만 담을 수 있는 저장 클래스 지정자 목록
type 완전 객체 타입(complete object type)이나 크기를 알 수 없는 배열을 지정하는 타입 이름. 단 VLA는 아님
initializer-list type 타입의 객체 초기화에 적합한 초기화 목록

설명 (Explanation)

복합 리터럴 표현식은 type이 지정하는 타입의 이름 없는 객체를 만들고, initializer-list가 지정한 대로 초기화해요. 지정 초기화(designated initializers)도 허용돼요.

복합 리터럴의 타입은 지정한 type이 돼요. 다만 type이 크기를 알 수 없는 배열이라면 그 크기는 배열 초기화에서처럼 초기화 목록으로부터 추론돼요.

복합 리터럴의 값 범주(value category)는 lvalue예요. 그래서 그 주소를 취할 수 있어요.

복합 리터럴이 평가되는 이름 없는 객체는 파일 스코프에서 나타나면 정적 저장 기간(static storage duration)을, 블록 스코프에서 나타나면 자동 저장 기간(automatic storage duration)을 가져요(이 경우 객체의 수명은 둘러싸는 블록의 끝에서 끝나요). (until C23)
복합 리터럴이 함수 본문 밖과 어떤 매개변수 목록 밖에서 평가되면 파일 스코프와 연관돼요. 그 외에는 둘러싸는 블록과 연관돼요. 이 연관에 따라 저장 클래스 지정자(빈 목록일 수 있음), 타입 이름, 초기화 목록은 각각 파일 스코프나 블록 스코프에서 아래 형태의 객체 정의에 유효한 지정자여야 해요: storage-class-specifiers typeof( type ) ID = { initializer-list } ; 여기서 ID는 전체 프로그램에서 고유한 식별자예요. 복합 리터럴은 그 값·타입·저장 기간·다른 속성이 위 정의 문법으로 준 것과 같은 이름 없는 객체를 제공해요. 저장 기간이 자동이면 그 이름 없는 객체 인스턴스의 수명은 둘러싸는 블록의 현재 실행이에요. 저장 클래스 지정자에 constexpr, static, register, thread_local 외의 지정자가 있으면 동작은 정의되지 않아요. (since C23)

주의 (Notes)

const 한정 타입의 복합 리터럴은 저장 공간을 공유할 수 있어요. const 한정 문자나 와이드 문자 배열 타입의 복합 리터럴은 문자열 리터럴과 저장 공간을 공유할 수 있어요.

&(const int){7} == &(const int){7} // 1일 수도 0일 수도 있음, 불특정(unspecified)
(const char []){"abc"} == "abc"    // 1일 수도 0일 수도 있음, 불특정

각 복합 리터럴은 그 스코프에서 단 하나의 객체만 만들어요:

#include <assert.h>

int main(void)
{
    struct S
    {
        int i;
    }
    *p = 0, *q;
    int j = 0;
again:
    q = p,
    p = &((struct S){ j++ }); // 타입 S의 이름 없는 객체를 만들고,
                              // j가 예전에 보유하던 값으로 초기화한 뒤
                              // 이 이름 없는 객체의 주소를 포인터 p에 대입
    if (j < 2)
        goto again; // 주의: 루프를 썼다면 여기서 스코프가 끝나,
                    // 복합 리터럴의 수명도 끝나 p가 댕글링 포인터가 됨
    assert(p == q && q->i == 1);
}

복합 리터럴은 이름이 없기 때문에 자기 자신을 참조할 수 없어요(이름 있는 구조체는 자기 자신을 가리키는 포인터를 포함할 수 있어요).

복합 리터럴의 문법은 캐스트(cast)와 비슷해 보이지만 중요한 차이가 있어요. 캐스트는 비-lvalue 표현식인 반면, 복합 리터럴은 lvalue라는 점이에요.

예제 (Example)

#include <stdio.h>

int *p = (int[]){2, 4}; // 타입 int[2]의 이름 없는 정적 배열을 만들고
                        // 배열을 {2, 4} 값으로 초기화
                        // 배열의 첫 요소를 가리키는 포인터 p를 만듦
const float *pc = (const float []){1e0, 1e1, 1e2}; // 읽기 전용 복합 리터럴

struct point {double x,y;};

int main(void)
{
    int n = 2, *p = &n;
    p = (int [2]){*p}; // 타입 int[2]의 이름 없는 자동 배열을 만들고
                       // 첫 요소를 *p가 예전에 보유하던 값으로 초기화
                       // 두 번째 요소를 0으로 초기화
                       // 첫 요소의 주소를 p에 저장

    void drawline1(struct point from, struct point to);
    void drawline2(struct point *from, struct point *to);
    drawline1(
        (struct point){.x=1, .y=1},  // 블록 스코프의 구조체 두 개를 만들어
        (struct point){.x=3, .y=4}); // 값으로 전달하며 drawline1을 호출
    drawline2(
        &(struct point){.x=1, .y=1},  // 블록 스코프의 구조체 두 개를 만들어
        &(struct point){.x=3, .y=4}); // 그 주소를 전달하며 drawline2를 호출
}

void drawline1(struct point from, struct point to)
{
    printf("drawline1: `from` @ %p {%.2f, %.2f}, `to` @ %p {%.2f, %.2f}\n",
        (void*)&from, from.x, from.y, (void*)&to, to.x, to.y);
}

void drawline2(struct point *from, struct point *to)
{
    printf("drawline2: `from` @ %p {%.2f, %.2f}, `to` @ %p {%.2f, %.2f}\n",
        (void*)from, from->x, from->y, (void*)to, to->x, to->y);
}

가능한 출력:

drawline1: `from` @ 0x7ffd24facea0 {1.00, 1.00}, `to` @ 0x7ffd24face90 {3.00, 4.00}
drawline2: `from` @ 0x7ffd24facec0 {1.00, 1.00}, `to` @ 0x7ffd24faced0 {3.00, 4.00}

더 알아보기