직접 초기화

직접 초기화 (Direct initialization)

직접 초기화(direct initialization)명시적인 생성자 인자 집합으로 객체를 초기화하는 방식을 말해요. 괄호 ()나 중괄호 {}로 인자를 감싸 전달하는 형태가 대표적이에요. 색상이 단순히 "복사 초기화"보다 더 관대해서, explicit 생성자와 사용자 정의 변환 함수까지 모두 고려한다는 특징이 있어요.

출처: cppreference

본문

문법 (Syntax)

직접 초기화가 일어나는 여러 형태가 있어요.

표기 상황
T object(arg); T object(arg1, arg2, ...); (1) 비어 있지 않은 괄호 표현식 목록 또는 중괄호 초기화 목록(C++11부터)으로 초기화
T object{arg}; (2) 비클래스 타입 객체를 하나의 중괄호 초기화자로 초기화 (C++11부터, 클래스 타입 등은 리스트 초기화 참고)
T(other) T(arg1, arg2, ...) (3) 함수형 캐스트 또는 괄호 표현식 목록으로 prvalue 임시(until C++17)/prvalue의 결과 객체(since C++17) 초기화
static_cast<T>(other) (4) static_cast 표현식으로 prvalue 임시(until C++17)/prvalue의 결과 객체(since C++17) 초기화
new T(args, ...) (5) 이니셜라이저를 가진 new 표현식으로 동적 저장 기간 객체 초기화
Class::Class() : member(args, ...) { ... } (6) 생성자 이니셜라이저 목록으로 기반 또는 비정적 멤버 초기화
[arg]() { ... } (7) 람다 표현식에서 복사로 잡은 변수들로 클로저 객체 멤버 초기화 (C++11부터)

설명 (Explanation)

직접 초기화의 효과는 타입 T에 따라 달라져요.

T가 배열 타입이면:

  • (until C++20) 그 프로그램은 ill-formed예요.
  • (since C++20) 배열은 애그리게이트 초기화처럼 초기화되는데, 축소 변환(narrowing conversion)은 허용되고, 이니셜라이저가 없는 원소는 값 초기화(value-initialized) 돼요.
struct A { explicit A(int i = 0) {} };
A a[2](A(1)); // OK: a[0]은 A(1)로, a[1]은 A()로 초기화
A b[2]{A(1)}; // 오류: b[1]은 {}에서 implicit copy-list-initialization,
              //       explicit 생성자를 선택하게 됨

T가 클래스 타입이면:

  • (since C++17) 이니셜라이저가 T의 클래스와 같은(단 cv 한정은 무시) prvalue 표현식이면, 그 prvalue에서 materialize된 임시가 아니라 이니셜라이저 표현식 자체가 목적 객체를 초기화하는 데 쓰여요. (C++17 이전에는 컴파일러가 이 경우 prvalue 임시에서의 생성을 생략할 수 있었지만, 적절한 생성자는 여전히 접근 가능해야 했어요. 복사 생략 참고.)
  • T의 생성자들을 조사하고 오버로드 해석으로 최선의 일치를 선택해요. 선택된 생성자가 호출되어 객체를 초기화해요.
  • (since C++20) 그 외에 목적 타입이 (cv 한정일 수 있는) 애그리게이트 클래스라면, 애그리게이트 초기화처럼 초기화되는데, 축소 변환은 허용되고, 지정 이니셜라이저(designated initializer)는 허용되지 않으며, 참조에 바인딩된 임시의 수명이 연장되지 않고, 중괄호 생략(brace elision)이 없고, 이니셜라이저가 없는 원소는 값 초기화돼요.
struct B { int a; int&& r; };
int f();
int n = 10;
B b1{1, f()};              // OK, 수명이 연장됨
B b2(1, f());              // well-formed이지만 댕글링 참조
B b3{1.0, 1};              // 오류: 축소 변환
B b4(1.0, 1);              // well-formed이지만 댕글링 참조
B b5(1.0, std::move(n));   // OK

