direct_initialization — 직접 초기화

direct_initialization — 직접 초기화 (Direct-initialization)

**직접 초기화(direct-initialization)**는 생성자 인자의 명시적 집합으로 객체를 초기화하는 방식이에요. T obj(arg)처럼 괄호나 중괄호로 인자를 직접 전달해 초기화해요.

복사 초기화와 달리, 직접 초기화는 모든 생성자(비-explicit 뿐 아니라 explicit까지 포함)를 고려해요. 그래서 std::unique_ptr처럼 explicit 생성자를 가진 타입도 T obj(arg)로 초기화할 수 있어요.

출처: cppreference

본문

직접 초기화는 생성자 인자의 명시적 집합으로 객체를 초기화해요.

문법 (Syntax)

T object ( arg );
T object ( arg1, arg2, ... );
T object { arg };
T ( other )
T ( arg1, arg2, ... )
static_cast< T >( other )
new T( args, ... )
Class::Class() : member( args, ... ) { ... }
[arg]() { ... }

설명 (Explanation)

직접 초기화는 다음 상황에서 수행돼요.

  1. 비어 있지 않은 괄호 표현식 목록 또는 중괄호 초기화 목록으로 초기화할 때
  2. 단일 중괄호 초기화 목록으로 비클래스 타입 객체를 초기화할 때
  3. 함수형 캐스트나 괄호 표현식 목록으로 prvalue 임시(의 결과 객체)를 초기화할 때
  4. static_cast 표현식으로 prvalue 임시(의 결과 객체)를 초기화할 때
  5. 초기화자가 있는 new-표현식으로 동적 저장 기간 객체를 초기화할 때
  6. 생성자 초기화 목록으로 기반 또는 비정적 멤버를 초기화할 때
  7. 람다 표현식에서 값으로 잡은 변수들로 클로저 객체 멤버를 초기화할 때

직접 초기화의 효과

  • T가 배열 타입이면, C++20부터 목록 초기화로 애그리거트 초기화처럼 처리하되 수축 변환은 허용하고 초기화자가 없는 요소는 값 초기화가 돼요. (C++20 이전에는 잘못된 형식)
  • T가 클래스 타입이면,
    • 초기화자가 T와 (cv 한정을 무시하고) 같은 클래스인 prvalue 표현식이면, 임시가 아니라 초기화자 표현식 자체로 목적 객체를 초기화해요.
    • 그 외에는 T의 생성자를 검사해 과부하 해석으로 최적 매치를 선택하고 그 생성자로 객체를 초기화해요.
    • C++20부터 목적 타입이 (가능하면 cv 한정된) 애그리거트 클래스면, 수축 변환은 허용되지만 지정 초기화자는 안 되고, 참조에 묶인 임시의 수명이 연장되지 않고, 중괄호 생략이 없고, 초기화자가 없는 요소는 값 초기화가 되는 애그리거트 초기화로 초기화해요.
  • T가 비클래스 타입인데 소스 타입이 클래스 타입이면, 소스 타입과 그 기반 클래스의 변환 함수를 검사해 과부하 해석으로 최적을 선택하고, 선택된 사용자 정의 변환으로 초기화자 표현식을 초기화 중인 객체로 변환해요.
  • Tbool이고 소스 타입이 std::nullptr_t면 초기화되는 객체의 값은 false예요.
  • 그 외에는 필요하다면 표준 변환으로 other의 값을 cv 한정 없는 T로 변환해요.

노트 (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>());
    // foo1 이라는 함수를 선언, 반환 타입은 std::string,
    // 첫 매개변수 타입은 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)

#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

std::unique_ptrexplicit 생성자도 직접 초기화(new int(1) 괄호)에서는 사용할 수 있다는 점이 핵심이에요. 직접 초기화는 explicit 생성자를 포함한 모든 생성자를 고려하기 때문이에요.

더 알아보기 (Learn more)

cppreference