기본 초기화
기본 초기화 (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부터)
T가std::meta::info이면, 객체는 null 반영 값으로 초기화돼요. - 그 외에는 아무 초기화도 수행되지 않아요 (아래 부정값 참고).
(until C++11) 처음에는 (cv 한정일 수 있는) 자동 저장 기간을 가진 비-POD 클래스 타입(또는 그 배열)만 이니셜라이저 없이 기본 초기화되는 것으로 간주됐어요. 동적 저장 기간의 스칼라와 POD 타입은 초기화되지 않는 것으로 간주됐죠 (C++11부터 이 상황은 기본 초기화의 한 형태로 재분류됐어요).
const 객체의 기본 초기화
프로그램이 const 한정 타입 T의 객체 기본 초기화를 요구한다면, T는 const-default-constructible 타입이어야 해요.
타입 T가 const-default-constructible이라면 (다음 중 하나):
- (C++26부터)
T가std::meta::info이다. - (C++11부터)
T가std::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이다.
- (until C++11)
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은 기본 초기화됨
}
여기서 눈여겨볼 점은 T1과 T2의 차이예요. 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)