기본 초기화

기본 초기화 (Default initialization)

기본 초기화(default initialization) 는 객체를 이니셜라이저 없이 만들 때 수행되는 초기화예요. int n;처럼 변수를 선언만 하거나, new T처럼 이니셜라이저 없는 동적 생성, 또는 생성자 이니셜라이저 목록에 빠진 멤버/기반이 이 범주에 들어와요.

출처: cppreference

본문

문법 (Syntax)

표기 의미
T object; (1) 이니셜라이저 없는 객체 선언
new T (2) 이니셜라이저 없는 동적 생성

설명 (Explanation)

기본 초기화는 세 가지 상황에서 수행돼요.

  • 1) 자동·정적·스레드-로컬 저장 기간을 가진 변수를 이니셜라이저 없이 선언할 때.
  • 2) 동적 저장 기간을 가진 객체를 이니셜라이저 없는 new 표현식으로 만들 때.
  • 3) 기반 클래스나 비정적 데이터 멤버가 생성자 이니셜라이저 목록에 언급되지 않고, 그 생성자가 호출될 때.

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

  • T가 (cv 한정일 수 있는) 클래스 타입이면, 생성자들을 고려해 빈 인자 목록에 대한 오버로드 해석을 수행해요. 선택된 생성자(기본 생성자 중 하나)가 호출되어 새 객체의 초기값을 제공해요.
  • T배열 타입이면, 배열의 모든 원소가 기본 초기화돼요.
  • (C++26부터) Tstd::meta::info이면, 객체는 null 반영 값으로 초기화돼요.
  • 그 외에는 아무 초기화도 수행되지 않아요 (아래 부정값 참고).

(until C++11) 처음에는 (cv 한정일 수 있는) 자동 저장 기간을 가진 비-POD 클래스 타입(또는 그 배열)만 이니셜라이저 없이 기본 초기화되는 것으로 간주됐어요. 동적 저장 기간의 스칼라와 POD 타입은 초기화되지 않는 것으로 간주됐죠 (C++11부터 이 상황은 기본 초기화의 한 형태로 재분류됐어요).

const 객체의 기본 초기화

프로그램이 const 한정 타입 T의 객체 기본 초기화를 요구한다면, Tconst-default-constructible 타입이어야 해요.

타입 T가 const-default-constructible이라면 (다음 중 하나):

  • (C++26부터) Tstd::meta::info이다.
  • (C++11부터) Tstd::nullptr_t이다.
  • T의 기본 초기화가 사용자 제공(user-provided) 생성자(기반에서 상속된 것이 아닌)를 호출한다. (C++11부터)
  • T가 다음을 만족하는 클래스 타입이다.
    • (until C++11) T의 각 직접 비정적 데이터 멤버가 클래스 타입 X(또는 그 배열)이고, X가 const-default-constructible이며, T에 직접 변형 멤버가 없고, T의 각 잠재적 생성 기반이 const-default-constructible이다.
    • (since C++11) T의 각 직접 비변형 비정적 데이터 멤버가 기본 멤버 이니셜라이저를 가지거나 const-default-constructible 타입이고, T가 비정적 데이터 멤버가 적어도 하나인 union이면 정확히 하나의 변형 멤버가 기본 멤버 이니셜라이저를 가지며, T가 union이 아니면 각 익명 union 멤버가 const-default-constructible이고, T의 각 잠재적 생성 기반이 const-default-constructible이다.
  • T가 const-default-initializable 타입의 배열이다.

