카테고리 없음

항목 45: "호환되는 모든 타입"을 받아들이는 데는 멤버 함수 템플릿이 직방!

FistKi 2021. 5. 27. 23:53

스마트 포인터(smart pointer) 일반적인 포인터처럼 동작하면서, 포인터가 주지못하는 다양한 기능을 제공하는 객체이다. 말만 들어서는 스마트 포인터가 완벽한 일반 포인터의 대체제처럼 들리지만, 스마트 포인터가 절대 대신할 없는 일반 포인터만의 특징이 있다.

  • 바로 암시적 변환(implicit conversion) 지원한다는 점이다.

 

이런 식의 타입 변환을 사용자 정의 스마트 포인터를 써서 흉내내기란 상당히 어렵다.

 

 

 

코드를 보면, 전부 같은 템플릿(SmartPtr)으로 만들어진 인스턴스들인데, 이들 사이에는 어떤 관계도 없기 때문에 컴파일러 입장에서 보면 SmartPtr<Top> SmartPtr<Middle> 완벽히 별개의 클래스이다.

 

변환을 가능하게 할려면 SmartPtr 클래스에서 변환을 지원할 타입에 대한 생성자를 지원해야 한다.

 

하지만 스마트 포인터를 구현하는 목적은 아무 타입의 자원이라도 관리하기 위함일탠데, 이를 위해서 무작정 생성자를 만들 없다.

 

하지만 템플릿을 활용하면 무제한 개수의 함수를 만들어낼 있다.

그러니까 SmartPtr 생성자 함수(function) 두는 것이 아니라 생성자를 만들어내는 템플릿(template) 사용하는 것이다.

 

위의 코드를 문장으로 말하면 다음과 같다.

모든 T타입과 U타입에 대해서, SmartPtr<T> 객체가 SmartPtr<U> 객체로부터 생성될 있다.

 

explicit 선언을 안한 것은, 기본제공 포인터는 포인터 타입 사이의 타입변환을 암시적으로 지원하기 때문에, 스마트 포인터도 이를 따라가는 것이다.

 

하지만, 기본 제공 포인터도

  • Top* -> Bottom* (dynamic casting)
  • int* -> double*

등을 암시적으로 지원하지 않기 때문에, 스마트 포인터도 이를 따라가야 한다.

 

auto_ptr shared_ptr에서 쓰는 방법을 그대로 사용해보자.

SmartPtr에서 get() 멤버 함수를 통해 해당 스마트 포인터 객체 내에 자체적으로 담긴 기본 제공 포인터의 사본을 반환한다고 가정하면(항목 15 참고), 이것을 이용하여 생성자 템플릿에 우리가 원하는 타입 변환 제약을 있다.

 

 

멤버 초기화 리스트를 사용해서, T* heldPtr = other.get(); 처럼 초기화하였다. T* U* 암시적 변환이 일어났는데 T*<->U* 기본제공 포인터의 변환이므로 타입 변환 제약이 걸린다.

 

정리하자면, SmartPtr<T> 일반화 복사 생성자는 호환되는(compatible) 타입의 매개변수를 넘겨받을 때에만 컴파일이 되도록 만들어졌다.


맴버 함수 템플은 복사 대입 연산에도 그대로 적용 가능하다. 예를 들면, shared_ptr 템플릿은 호환되는 모든 기본제공 포인터, shared_ptr, weak_ptr, auto_ptr 객체들로부터 생성자 호출이 가능하고, 대입 연산도 weak_ptr 제외한 나머지는 가능하다.

 

 

 

 

여기서 흥미로운 점은

  • explicit 선언이 되있는 것과, 안되있는 것이 있다.
  • auto_ptr 객체을 넘기는 생성자 대입 연산에서는 auto_ptr const 선언되있지 않다 : auto_ptr 복사 연산으로 인해 객체가 수정될 복사된 하나만 유효하게 남는다는 사실을 반영한 (항목 13 참고)

멤버 함수 템플릿을 사용할 주의할 점은, 이게 C++ 기본 규칙까지 바꾸지는 않는다는 점이다. 이때 기본 규칙이란,

  • 개발자가 따로 명시하지 않으면 컴파일러가 기본으로 생성하는 함수들이 있다.(기본 생성자, 기본 소멸자, 기본 복사 생성자, 기본 복사 대입 연산자)

복사 생성자, 복사 대입 연산자를 조심해야 한다. 지금 shared_ptr에는 분명히 일반화 복사 생성자가 선언되어 있는데,

 

"일반화 복사 생성자는 보통의 복사 생성자가 아니다"

 

라는 사실을 알아야 한다. , 일반화 복사 생성자만 있다고 가정할 T타입과 Y타입이 동일하게 들어온다면, 일반화 복사 생성자가 보통의 복사 생성자를 만드는 것이 아니라, 컴파일러가 보통의 복사 생성자가 없는 줄알고 만든 기본 복사 생성자가 호출된다는 것이다.

 

그래서 이와 같은 상황을 막을려면, 모든 복사 생성 과정을 내것으로 만들기 위해서는 보통의 복사 생성자도 같이 구현해주어야 한다.(물론 복사 대입 연산자도 동일하게 적용된다.)

 

 

 


  • 호환되는 모든 타입을 받아들이는 멤버 함수를 만들려면 멤버 함수 템플릿을 사용하자.
  • 일반화된 복사 생성 연산과 일반화된 대입 연산을 위해 멤버 템플릿을 선언했다 하더라도, 보통의 복사 생성자와 복사 대입 연산자는 여전히 직접 선언해야 한다.