본문 바로가기
NC University/Advanced C++

4일차 - trivial

by 날쑤 2015. 7. 16.

trivial

  일반적으로 클래스의 생성자(혹은 복사생성자, 소멸자)가 아무일도 하지 않으면, 이를 trivial하다고 한다. 그럼 '아무 일도 하지않는' 생성자는 정확히 어떤 것을 의미하는 것일까? C++에서는 명시적으로 생성자를 구현하지 않으면 컴파일러가 자동으로 생성자를 만들어준다. 개발자가 생성자를 명시적으로 작성하지 않았다는 것은 기본적으로 생성자에서는 별도의 일을 하지 않겠다는 의미를 담고 있으니, 디폴트 생성자는 trivial하지 않을까라는 생각을 가질 수 있다. 이를 확인하기 위해 아래의 코드를 살펴보자.

  위의 코드에서 클래스 B에는 생성자가 없다. 그러므로 컴파일러는 자동으로 B에 생성자를 추가할 것이다. 이 때, B의 디폴트 생성자는 trivial할까? 만약에 그렇다면 클래스 B로 부터 객체를 생성자 호출없이 만들었을 때, 이 객체는 아무 문제없이 동작해야한다. 한번 시도해보자.

  위의 코드를 수행하면 크래시가 발생한다. 그런데 2번째 라인의 주석을 제거하고 코드를 수행하면 정상적으로 동작한다. 결국, 위의 코드는 생성자 호출을 안해서 죽은 것이었고, 클래스 B의 생성자는 뭔가 하는 일이 있다(non-trivial)는 결론에 도달하게 된다.

  그렇다면 개발자가 별다른 의미를 부여하지 않은 B의 (디폴트) 생성자에서는 어떠한 일들이 수행되는 것일까? 우선 B는 A를 상속받기 때문에 부모인 A의 생성자를 호출한다. 그리고 B는 가상함수를 가지므로 가상함수 테이블도 여기서 초기화된다. 이 코드에는 해당되지 않지만 만약에 B가 객체인 멤버변수를 가졌다면, 이들에 대한 생성자도 호출되어야 한다. 정리하면 일반적으로 디폴트 생성자는 다음의 작업들을 수행하도록 코드가 만들어진다.

  1. 부모 클래스가 있다면 부모 클래스의 생성자 호출
  2. 객체인 멤버변수들이 있다면 이들의 생성자 호출
  3. 가상함수가 있을 경우 가상함수 테이블 초기화

  그런데 위의 작업들은 모두 조건부로 수행되는 일들이다. 그러므로 각 작업들이 수행될 조건들이 모두 충족되지 않는다면 그 때는 생성자가 아무 일도 하지않는, 즉 trivial한 생성자라고 할 수 있을 것이다. 위의 내용을 바탕으로 생성자가 trivial인 조건들을 다시 써보면 다음과 같다.

  1. 부모 클래스가 없음 (있다면 부모 클래스의 생성자가 trivial)
  2. 객체인 멤버변수가 없음 (있다면 이들의 생성자가 trivial)
  3. 가상함수가 없음
  4. 명시적인 생성자가 없음 (있다면 비어있는 함수)

trivial 개념의 필요성

  모든 타입의 배열을 복사하는 strcpy 함수의 일반화 버전을 생각해보자. 함수의 시그니쳐는 아래와 같을 것이다. 그리고 배열이므로 복사 방식은 단순하게 memcpy를 적용할 수 있을 것이다.

  그런데 만약에 타입T의 멤버에 동적할당을 하는 변수가 있다면 어떻게 될까? 아마도 얕은 복사(shallow copy)가 일어날 것이다. 요소의 복사는 단순히 메모리 복사가 아닌 T를 정의한 사람이 의도한대로 적용되어야 하며, 이를 정의하는 것이 바로 복사 생성자이다. 그렇다면 위의 함수를 각 요소별로 복사 생성자를 사용하도록 수정하면 문제가 해결되는 것으로 생각할 수도 있다. 아마도 정상적으로 동작할 것이다. 하지만 위에서 언급했던 것처럼 복사 생성자가 trivial하다면 요소 별로 복사 생성자를 적용하는 것은 memcpy를 적용하는 것에 비해 성능저하를 가져올 것이다. 그러므로 가장 바람직한 해답은 두 케이스를 나눠서 처리하도록 하는 것이다. 이제 이를 구현해보자.

  이제는 어떤 상황에서도 성능저하없이 배열을 복사할 수 있다. 한발짝 더 나아가 만약에 T의 복사 생성자가 trivial이라면 컴파일러는 최적화 과정에서 memset을 제외한 모든 코드를 지워버릴 것이다. 이는 위의 분기중 어디를 탈지 컴파일 타임에 결정할 수 있기 때문이다. 그리고 copy_type 함수 자체도 인라인으로 선언되었기 때문에 호출자 쪽에서 복사 생성자가 trivial한 타입의 배열로 이 함수를 호출할 경우 그 부분은 memcpy로 인라인 치환될 것이다.