그 외에 T가 비클래스 타입이지만 원본 타입이 클래스 타입이면, 원본 타입과 그 기반 클래스(있다면)의 변환 함수를 조사하고 오버로드 해석으로 최선의 일치를 선택해요. 선택된 사용자 정의 변환으로 초기화 표현식을 초기화되는 객체로 변환해요.

그 외에 Tbool이고 원본 타입이 std::nullptr_t 이면, 초기화되는 객체의 값은 false예요.

그 외에는 필요하다면 표준 변환으로 other의 값을 T의 cv-미한정 버전으로 변환하고, 초기화되는 객체의 초기값은 (변환됐을 수 있는) 그 값이에요.

참고 (Notes)

직접 초기화는 복사 초기화보다 더 관대해요. 복사 초기화는 비-explicit 생성자와 비-explicit 사용자 정의 변환 함수만 고려하지만, 직접 초기화는 모든 생성자와 모든 사용자 정의 변환 함수를 고려해요.

직접 초기화 문법 (1)(둥근 괄호)을 사용한 변수 선언과 함수 선언 사이에 모호함이 생기면, 컴파일러는 항상 함수 선언을 선택해요. 이 규칙은 직관에 반하는 경우가 있어 "가장 성가신 해석 (most vexing parse)" 이라고 불려요.

#include <fstream>
#include <iterator>
#include <string>

int main()
{
    std::ifstream file("data.txt");

    // 다음은 함수 선언입니다:
    std::string foo1(std::istreambuf_iterator<char>(file),
                     std::istreambuf_iterator<char>());
    // 반환 타입이 std::string이고 이름이 foo1인 함수를 선언하며,
    // 첫 매개변수는 타입 std::istreambuf_iterator<char>, 이름 "file",
    // 두 번째 매개변수는 이름이 없고 타입 std::istreambuf_iterator<char>(),
    // 이는 함수 포인터 타입 std::istreambuf_iterator<char>(*)()로 재작성됨

    // C++11 이전 해결책(변수를 선언하려면) - 인자 하나에 괄호를 추가:
    std::string str1((std::istreambuf_iterator<char>(file)),
                      std::istreambuf_iterator<char>());

    // C++11 이후 해결책(변수를 선언하려면) - 인자에 리스트 초기화 사용:
    std::string str2(std::istreambuf_iterator<char>{file}, {});
}

예제 (Example)

직접 초기화가 explicit 생성자를 허용한다는 점을 코드로 확인해 볼게요. 여기서 핵심은 unique_ptrFooexplicit 생성자가 = 형태(복사 초기화)에서는 오류를 내지만 괄호 형태(직접 초기화)에서는 정상 동작한다는 거예요.

#include <iostream>
#include <memory>
#include <string>

struct Foo
{
    int mem;
    explicit Foo(int n) : mem(n) {}
};

int main()
{
    std::string s1("test"); // const char* 생성자
    std::string s2(10, 'a');

    std::unique_ptr<int> p(new int(1));  // OK: explicit 생성자 허용
//  std::unique_ptr<int> p = new int(1); // 오류: 생성자가 explicit

    Foo f(2); // f는 직접 초기화됨:
              // 생성자 매개변수 n은 rvalue 2에서 복사 초기화됨
              // f.mem은 매개변수 n에서 직접 초기화됨
//  Foo f2 = 2; // 오류: 생성자가 explicit

    std::cout << s1 << ' ' << s2 << ' ' << *p << ' ' << f.mem  << '\n';
}

출력:

test aaaaaaaaaa 1 2

복사 초기화와의 차이가 이 예제에서 그대로 드러나요. explicit 생성자는 대입 형태의 초기화(=)를 거부하지만, 직접 초기화 문법(() 또는 {})에서는 문제없이 호출돼요.

더 알아보기 (Learn more)

  • 복사 초기화 (Copy initialization) — 직접 초기화와 대비되는, = 형태의 초기화. (cppreference)
  • 리스트 초기화 (List initialization) — 중괄호 {} 를 사용한 초기화의 전체 규칙. (cppreference)
  • 복사 생략 (Copy elision) — prvalue의 임시 객체 materialize를 생략하는 규칙. (cppreference)
  • explicit 지정자 — 생성자·변환 연산자의 암시적 사용을 막는 지정자. (cppreference)