부정(不定) 값과 오류 값 (Indeterminate and erroneous values)

  • (until C++26) 자동 또는 동적 저장 기간의 객체에 대한 저장 공간이 확보되면, 그 객체는 부정 값(indeterminate value) 을 가져요. 객체에 대해 초기화가 수행되지 않으면, 그 값이 교체될 때까지 그 부정 값을 유지해요.
  • (since C++26) 자동 또는 동적 저장 기간의 객체에 대한 저장 공간이 확보되면, 그 객체를 이루는 바이트들은 다음 초기값을 가져요.
    • 객체가 동적 저장 기간을 갖거나, 첫 선언이 [[indeterminate]]로 표시된 변수·함수 매개변수와 연관된 객체라면, 바이트들은 부정 값을 가져요.
    • 그 외에는 바이트들이 오류 값(erroneous value) 을 가지며, 각 값은 프로그램 상태와 무관하게 구현이 결정해요.
    • 객체(부분 객체 포함)에 대해 초기화가 수행되지 않으면, 그 바이트는 값이 교체될 때까지 초기값을 유지해요.
    • 값 표현의 어떤 비트가 부정 값이면 객체는 부정 값을, 어떤 비트가 오류 값이면 객체는 오류 값을 가져요.

평가(evaluation)가 부정 값을 만들어내면 동작은 정의되지 않음(undefined) 이에요. (C++26부터) 평가가 오류 값을 만들어내면 동작은 오류적(erroneous) 이에요.

특별한 경우 (Special cases)

다음 타입들은 미초기화 친화적(uninitialized-friendly) 이에요.

  • (C++17부터) std::byte
  • unsigned char
  • 기반 타입이 unsigned char인 경우의 char

부정 값 또는 (C++26부터) 오류 값 value가 주어졌을 때, value미초기화 결과 값(uninitialized result value) 은 다음과 같아요.

  • value가 부정 값이면 부정 값.
  • (C++26부터) value가 오류 값이면 value 그 자체.

미초기화 친화적 타입의 부정 또는 (C++26부터) 오류 값 value를 만들어내는 평가가 다음 중 하나라면 그 동작은 잘 정의됩니다(well-defined).

  • 다음 표현식과 피연산자 중 하나의 평가:
    • 조건 표현식의 두 번째 또는 세 번째 피연산자.
    • 콤마 표현식의 오른쪽 피연산자.
    • 미초기화 친화적 타입으로의 정수 변환, 명시적 캐스트, 또는 static_cast의 피연산자.
    • 폐기-값(discarded-value) 표현식.
  • 왼쪽 피연산자가 미초기화 친화적 타입의 lvalue인 단순 대입 연산자의 오른쪽 피연산자 평가.
  • 미초기화 친화적 타입의 객체를 초기화할 때의 초기화 표현식 평가.

미초기화 친화적 타입의 부정 값을 변환하면 부정 값을 만들어내요. (C++26부터) 미초기화 친화적 타입의 오류 값을 변환하면 오류 값을 만들어내고, 변환 결과는 변환된 피연산자의 값이에요.

// 사례 1: 동적 저장 기간의 미초기화 객체
// 모든 C++ 버전: 부정 값 + 정의되지 않은 동작
int f(bool b)
{
    unsigned char* c = new unsigned char;
    unsigned char d = *c; // OK, "d"는 부정 값
    int e = d;            // 정의되지 않은 동작
    return b ? d : 0;     // "b"가 true면 정의되지 않은 동작
}

// 사례 2: 자동 저장 기간의 미초기화 객체
// C++26 이전: 부정 값 + 정의되지 않은 동작
// C++26 이후: 오류 값 + 오류적 동작
int g(bool b)
{
    unsigned char c;     // "c"는 부정/오류 값

    unsigned char d = c; // 정의되지 않거나 오류적이진 않지만,
                         // "d"도 부정/오류 값

    assert(c == d);      // 성립하지만, 두 정수 승격 모두
                         // 정의되지 않거나 오류적

    int e = d;           // 정의되지 않거나 오류적 동작
    return b ? d : 0;    // "b"가 true면 정의되지 않거나 오류적 동작
}

