복합 리터럴(Compound literal) — 이름 없는 객체 만들기

복합 리터럴(Compound literal) — 이름 없는 객체 만들기

함수에 구조체를 넘길 때마다 일일이 변수를 선언하고 초기화하기 번거로울 때가 있어요. 그때 쓰는 것이 **복합 리터럴(compound literal)**이에요. 타입을 괄호로 감싸고 그 뒤에 초기화 목록을 붙이는 (struct point){.x=1, .y=2} 같은 형태로, 이름 없는 객체를 그 자리에서 만들고 초기화해요. 캐스트와 겉모습이 비슷하지만, 캐스트가 비-좌측값인 데 비해 복합 리터럴은 **좌측값(lvalue)**이라는 점이 큰 차이예요.

출처: cppreference

본문

지정된 타입(구조체, 공용체, 심지어 배열 타입일 수도 있어요)의 이름 없는 객체를 그 자리( in-place)에서 생성해요.

문법(Syntax)

( storage-class-specifiers(optional, since C23) type ) { initializer-list }        (1)  (since C99)
( storage-class-specifiers(optional, since C23) type ) { initializer-list , }      (2)  (since C99)
( storage-class-specifiers(optional) type ) { }                                     (3)  (since C23)
  • storage-class-specifiers — (C23부터) constexpr, static, register, thread_local만 담을 수 있는 저장 클래스 지정자 목록.
  • type — 어떤 완전 객체 타입(type name)이거나 크기를 알 수 없는 배열이지만, VLA는 아니어야 해요.
  • initializer-list — 해당 타입의 객체 초기화에 적합한 초기화자 목록.

설명(Explanation)

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

복합 리터럴의 타입은 type이에요(크기를 알 수 없는 배열인 경우는 예외 — 그때는 배열 초기화에서처럼 크기가 initializer-list로부터 추론돼요).

복합 리터럴의 값 범주는 **좌측값(lvalue)**이에요(그 주소를 취할 수 있어요).

C23 이전에는, 복합 리터럴이 파일 스코프에서 발생하면 그 이름 없는 객체는 **정적 저장 기간(static storage duration)**을, 블록 스코프에서 발생하면 **자동 저장 기간(automatic storage duration)**을 가졌어요(후자의 경우 객체의 수명은 바깥 블록이 끝날 때 끝나요).

C23부터는, 복합 리터럴이 함수 본문 밖과 매개변수 목록 밖에서 평가되면 파일 스코프와 연관되고, 그 외에는 바깥 블록과 연관돼요. 이 연관에 따라 저장 클래스 지정자(비어 있을 수 있음), 타입 이름, 초기화 목록은 파일 스코프 또는 블록 스코프에서 다음 형식의 객체 정의에 유효한 지정자여야 해요.

storage-class-specifiers typeof( type ) ID = { initializer-list } ;

여기서 ID는 전체 프로그램에 대해 고유한 식별자예요. 복합 리터럴은 값·타입·저장 기간·그 외 속성이 위 정의 문법에 따른 것과 같은 이름 없는 객체를 제공해요. 저장 기간이 자동이면 이름 없는 객체 인스턴스의 수명은 바깥 블록의 현재 실행 동안이에요. 저장 클래스 지정자가 constexpr, static, register, thread_local 외의 다른 지정자를 담으면 동작은 미정의예요.

참고(Notes)

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

&(const int){7} == &(const int){7} // might be 1 or 0, unspecified
(const char []){"abc"} == "abc" // might be 1 or 0, unspecified

각 복합 리터럴은 자신의 스코프에서 단일 객체만 만들어요.

#include <assert.h>

int main(void)
{
    struct S
    {
        int i;
    }
    *p = 0, *q;
    int j = 0;
again:
    q = p,
    p = &((struct S){ j++ }); // creates an unnamed object of type S,
                              // initializes it to the value formerly
                              // held in j, then assigns the address
                              // of this unnamed object to the pointer p
    if (j < 2)
        goto again; // note: if a loop were used, it would end scope here,
                    // which would terminate the lifetime of the compound
                    // literal leaving p as a dangling pointer
    assert(p == q && q->i == 1);
}

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

복합 리터럴의 문법이 캐스트와 비슷하지만, 중요한 차이는 캐스트는 비-좌측값 표현식인 데 반해 복합 리터럴은 좌측값이라는 점이에요.

예제(Example)

#include <stdio.h>

int *p = (int[]){2, 4}; // creates an unnamed static array of type int[2]
                        // initializes the array to the values {2, 4}
                        // creates pointer p to point at the first element of
                        // the array
const float *pc = (const float []){1e0, 1e1, 1e2}; // read-only compound literal

struct point {double x,y;};

int main(void)
{
    int n = 2, *p = &n;
    p = (int [2]){*p}; // creates an unnamed automatic array of type int[2]
                       // initializes the first element to the value formerly
                       // held in *p
                       // initializes the second element to zero
                       // stores the address of the first element in p

    void drawline1(struct point from, struct point to);
    void drawline2(struct point *from, struct point *to);
    drawline1(
        (struct point){.x=1, .y=1},  // creates two structs with block scope and
        (struct point){.x=3, .y=4}); // calls drawline1, passing them by value
    drawline2(
        &(struct point){.x=1, .y=1},  // creates two structs with block scope and
        &(struct point){.x=3, .y=4}); // calls drawline2, passing their addresses
}

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}

파일 스코프의 (int[]){2, 4}는 정적 배열을 만들고, 블록 스코프의 (int [2]){*p}는 자동 배열을 만들어요. drawline1은 복합 리터럴을 값으로, drawline2&로 주소를 얻어 포인터로 넘겨요. 두 방식 모두 좌측값이기 때문에 주소를 취할 수 있어요.

더 알아보기

  • 캐스트에 대한 Cast operators 문서에서 복합 리터럴과의 차이(좌측값 여부)를 확인해요.
  • 초기화 규칙에 대한 Initialization 문서를 참고해요.