Placement New를 통해 Trivial 객체를 올바르게 복사하는 방법

2022. 3. 3. 21:49C++

[원문 링크]

 

위 링크에서는 다음 질문을 하고있다.

T가 trivial 타입이면 memcpy를 통해 오브젝트를 복사할 수 있으며, trivial 타입의 default initialization은 오브젝트의 representation을 변경하지 않으므로, 아래의 코드는 복사된 오브젝트를 올바르게 초기화 하는가?
template <typename T>
T* copy_trivial(T orig) requires std::is_trivial_v<T>
{
    void* buf = std::aligned_alloc(alignof(T), sizeof(T));

    // Note the order of these statements
    std::memcpy(buf, &orig, sizeof(T));
    return new(buf) T;
}

 

위 코드는 다음과 같이 수행된다.

1. 힙에 T 타입의 alignment requirement를 만족하는 sizeof(T)의 메모리 할당
2. 인자로 전달된 T 타입의 오브젝트를 할당한 힙 메모리에 memcpy

3. placement new를 통해 오브젝트 생성

 

위 함수는 유효한 포인터를 반환하며, 언뜻보면 문제가 없어보이지만, 위 함수를 사용하는 곳에서 반환된 오브젝트의
서브 오브젝트(멤버 데이터)를 접근 할 경우, 정의되지 않은 동작(Undefined Behaviour)이 발생한다.

 

문제는 2번과 3번의 순서에 있는데,

3번에서 어떠한 동작이 발생하는지 알면 문제를 해결할 수 있을 것이다.

 

그럼 3번을 분석해보자.

 

[expr.new#23.1]

If the new-initializer is omitted, the object is default-initialized ([dcl.init]).
[ Note: If no initialization is performed, the object has an indeterminate value. — end note ]

위 코드에서 placement new는 initializer를 생략했으며,

위에서 말하는 것과 같이, new 연산자에서 initializer를 생략하면 default initialization이 수행된다.

 

[dcl.init.general#7]

To default-initialize an object of type T means:
(7.1) If T is a (possibly cv-qualified) class type ([class]), constructors are considered. ...
The constructor thus selected is called, with an empty argument list, to initialize the object.
(7.2) If T is an array type, each element is default-initialized.
(7.3) Otherwise, no initialization is performed.

trivial 타입은 trivial default constructor를 가지며, 모든 서브 오브젝트 역시 trivial 타입이다.
결국 trivial 타입에 default initialization이 수행되면 서브 오브젝트에 대해 재귀적으로
default initialization이 수행되며, 최종적으로 no initialization이 수행된다.

자, [expr.new#23.1]를 다시 보자.

[ Note: If no initialization is performed, the object has an indeterminate value. — end note ]

 

따라서 no initialization이 수행되었기 때문에 indeterminate value를 가지게 되며,

 

[basic.indet#2]

If an indeterminate value is produced by an evaluation, the behavior is undefined except in the following cases:
...

indeterminate value를 가지는 expression을 evaluation하는 것은 undefined behaviour로 정의된다.
(예외가 있긴하다. 위 링크 참조)

 

다른 관점에서도 indeterminate value를 가질 수 있다.

 

[basic.indet#1]

When storage for an object with automatic or dynamic storage duration is obtained, the object has an indeterminate value, and if no initialization is performed for the object, that object retains an indeterminate value until that value is replaced ([expr.ass]).

[Note 1: Objects with static or thread storage duration are zero-initialized, see [basic.start.static]. — end note]

 

그런데 placement new가 주소를 반환하긴 하는데.. storage를 obtain한다고 보는게 맞을까?

 

[expr.new#11]

A new-expression may obtain storage for the object by calling an allocation function.

맞댄다.

 

이제 문제가 뭔지 알 수 있다.

오브젝트는 생성 되었지만 값이 초기화가 안되었다!

 

이제 2번과 3번의 순서를 바꿔보자.

T* t = new(buf) T;
std::memcpy(t, &orig, sizeof(T));
return t;

1. placement new를 통해 초기화가 안된 오브젝트를 생성했으며,
2. memcpy를 통해 인자로 넘어온 오브젝트의 값을 복사했다.

 

이제 반환된 오브젝트를 사용해도 Undefined Behaviour가 발생하지 않는다.

 

 

C++20부터, 위 코드에서 placement new는 필요가 없다.

Implicit Object Creation에 의해, trivial 타입에 대해서 memcpy는 목적지에 T 타입 오브젝트를 생성한다.
[참조1, 참조2]