C++: 객체를 생성해낸다는 것

C++에서 객체를 생성한다는 것은 크게 2 단계의 작업이 필요하다. 코드에선 이 두 연산이 하나의 구문처럼 보이지만 별개다.

  • 객체를 저장하기 위한 메모리 공간을 확보하는 것
  • 객체 자체가 “초기화” 되는 것

이렇게 두 가지이고, C++에서는 이 두 가지를 구분할 수 있는 문법적인 도구를 제공한다.

예제로 사용하기 위해서, 이런 클래스가 하나 있다고 하자.

class Base {
 protected:
  int x;
 public:
  Base() {x = 1;}
  virtual ~Base() { }
  virtual void DoSomething() {
    cout << "Base:" << x << endl;
  }
};

메모리 할당 + 초기화를 동시에

우선 두 단계가 “동시에” 일어나는 코드를 하나 보자.

Base* x = new Base();

new가 메모리를 할당해주고, Base() 함수가 데이터를 초기화한다.

메모리 할당과 초기화를 분리

  • 전역 new 함수를 호출해서 메모리를 할당하고
  • 할당된 메모리에 초기화를 수행하는1

예제를 하나 들어보면 이런 것.

Base* y = new (static_cast<Base*>(operator new(sizeof(Base)))) Base();

// or equivalent allocation/initialzation
Base* p = static_cast<Base*>(operator new(sizeof(Base))); // 생성
new (p) Base(); // 초기화(생성자호출)

참고로 처음 한 줄 / 나중 2줄이 같은 의미다. operator new( 크기 ) 로 호출한 코드가 전역 new를 써서 메모리 할당2 + 그러고 나서 new (메모리 위치) 생성자() 호출로 초기화를 수행한다.

다만 이런 동작을 하고나면 소멸자도 직접 호출해줘야하고, 메모리 해제도 따로 해야한다. 즉, 이런 코드를 사용해야 한다.3

p->~Base();
delete static_cast<void*>(p);

이걸 알면 뭐에 써먹을 수 있냐고? 메모리 할당과 객체 생성이 분리되었다는 사실을 알면,

  • intel TBB의 scalable malloc 같은 함수를 호출해서 메모리를 할당하고 / 객체를 만들어낼 수 있고4
  • 임시 객체를 (소멸자가 필요없다면 더 좋고) 다형성을 쓸 수 있는 형태로 잠시 스택에 할당하고 써버릴 수도 있고5
  • 메모리 풀링 / 객체 생성 프레임웍을 작성할 수도 있다6

뭐 이런 것들이 가능하다.7 반면에 이렇게 사실 상 두 단계인 객체생성을 이해 하지 못하면 다음과 같은 경우를 볼 수 있다.

  1. malloc() 으로 할당하고 — 당연히 생성자는 호출 안되고 — 객체를 사용하려고 시도.
  2. memcpy 혹은 유사한 형태로 객체의 데이터만 가지고 있는(초기화된)상태에서 가상함수를 호출.8 이 경우엔 참 재미있는 에러메시지를 볼 수 있다.

1의 경우는 학부생 시절에 프로젝트 수업에서 같은 팀원의 코드 — C 프로그래머고 C++은 잘 모르는 — 에서 발견했다. 생성자가 없는 C 의 부족한 추상화에 당한 경우라고 생각한다.

2의 경우는 사실 이 토픽과는 약간 거리가 있긴하지만, 놀랍게도 회사원 생활 중에 발견. C++의 추상화가 애매한 수준이라 발생한 문제인 것 같다. OS/Compiler에 따라선 virtual function table pointer를 잘 조작해서 실제로 객체가 생성된 것처럼 동작하게 만들 수 있지만, “확신이 서도” 왠간하면 하지 말 짓이겠지. 근데 저게 왜 안되는지 이해하지 못하고 있는건 좀 보고있기 괴로웠다(…).

여튼 한 줄 요약.

C++의 객체 생성은 할당/초기화 2 단계고, 이걸 잘 이용하면 성능을 올리거나 편의성을 증대할 수 있다.


  1. displacement-new operator; 위치지정 new 연산자라고도 한다. ↩︎

  2. C에서 malloc() 류의 함수를 써본 사람들은 익숙할 듯. ↩︎

  3. 대조적으로 동시에 할당/초기화 했던 코드는 delete x; 면 된다. ↩︎

  4. 상대적으로 cache 효율이 좋거나, 코어 수가 많을 때도 효율적인 할당자를 선택적으로 쓸 수 있다. ↩︎

  5. 스택 객체로 다형성 쓰는거 참 골치아픈일인데 쉽게 된다. 다만 소멸자가 필요하면 직접 호출해야하는 것은 괴롭다. ↩︎

  6. 메모리를 프로그램 시작 시에 정적 혹은 동적으로 왕창 할당받고, 여기에다 대고 displacement-new를 호출해서 객체를 만들고, 소멸자 호출해서 없애는 식으로 사용하기 쉬워진다. 그리고 이런데는 domain-specific-knowledge를 쓰면 성능 올리기 좋을 걸? ↩︎

  7. 이게 C++의 저레벨 접근과 추상화의 힘? 혹자는 이런것 때문에 너무 복잡하다고 말하기도 하겠다. ↩︎

  8. 비가상함수들은 잘 호출될 수도 있지만. ↩︎