new 표현식
new 표현식 (new expression)
C++에서 힙(heap)에 객체를 만들 때 쓰는 게 new 표현식이에요. new는 단순히 메모리를 할당하는 게 아니라, 그 위에 객체를 생성하고 초기화하는 두 단계를 묶어서 수행해요.
출처: cppreference
본문
new 표현식은 동적 저장 기간(dynamic storage duration)을 가진 객체를 생성하고 초기화해요. 즉, 그 객체의 수명이 생성된 스코프에 반드시 제한되지 않는 객체를요.
문법
::(optional) new ( type ) new-initializer(optional) (1)
::(optional) new type new-initializer(optional) (2)
::(optional) new ( placement-args ) ( type ) new-initializer(optional) (3)
::(optional) new ( placement-args ) type new-initializer(optional) (4)
type— 대상 타입 id.new-initializer— 괄호로 감싼 표현식 목록 또는 중괄호로 감싼 초기화 목록(C++11부터).placement-args— 추가 배치(placement) 인자.
설명
new 표현식은 저장 공간을 할당하려 시도한 다음, 할당된 저장 공간에 단일 무명 객체 또는 무명 객체 배열을 생성하고 초기화하려 시도해요. new 표현식은 생성된 객체에 대한 prvalue 포인터를, 배열이 생성되었다면 배열의 첫 번째 요소에 대한 포인터를 반환해요.
type에 괄호가 포함되면 문법 (1) 또는 (3)이 필요해요:
new int(*[10])(); // error: (new int) (*[10]) ()로 해석됨
new (int (*[10])()); // okay: 함수에 대한 포인터 10개의 배열을 할당
게다가 type은 탐욕적으로(greedily) 해석돼요. 선언자(declarator)의 일부가 될 수 있는 모든 토큰을 포함하도록 취해져요:
new int + 1; // okay: (new int) + 1로 해석, new int가 반환한 포인터를 증가시킴
new int * 1; // error: (new int*) (1)로 해석됨
new-initializer는 다음과 같은 경우 선택 사항이 아니에요:
type이 알 수 없는 경계(unknown bound)의 배열일 때,type에 placeholder, 즉auto나decltype(auto)(C++14부터)가 사용될 때, (C++11부터) 가능하면 타입 제약(C++20부터)과 결합될 수 있음,type에 인자를 추론해야 하는 클래스 템플릿이 사용될 때. (C++17부터)
double* p = new double[]{1, 2, 3}; // double[3] 타입의 배열 생성
auto p = new auto('c'); // char 타입의 단일 객체 생성. p는 char*
auto q = new std::integral auto(1); // OK: q는 int*
auto q = new std::floating_point auto(true) // ERROR: 타입 제약이 만족되지 않음
auto r = new std::pair(1, true); // OK: r는 std::pair<int, bool>*
auto r = new std::vector; // ERROR: 요소 타입을 추론할 수 없음
동적 배열 (Dynamic arrays)
type이 배열 타입이면, 첫 번째 차원을 제외한 모든 차원은 양의 정수 상수 표현식(C++14 이전까지) / std::size_t 타입의 변환된 상수 표현식(C++14부터)으로 지정되어야 해요. 하지만 (괄호 없는 문법 (2)와 (4)를 사용할 때만) 첫 번째 차원은 정수 타입, 열거형 타입, 또는 정수·열거형 타입으로의 단일 비-explicit 변환 함수를 가진 클래스 타입의 표현식(C++14 이전까지) / std::size_t로 변환 가능한 어떤 표현식(C++14부터)일 수 있어요. 이것이 런타임에 크기가 정해진 배열을 직접 만드는 유일한 방법이에요. 그런 배열을 흔히 동적 배열(dynamic arrays)이라고 불러요:
int n = 42;
double a[n][5]; // error
auto p1 = new double[n][5]; // OK
auto p2 = new double[5][n]; // error: 첫 번째 차원만 비상수일 수 있음
auto p3 = new (double[n][5]); // error: 동적 배열에 문법 (1)을 쓸 수 없음
첫 번째 차원의 값이 (필요하면 정수나 열거형 타입으로 변환했을 때) 음수면 동작은 정의되지 않아요. (C++11 이전까지) 다음 경우에 첫 번째 차원을 지정하는 표현식의 값은 유효하지 않아요: 비클래스 타입이고 std::size_t로의 변환 전 값이 음수인 경우; 클래스 타입이고 사용자 정의 변환 함수 이후, 두 번째 표준 변환 이전 값이 음수인 경우; 값이 어떤 구현 정의 한계보다 큰 경우; 값이 중괄호로 감싼 초기화 목록에 제공된 배열 요소 수보다 작은 경우(문자열 리터럴의 종료 '\0' 포함). 첫 번째 차원의 값이 이런 이유 중 하나로 유효하지 않으면, std::size_t로의 변환 후 첫 번째 차원이 코어 상수 표현식이고 잠재적으로 평가되면(potentially evaluated) 프로그램은 ill-formed예요. 그렇지 않으면, 호출되었을 할당 함수가 비던지기(non-throwing)라면(std::nothrow 오버로드 포함, noexcept로 선언되지 않음) new 표현식은 필요한 결과 타입의 널 포인터를 반환하고, 그렇지 않으면 new 표현식은 할당 함수를 호출하지 않고 대신 std::bad_array_new_length 타입의 핸들러와 일치하는 타입의 예외를 던져요. (C++11부터)
첫 번째 차원이 0인 것은 허용되며, 할당 함수가 호출돼요.
new-initializer가 중괄호로 감싼 초기화 목록이고 첫 번째 차원이 잠재적으로 평가되고 코어 상수 표현식이 아니면, 빈 초기화 목록에서 배열의 가상 요소를 복사 초기화하는 의미 제약이 검사돼요. (C++11부터)
할당 (Allocation)
new 표현식은 적절한 할당 함수(allocation function)를 호출하여 저장 공간을 할당해요. type이 비배열 타입이면 함수 이름은 operator new이고, type이 배열 타입이면 함수 이름은 operator new[]예요.
할당 함수에서 설명한 대로, C++ 프로그램은 이 함수들에 대한 전역 및 클래스별 교체(replacement)를 제공할 수 있어요. new 표현식이 선택적 :: 연산자로 시작하면, 예를 들어 ::new T나 ::new T[n]처럼, 클래스별 교체는 무시돼요(함수는 전역 스코프에서 탐색됨). 그렇지 않고 T가 클래스 타입이면, 탐색은 T의 클래스 스코프에서 시작돼요.
할당 함수를 호출할 때 new 표현식은 요청된 바이트 수를 첫 번째 인자로, std::size_t 타입으로 전달해요. 비배열 T에 대해 이 값은 정확히 sizeof(T)예요.
배열 할당은 불특정 오버헤드를 공급할 수 있어요. 이 오버헤드는 한 new 호출에서 다음 new 호출로 달라질 수 있어요. 단, 선택된 할당 함수가 표준 비할당(non-allocating) 형태라면 예외예요. new 표현식이 반환하는 포인터는 할당 함수가 반환한 포인터에서 그 값만큼 오프셋돼요. 많은 구현이 배열 오버헤드를 사용해 배열의 객체 수를 저장하고, delete[] 표현식이 올바른 수의 소멸자를 호출하도록 해요. 게다가 new 표현식으로 char, unsigned char, std::byte(C++17부터) 배열을 할당하면, 나중에 할당된 배열에 배치되는 모든 타입의 객체가 요청된 배열 크기보다 크지 않은 경우 올바른 정렬을 보장하기 위해 필요하다면 할당 함수에 추가 메모리를 요청할 수 있어요.
new 표현식은 교체 가능한 할당 함수로 만들어진 할당을 생략(elide)하거나 결합(combine)할 수 있어요. 생략의 경우, 저장 공간은 할당 함수를 호출하지 않고 컴파일러가 제공할 수 있어요(이로써 사용되지 않는 new 표현식도 최적화할 수 있어요). 결합의 경우, new 표현식 E1이 만든 할당은 다음이 모두 참일 때 다른 new 표현식 E2를 위해 추가 저장 공간을 제공하도록 확장될 수 있어요: 1) E1이 할당한 객체의 수명이 E2가 할당한 객체의 수명을 엄격히 포함한다. 2) E1과 E2가 같은 교체 가능한 전역 할당 함수를 호출한다. 3) 던지는 할당 함수의 경우, E1과 E2의 예외가 같은 핸들러에서 먼저 잡힌다. 이 최적화는 new 표현식이 사용될 때만 허용되지, 교체 가능한 할당 함수를 호출하는 다른 방법에는 허용되지 않아요: delete[] new int[10];은 최적화될 수 있지만, operator delete(operator new(10));은 그럴 수 없어요. (C++14부터) 상수 표현식을 평가하는 동안 할당 함수 호출은 항상 생략돼요. 교체 가능한 전역 할당 함수 호출로 이어질 new 표현식만 상수 표현식에서 평가될 수 있어요. (C++20부터)
배치 new (Placement new)
placement-args가 제공되면 할당 함수에 추가 인자로 전달돼요. 그런 할당 함수를 배치 new라고 불러요. 표준 할당 함수 void* operator new(std::size_t, void*)는 두 번째 인자를 변경 없이 반환하는데, 이를 따라 이름이 붙었어요. 이것은 할당된 저장 공간에 객체를 구성하는 데 사용돼요:
// 어떤 블록 스코프 안에서...
{
// "T" 타입의 어떤 객체에도 충분히 큰
// 자동 저장 기간의 저장 공간을 정적으로 할당한다.
alignas(T) unsigned char buf[sizeof(T)];
T* tptr = new(buf) T; // "T" 객체를, 미리 할당한 저장 공간에
// 메모리 주소 "buf"에 직접 배치해 생성한다.
tptr->~T(); // 객체의 부수 효과가 프로그램이 의존한다면
// 반드시 **수동으로** 객체의 소멸자를 호출해야 한다.
} // 이 블록 스코프를 벗어나면 "buf"가 자동으로 해제된다.
참고: 이 기능은 Allocator 클래스 계열의 멤버 함수들에 캡슐화돼 있어요.
정렬 요구사항이 __STDCPP_DEFAULT_NEW_ALIGNMENT__를 초과하는 객체나 그런 객체들의 배열을 할당할 때, new 표현식은 정렬 요구사항을(std::align_val_t로 감싸서) 할당 함수의 두 번째 인자로 전달해요(배치 형태의 경우 placement-arg는 정렬 뒤에 세 번째, 네 번째 등의 인자로 나타남). 오버로드 해석이 실패하면(클래스별 할당 함수가 다른 시그니처로 정의된 경우, 전역 함수를 숨기기 때문에), 인자 목록에서 정렬 없이 오버로드 해석을 두 번째 시도해요. 이렇게 하면 정렬을 모르는 클래스별 할당 함수가 전역의 정렬을 아는 할당 함수보다 우선할 수 있어요. (C++17부터)
new T; // operator new(sizeof(T)) 호출
// (C++17) 또는 operator new(sizeof(T), std::align_val_t(alignof(T)))
new T[5]; // operator new[](sizeof(T)*5 + overhead) 호출
// (C++17) 또는 operator new(sizeof(T)*5+overhead, std::align_val_t(alignof(T)))
new(2,f) T; // operator new(sizeof(T), 2, f) 호출
// (C++17) 또는 operator new(sizeof(T), std::align_val_t(alignof(T)), 2, f)
비던지기 할당 함수(예: new(std::nothrow) T가 선택한 것)가 할당 실패로 널 포인터를 반환하면, new 표현식은 즉시 반환하고 객체를 초기화하거나 해제 함수를 호출하려 시도하지 않아요. 비할당 배치 new 표현식에 널 포인터가 인자로 전달되어, 선택된 표준 비할당 배치 할당 함수가 널 포인터를 반환하게 하면 동작은 정의되지 않아요.
초기화 (Initialization)
new 표현식이 만든 객체는 다음 규칙에 따라 초기화돼요.
type이 배열 타입이 아니면, 단일 객체가 획득된 메모리 영역에 구성돼요:
new-initializer가 없으면, 객체는 기본 초기화(default-initialized)돼요.new-initializer가 괄호로 감싼 표현식 목록이면, 객체는 직접 초기화(direct-initialized)돼요.new-initializer가 중괄호로 감싼 초기화 목록이면, 객체는 리스트 초기화(list-initialized)돼요. (C++11부터)
type이 배열 타입이면, 객체 배열이 초기화돼요:
new-initializer가 없으면, 각 요소는 기본 초기화돼요.- 첫 번째 차원이 0이어도, 가상 요소를 기본 초기화하는 의미 제약은 여전히 충족되어야 해요.
new-initializer가 괄호 쌍이면, 각 요소는 값 초기화(value-initialized)돼요.- 첫 번째 차원이 0이어도, 가상 요소를 값 초기화하는 의미 제약은 여전히 충족되어야 해요.
new-initializer가 중괄호로 감싼 초기화 목록이면, 배열은 집계 초기화(aggregate-initialized)돼요. (C++11부터)new-initializer가 괄호로 감싼 비어 있지 않은 표현식 목록이면, 배열은 집계 초기화돼요. (C++20부터)
초기화 실패 (Initialization failure)
초기화가 예외를 던지면서 종료되면(예: 생성자에서), 프로그램은 일치하는 해제 함수(deallocation function)를 찾은 다음:
- 적절한 해제 함수를 찾을 수 있으면, 객체가 구성되고 있던 메모리를 해제하도록 해제 함수가 호출돼요. 그 후 예외는
new표현식의 문맥에서 계속 전파돼요. - 모호하지 않은 일치하는 해제 함수를 찾을 수 없으면, 예외를 전파해도 객체의 메모리가 해제되지 않아요. 이것은 호출된 할당 함수가 메모리를 할당하지 않을 때만 적절하고, 그렇지 않으면 메모리 누수로 이어질 가능성이 커요.
일치하는 해제 함수의 탐색 스코프는 다음과 같이 정해져요:
new표현식이::로 시작하지 않고, 할당된 타입이 클래스 타입T또는 클래스 타입T의 배열이면, 해제 함수의 이름에 대한 탐색이T의 클래스 스코프에서 수행돼요.- 그 외, 또는
T의 클래스 스코프에서 아무것도 찾지 못하면, 해제 함수의 이름은 전역 스코프에서 탐색해요.
비배치 할당 함수의 경우, 일치하는 해제 함수를 찾기 위해 일반 해제 함수 탐색이 사용돼요(delete 표현식 참고).
배치 할당 함수의 경우, 일치하는 해제 함수는 같은 수의 매개변수를 가져야 하고, 첫 번째를 제외한 각 매개변수 타입이(매개변수 변환 후) 할당 함수의 대응 매개변수 타입과 동일해야 해요.
- 탐색이 단일 일치 해제 함수를 찾으면 그 함수가 호출돼요. 그렇지 않으면 어떤 해제 함수도 호출되지 않아요.
- 탐색이 비배치 해제 함수를 찾는데, 그 함수를 배치 해제 함수로 간주했을 때 할당 함수에 대한 일치로 선택되었을 함수라면, 프로그램은 ill-formed예요.
어떤 경우든, 일치하는 해제 함수(있다면)는 삭제되지 않아야 하고(C++11부터) new 표현식이 나타나는 지점에서 접근 가능해야 해요.
struct S
{
// 배치 할당 함수:
static void* operator new(std::size_t, std::size_t);
// 비배치 해제 함수:
static void operator delete(void*, std::size_t);
};
S* p = new (0) S; // error: 비배치 해제 함수가 배치 할당 함수와 일치함
new 표현식에서 해제 함수가 호출되면(초기화 실패로 인해), 그 함수에 전달되는 인자는 다음과 같이 정해져요:
- 첫 번째 인자는 할당 함수 호출에서 반환된 값(
void*타입)이에요. - 다른 인자(배치 해제 함수에만)는 배치 할당 함수에 전달된
placement-args예요.
구현이 할당 함수 호출의 일부로 임시 객체를 도입하거나 인자의 복사본을 만드는 것이 허용되면, 할당 함수와 해제 함수 호출 양쪽에 같은 객체가 사용되는지는 불특정이에요.
메모리 누수 (Memory leaks)
new 표현식이 만든 객체(동적 저장 기간을 가진 객체)는, new 표현식이 반환한 포인터가 일치하는 delete 표현식에 사용될 때까지 지속돼요. 포인터의 원래 값이 유실되면, 객체는 도달 불가능(reachable)해지고 할당 해제할 수 없게 되어 메모리 누수(memory leak)가 발생해요.
이것은 포인터가 다음과 같이 대입되면 일어날 수 있어요:
int* p = new int(7); // 값 7을 가진 동적으로 할당된 int
p = nullptr; // memory leak
또는 포인터가 스코프를 벗어나면:
void f()
{
int* p = new int(7);
} // memory leak
또는 예외 때문에:
void f()
{
int* p = new int(7);
g(); // may throw
delete p; // okay if no exception
} // memory leak if g() throws
동적으로 할당된 객체의 관리를 단순화하기 위해, new 표현식의 결과는 흔히 스마트 포인터(smart pointer)에 저장돼요: std::auto_ptr(C++17 이전까지), std::unique_ptr, 또는 std::shared_ptr(C++11부터). 이 포인터들은 위에 보인 상황에서 delete 표현식이 실행되도록 보장해요.
참고 사항 (Notes)
Itanium C++ ABI는 생성된 배열의 요소 타입이 자명하게 소멸 가능(trivially destructible)하면 배열 할당 오버헤드가 0이어야 한다고 요구해요. MSVC도 그렇게 해요.
일부 구현(예: VS 2019 v16.7 이전의 MSVC)은 요소 타입이 자명하게 소멸 가능하지 않으면 비할당 배치 배열 new에 대해 0이 아닌 배열 할당 오버헤드를 요구했는데, CWG 이슈 2382 이후로 더 이상 준수하지 않아요.
unsigned char 또는 std::byte(C++17부터)의 배열을 만드는 비할당 배치 배열 new 표현식은 주어진 저장 영역에 객체를 암시적으로 생성하는 데 사용될 수 있어요. 배열과 겹치는 객체들의 수명을 끝내고, 배열 안에 implicit-lifetime 타입의 객체를 암시적으로 생성하죠.
std::vector는 1차원 동적 배열에 대해 유사한 기능을 제공해요.
기능 테스트 매크로:
| 기능 테스트 매크로 | 값 | 표준 | 기능 |
|---|---|---|---|
__cpp_constexpr |
202406L | C++26 | Constexpr 배치 new |
키워드
new
결함 보고 (Defect reports)
| DR | 적용 대상 | 발표 당시 동작 | 올바른 동작 |
|---|---|---|---|
| CWG 74 | C++98 | 첫 번째 차원의 값이 정수 타입이어야 했음 | 열거형 타입 허용 |
| CWG 299 | C++98 | 첫 번째 차원의 값이 정수 또는 열거형 타입이어야 했음 | 정수·열거형 타입으로의 단일 변환 함수를 가진 클래스 타입 허용 |
| CWG 624 | C++98 | 할당된 객체의 크기가 구현 정의 한계를 초과하면 동작이 불특정이었음 | 이 경우 저장 공간을 얻지 않고 예외를 던짐 |
| CWG 1748 | C++98 | 비할당 배치 new가 인자가 null인지 확인할 필요가 있었음 | null 인자에 대해 정의되지 않은 동작 |
| CWG 1992 | C++11 | new(std::nothrow) int[N]이 std::bad_array_new_length를 던질 수 있었음 |
널 포인터를 반환하도록 변경 |
| CWG 2102 | C++98 | 빈 배열을 초기화할 때 기본/값 초기화가 well-formed여야 하는지 불명확했음 | 필요함 |
| CWG 2382 | C++98 | 비할당 배치 배열 new가 할당 오버헤드를 요구할 수 있었음 | 그런 할당 오버헤드 금지 |
| CWG 2392 | C++11 | 첫 번째 차원이 잠재적으로 평가되지 않아도 프로그램이 ill-formed일 수 있었음 | 이 경우 well-formed |
| P1009R2 | C++11 | new 표현식에서 배열 경계를 추론할 수 없었음 |
추론 허용 |