malloc으로 받은 메모리를 구조체 포인터로 캐스팅하면 C++에서는 미정의 동작이다
![]()
네트워크에서 읽어 온 바이트 버퍼를 구조체 포인터로 캐스팅해서 바로 접근하는 코드, 한 번쯤 작성해보셨을 거예요. C에서는 아무 문제가 없는 이 패턴이 C++17까지는 엄밀히 말해 미정의 동작(UB)이었다는 사실을 아는 분은 생각보다 적어요. 이 차이의 근원에는 C++만의 독특한 개념인 객체 수명(object lifetime)이 자리 잡고 있어요.
같은 메모리 주소, 다른 객체 — C와 C++의 근본적 차이
C에서 포인터는 단순한 메모리 주소예요. 해당 주소의 바이트 표현을 원하는 타입으로 해석하기만 하면 되죠. 반면 C++은 한 가지 조건을 더 요구해요. 포인터가 가리키는 주소에 해당 타입의 살아 있는 객체가 존재해야 한다는 거예요.
객체 수명의 시작과 끝은 명확하게 정의돼 있어요. 시작은 적절한 정렬 조건과 크기의 스토리지가 확보되고 초기화가 완료된 시점이고, 종료는 소멸자 호출이 시작되거나 스토리지가 해제/재사용되는 시점이에요. 하나의 스토리지에서 여러 객체가 순차적으로 생성되고 소멸할 수 있다는 것도 중요한 포인트예요. 메모리 주소가 같고 스토리지도 동일하지만, 시점이 다르면 엄연히 서로 다른 객체인 거죠.

int에도 수명이 있고, reinterpret_cast는 객체를 만들지 않는다
놀라운 점은 int 같은 스칼라 타입에도 객체 수명이 존재한다는 거예요. placement new는 단순히 메모리의 비트 패턴을 바꾸는 연산이 아니에요. 해당 위치에 int 객체를 명시적으로 생성하고 수명을 시작시키는 연산이죠. reinterpret_cast는 포인터의 타입만 변경할 뿐, 대상 주소에 새로운 객체를 생성하지는 않아요. 따라서 reinterpret_cast로 얻은 포인터를 역참조하려면, 반드시 해당 주소에 그 타입과 일치하는 살아 있는 객체가 이미 존재해야 하거든요.
C++20이 실무 관행을 합법으로 만든 방법
C++20에 도입된 암묵적 객체 생성(implicit object creation)은 이런 실무적 관행을 합법화하는 방향으로 발전해 왔어요. malloc이나 memcpy 같은 특정 연산이 수행될 때, 프로그램을 합법적으로 만드는 데 필요한 객체가 암묵적으로 생성된 것으로 간주하는 거죠. 단, 포인터의 파생 관계와 `std::launder`도 함께 이해해야 해요. 비록 메모리에 살아 있는 객체가 존재하더라도, reinterpret_cast만으로는 그 객체를 가리키는 유효한 포인터를 얻지 못할 수도 있거든요. C에서 온 개발자가 "어차피 잘 돌아가는데 뭐가 문제야"라고 느끼는 코드가, C++ 표준의 관점에서는 시한폭탄일 수 있다는 걸 이 글이 잘 보여주고 있어요.