값 초기화(Value initialization)

값 초기화(Value initialization)

빈 초기화식, 예를 들어 T()T{}로 객체를 만들면 어떤 일이 일어날까요? 기본 생성자를 그냥 부르는 걸까요? C++는 이 경우 **값 초기화(value initialization)**라는 특별한 초기화를 수행해요. 값 초기화는 빈 초기화식으로 객체를 생성할 때 수행되는 초기화예요.

출처: cppreference

본문

문법

T() (1)
new T() (2)
Class::Class(...) : member() { ... } (3)
T object{}; (4) (C++11부터)
T{} (5) (C++11부터)
new T{} (6) (C++11부터)
Class::Class(...) : member{} { ... } (7) (C++11부터)

동작 방식(Explanation)

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

모든 경우에, 빈 중괄호 {}가 쓰이고 T가 집계 타입(aggregate type)이면, 값 초기화 대신 집계 초기화(aggregate initialization)가 수행돼요.

T가 기본 생성자는 없지만 std::initializer_list를 받는 생성자가 있는 클래스 타입이면, 목록 초기화(list-initialization)가 수행돼요. (C++11부터)

값 초기화의 효과는 다음과 같아요.

  • T가 (cv-한정일 수 있는) 클래스 타입이면:
    • T에 대한 기본 초기화가 생성자를 선택하고, 그 생성자가 사용자 선언(until C++11)사용자 제공(since C++11) 생성자가 아니면 객체가 먼저 0 초기화(zero-initialized)돼요.
    • 어쨌든 객체는 기본 초기화(default-initialized)돼요.
  • 그렇지 않고 T가 배열 타입이면, 배열의 각 요소가 값 초기화돼요.
  • 그렇지 않으면 객체는 0 초기화돼요.

정리하면, 값 초기화는 스칼라 타입에는 0을, 클래스 타입에는 조건에 따라 0 초기화 후 기본 초기화를 적용하는 방식이에요.

주의할 점(Notes)

T object(); 문법은 객체를 초기화하지 않아요. 인자를 받지 않고 T를 반환하는 함수를 선언할 뿐이에요. C++11 이전에 이름 있는 변수를 값 초기화하는 방법은 T object = T();였는데, 이는 임시 객체를 값 초기화한 뒤 그 임시 객체를 복사 초기화해요. 대부분의 컴파일러는 이 경우 복사를 최적화해 없애요.

참조는 값 초기화할 수 없어요.

함수 스타일 캐스트에서 설명했듯이, T() (1) 문법은 T가 배열 타입을 가리킬 때 금지되지만 T{} (5)는 허용돼요.

모든 표준 컨테이너(std::vector, std::list 등)는 단일 size_type 인자로 생성되거나 resize() 호출로 커질 때 요소들을 값 초기화해요. 할당기(allocator)가 construct의 동작을 커스터마이즈하지 않는 경우에요.

예제

여기서는 같은 {} 문법이 타입에 따라 어떻게 달라지는지 볼게요. 스칼라는 0으로, 클래스는 기본 생성자 유무에 따라 0 초기화 후 기본 초기화로 처리돼요.

#include <cassert>
#include <iostream>
#include <string>
#include <vector>

struct T1
{
    int mem1;
    std::string mem2;
    virtual void foo() {} // T1이 집계 타입이 아니도록 보장
}; // 암시적 기본 생성자

struct T2
{
    int mem1;
    std::string mem2;
    T2(const T2&) {} // 사용자 제공 복사 생성자
};                   // 기본 생성자 없음

struct T3
{
    int mem1;
    std::string mem2;
    T3() {} // 사용자 제공 기본 생성자
};

std::string s{}; // 클래스 => 기본 초기화, 값은 ""

int main()
{
    int n{};                // 스칼라 => 0 초기화, 값은 0
    assert(n == 0);
    double f = double();    // 스칼라 => 0 초기화, 값은 0.0
    assert(f == 0.0);
    int* a = new int[10](); // 배열 => 각 요소의 값 초기화
    assert(a[9] == 0);      //          각 요소의 값은 0
    T1 t1{};                // 암시적 기본 생성자를 가진 클래스 =>
    assert(t1.mem1 == 0);   //     t1.mem1은 0 초기화, 값은 0
    assert(t1.mem2 == "");  //     t1.mem2는 기본 초기화, 값은 ""
//  T2 t2{};                // 오류: 기본 생성자가 없는 클래스
    T3 t3{};                // 사용자 제공 기본 생성자를 가진 클래스 =>
    std::cout << t3.mem1;   //     t3.mem1은 기본 초기화되어 불확정 값
    assert(t3.mem2 == "");  //     t3.mem2는 기본 초기화, 값은 ""
    std::vector<int> v(3);  // 각 요소의 값 초기화
    assert(v[2] == 0);      // 각 요소의 값은 0
    std::cout << '\n';
    delete[] a;
}

가능한 출력:

42

t3.mem1이 초기화되지 않은 값이므로 이 출력 값은 실행 환경에 따라 달라질 수 있어요.

결함 보고(Defect reports)

이전에 발표된 C++ 표준들에 소급 적용된 동작 변경 결함 보고들이 있어요.

DR 적용 대상 발표 당시 동작 올바른 동작
CWG 178 C++98 값 초기화가 없었고, 빈 초기화식이 기본 초기화를 호출했음 (단 new T()는 0 초기화도 수행) 빈 초기화식이 값 초기화를 호출
CWG 543 C++98 사용자 제공 생성자가 없는 클래스 객체의 값 초기화는 각 하위 객체를 값 초기화하는 것과 동등했음 전체 객체를 0 초기화한 뒤 기본 생성자를 호출
CWG 1301 C++11 삭제된 기본 생성자를 가진 공용체의 값 초기화가 0 초기화로 이어졌음 기본 초기화됨
CWG 1368 C++98 사용자 제공 생성자가 있으면 0 초기화가 건너뛰어졌음 사용자 제공 기본 생성자만 0 초기화를 건너 뜀
CWG 1502 C++11 기본 멤버 초기화가 있어도 사용자 제공 기본 생성자가 없는 공용체의 값 초기화는 객체만 0 초기화했음 0 초기화 후 기본 초기화를 수행
CWG 1507 C++98 사용자 제공 생성자가 없는 클래스 객체의 값 초기화는 자명한 기본 생성자의 유효성을 검사하지 않았음 자명한 기본 생성자의 유효성이 검사됨
CWG 2820 C++98 0 초기화 뒤의 기본 초기화가 비자명 생성자를 요구했음 요구하지 않음
CWG 2859 C++98 클래스 객체의 값 초기화가 기본 초기화가 실제로 사용자 제공 생성자를 선택하지 않아도 0 초기화를 포함할 수 있었음 이 경우 0 초기화 없음

더 알아보기

  • 기본 초기화: 생성자 없이 하는 초기화.
  • 0 초기화: 값을 0으로 채우는 초기화.
  • 집계 초기화: {}로 집계 타입을 초기화하는 방법.
  • 목록 초기화: {} 문법으로 하는 초기화 전반.