어처구니 없는 사실이지만, C++17까지는 std::vector를 지금처럼 짜는 것 자체가 엄밀히 따지면 UB였습니다.


gcc, clang, msvc 등등 모든 표준 라이브러리 구현자들이 UB 속에서 std::vector를 짠 것이죠.

그 이유는 이렇습니다.


std::vector는 reserve() 없이는 성립할 수 없는 존재입니다.

그런데 reserve() 짜려면 이런 식으로 해야 합니다.




여기에 디테일 좀 추가하면 모든 메이져 플랫폼에서 오류 없이 맞는 동작을 하는 reserve()를 만들 수 있고

메이져 구현체들 reserve()도 대충 이런 식입니다만, C++17 표준상으로는 #a, #b, #c 모두 UB입니다.


이유를 알아봅시다.


http://www.eel.is/c++draft/basic.compound#4


두 오브젝트 a, b는 다음 경우에 한해 포인터 호환이 가능하다:

- 같은 오브젝트거나.

- 하나가 공용체 오브젝트고 다른 하나가 그 오브젝트의 비정적 데이터 멤버거나.

- 하나가 standard-layout 클래스 오브젝트고, 다른 하나는 그 오브젝트의 첫 번째 비정적 데이터 멤버이거나, 그 오브젝트의 기반 클래스 중 하나 오브젝트거나.

- 중간 오브젝트 c가 있어서 a-c간 포인터 호환 가능하고, c-b간 포인터 호환 가능하거나.


두 오브젝트가 포인터 호환 가능하면 그 주소값이 같고,

reinterpret_cast를 통해 a를 가리키는 포인터에서 b를 가리키는 포인터로 변환할 수 있다.


(배열 오브젝트와 배열의 첫 번째 원소 오브젝트는 설령 주소값이 같더라도 포인터 호환 가능하지 않다.)



그런데 newbuf는 그냥 바이트 버퍼이고 T 타입의 오브젝트를 만들어 주지 않았습니다.

오브젝트가 이미 존재하지 않으면, 바이트 버퍼를 T*로 변환하는 것 자체가 UB인 것이죠.

표준상에 의하면 이 바이트 버퍼에 placement new로 T 오브젝트를 하나하나 만들어줘야 합니다.


size가 500, capacity가 500인데 push_back을 해서 reserve가 불렸고, capacity가 두 배로 늘어난다고 칩시다.

그러면 placement new를 newbuf의 0번째 T, 1번째 T, ..., 1000번째 T 자리까지 1000번을 하나하나 다 불러줘야 UB를 피할 수 있는 겁니다.

어떻게 봐도 미친 짓입니다. 당연히 major vendor들 그 누구도 그렇게 하고 있지 않습니다.


C++ 쓰려는 이유 자체가 성능 때문에 쓰는 거잖아요. 이런 모순을 견뎌야 C++을 할 수 있는 걸까요?


그래서 C++20부터는 다음의 표준안 개선을 제안했습니다.


"특정 함수들이나 동작들에 한해서는, 따로 추가 작업을 하지 않더라도 오브젝트가 만들어진 걸로 치자."


이 경우들은 다음과 같습니다:


  • Creation of an array of char, unsigned char, or std::byte implicitly creates objects within that array.

  • A call to malloc, calloc, realloc, or any function named operator new or operator new[] implicitly creates objects in its returned storage.

  • std::allocator<T>::allocate(n) implicitly creates a T[n] object in its returned storage; the allocator requirements should require other allocator implementations to do the same.

  • A call to memmove behaves as if it

    1. copies the source storage to a temporary area

    2. implicitly creates objects in the destination storage, and then

    3. copies the temporary storage to the destination storage.

    This permits memmove to preserve the types of trivially-copyable objects, or to be used to reinterpret a byte representation of one object as that of another object.

  • A call to memcpy behaves the same as a call to memmove except that it introduces an overlap restriction between the source and destination.

  • A call to std::bit_cast implicitly creates objects in the result, to handle the case where the destination type contains a union.

  • A new barrier operation (distinct from std::launder, which does not create objects) could be introduced to the standard library, with semantics equivalent to a memmove with the same source and destination storage. Prior versions of this document suggested:


이로 인해서, "여기 나온 것들에 한정된 도구들을 잘 쓰면" std::vector를 짜는 것이 UB가 아니게 되었다는 이야기입니다.


원문 : https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p0593r6.html