// 사례 2와 같음
void h()
{
    int d1, d2;  // "d1"과 "d2"는 부정/오류 값
    int e1 = d1; // 정의되지 않거나 오류적 동작
    int e2 = d1; // 정의되지 않거나 오류적 동작

    assert(e1 == e2); // 성립
    assert(e1 == d1); // 성립, 정의되지 않거나 오류적 동작
    assert(e2 == d1); // 성립, 정의되지 않거나 오류적 동작

    // 정의되지 않거나 오류적이진 않지만,
    // "d2"는 부정/오류 값
    std::memcpy(&d2, &d1, sizeof(int));

    assert(e1 == d2); // 성립, 정의되지 않거나 오류적 동작
    assert(e2 == d2); // 성립, 정의되지 않거나 오류적 동작
}

참고 (Notes)

참조와 const 스칼라 객체는 기본 초기화될 수 없어요.

기능 검사 매크로 표준 기능
__cpp_constexpr 201907L (C++20) constexpr 함수에서의 trivial 기본 초기화와 asm 선언

예제 (Example)

어떤 객체가 실제로 아무 초기화도 받지 않고, 어떤 객체가 기본 생성자를 통해 초기화되는지를 확인해 볼게요.

#include <string>

struct T1 { int mem; };

struct T2
{
    int mem;
    T2() {} // "mem"은 이니셜라이저 목록에 없음
};

int n; // 정적 비클래스, 두 단계 초기화가 수행됨:
       // 1) zero-initialization이 n을 0으로 초기화
       // 2) default-initialization은 아무것도 하지 않아 n은 0 유지

int main()
{
    [[maybe_unused]]
    int n;            // 비클래스, 값은 부정
    std::string s;    // 클래스, 기본 생성자 호출, 값은 ""
    std::string a[2]; // 배열, 원소들을 기본 초기화, 값은 {"", ""}
//  int& r;           // 오류: 참조
//  const int n;      // 오류: const 비클래스
//  const T1 t1;      // 오류: 암시적 기본 생성자를 가진 const 클래스
    [[maybe_unused]]
    T1 t1;            // 클래스, 암시적 기본 생성자 호출
    const T2 t2;      // const 클래스, 사용자 제공 기본 생성자 호출
                      // t2.mem은 기본 초기화됨
}

여기서 눈여겨볼 점은 T1T2의 차이예요. T1은 암시적 기본 생성자를 쓰므로 const 객체로 만들 수 없지만, T2는 사용자 제공 기본 생성자를 쓰므로 const T2가 허용돼요. 또한 int n;(정적)은 영(0) 초기화가 먼저 일어나 0이 되지만, main 안의 int n;(자동)은 부정 값을 갖는다는 차이도 확인할 수 있어요.

결함 보고 (Defect reports)

다음의 동작을 바꾸는 결함 보고가 이전에 공표된 C++ 표준에 소급 적용됐어요.

DR 적용 대상 공표된 동작 올바른 동작
CWG 178 C++98 값 초기화가 없었고, 빈 이니셜라이저가 기본 초기화를 호출함 (단 new T()는 zero-initialization도 수행) 빈 이니셜라이저가 값 초기화 호출
CWG 253 C++98 const 객체의 기본 초기화가 암시적으로 선언된 기본 생성자를 호출할 수 없었음 모든 부분 객체가 초기화되면 허용
CWG 616 C++98 미초기화 객체의 lvalue-to-rvalue 변환이 항상 UB였음 부정 unsigned char는 허용
CWG 1787 C++98 레지스터에 캐시된 부정 unsigned char 읽기가 UB였음 잘 정의되도록 만듦
CWG 3089 C++11 std::nullptr_t가 const-default-initializable이 아니었음 const-default-initializable로 만듦

더 알아보기 (Learn more)

  • 변환 생성자 (Converting constructor) — 인자 하나로 호출 가능해 암시적 변환에 쓰이는 생성자. (cppreference)
  • 기본 생성자 (Default constructor) — 인자 없이 호출되는 생성자. (cppreference)
  • explicit 지정자 — 생성자·변환 연산자의 암시적 변환을 막는 지정자. (cppreference)
  • new 표현식 — 동적 저장 기간 객체 생성. (cppreference)