본문 바로가기
Object Oriented Programming(C++)/Effective C++

Effective C++ | 항목 27 캐스팅은 절약, 또 절약! 잊지 말자

by continue96 2023. 3. 6.

이펙티브 C++ 항목 27

 

항목 27 캐스팅은 최대한 절약하자

27.1 캐스팅 문법

 27.1.1 캐스트 스타일

  • C 캐스트(구형 스타일 캐스트) 
    • (T) 표현식
    • T (표현식)
  • C++ 캐스트(신형 스타일 캐스트)
C++ 캐스트 설명 예시
const_cast<T>(표현식) 객체의 상수성(constness) 혹은 휘발성(volatileness)을 없애는 용도로 사용한다. const → non-const 
static_cast<T>(표현식) 암시적 변환을 진행하거나 타입을 거꾸로 변환하는 용도로 쓰인다. int → double
void* → int*
Parent* → Child*
reinterpret_cast<T>(표현식) 하부 수준 캐스팅을 위해 만들어진 연산자다. 구현 환경에 의존적이므로 이식성이 없다. int → char*
dynamic_cast<T>(표현식) 안전하게 다운 캐스팅할 때 사용하는 연산자다. Parent* → Child*

 

27.1.2 C++ 캐스트의 장점

  • 타입 시스템이 망가졌을 때 코드를 알아보기 쉬워 찾아보는 작업이 편리하다.
  • 사용 목적이 드러나기 때문에 캐스트를 잘못 사용했을 때 컴파일러가 오류를 파악할 수 있다.

 

27.2 캐스팅 동작 방식

  • 다른 타입으로 변환될 때 실행 시간(run time)에 코드가 만들어진다.
// int 타입의 x가 double 타입으로 명시적으로 캐스팅되는 부분에서 코드가 만들어집니다.
int x, y;
double d = static_cast<double>(x) / y;

// Child* 타입의 c가 Parent* 타입으로 암시적으로 캐스팅되는 부분에서 코드가 만들어집니다.
class Parent { ... };
class Child : public Parent { ... };
Child c;
Base* p = &c;

 

  • C++에서는 데이터가 메모리에 있을 것이라고 가정할 수 없다.
    • 어떤 객체의 주소를 다른 타입의 포인터로 바꿔서 포인터 연산을 하는 코드는 정의되지 않은 동작을 보인다.
    • 컴파일러마다 객체를 메모리에 할당하는 구조와 주소를 계산하는 방법이 다르다.

 

27.3 잘못된 캐스팅

 27.3.1 static_cast 남용

  • static_cast<T>(*this)는 부모 클래스에 대한 임시 객체를 만든다.
    • 임시 객체에 대해 onResize 함수를 호출하고 원본 객체에 대해 onResize 함수는 호출되지 않아 문제가 발생한다.
    • 문제를 해결하려면 캐스팅을 사용하지 않고 부모 클래스의 onResize 함수를 올바르게 호출한다.
// 부모 클래스입니다.
class Window {
public:
	virtual void onResize() { ... }
};

// 자식 클래스입니다.
class SpecialWindow : public Window {
public:
	virtual void onResize() {
		// 부모 클래스의 onResize 함수를 올바르게 호출합니다.
		Window::onResize();

		// 부모 클래스의 사본에 onResize 함수를 잘못 호출합니다.
		static_cast<Window>(*this).onResize();
		...
	}
};

 

 27.3.2 dynamic_cast 남용

  • dynamic_cast는 대부분 구현 환경에서 매우 느리게 동작한다.
    • 폭포식(cascading) dynamic_cast 구조는 매우 느리고 망가지기 쉽다는 문제가 발생한다.
class Window { ... };

class SpecialWindow1 : public Window { ... };
class SpecialWindow2 : public Window { ... };
class SpecialWindow3 : public Window { ... };

// 부모 클래스를 가리키는 포인터를 배열에 저장합니다.
vector<shared_ptr<Window>> winPtrs;

// 부모 클래스 포인터를 일일이 다운캐스트해서 자식 클래스 객체에 접근합니다.
for (auto iter = winPtrs.begin(); iter != winPtr.end(); ++iter) {
	if (SpecialWindow1* psw1 = dynamic_cast<SpecialWindow1>(iter->get())) { ... }
	else if (SpecialWindow2* psw2 = dynamic_cast<SpecialWindow2>(iter->get())) { ... }
	else if (SpecialWindow3* psw3 = dynamic_cast<SpecialWindow3>(iter->get())) { ... }
}

 

 

  • 첫 번째 해결 방법은 자식 클래스 객체에 대한 포인터를 컨테이너에 저장한다.
    • 각 객체를 자식 클래스 인터페이스를 통해서 접근한다.
class Window { ... };

class SpecialWindow : public Window {
public:
	void blink();
};

// 자식 클래스를 가리키는 포인터를 배열에 저장합니다.
vector<shared_ptr<SpecialWindow>> winPtrs;
for (auto iter = winPtrs.begin(); iter != winPtrs.end(); ++iter) {
	// 자식 클래스 포인터로 함수를 호출합니다.
	(*iter)->blink();
}

 

 

  • 두 번째 해결 방법은 가상 함수를 기본 클래스에 선언하고 자식 클래스에서 재정의(override)한다.
    • 각 객체에서 재정의된 함수를 부모 클래스의 포인터로 호출한다.
class Window {
public:
	virtual void blink() { }
};

class SpecialWindow : public Window {
public:
	virtual void blink() { ... }
};

// 부모 클래스를 가리키는 포인터를 배열에 저장합니다.
vector<shared_ptr<Window>> winPtrs;
for (auto iter = WinPtrs.begin(); iter != WinPtrs.end(); ++iter) {
	// 부모 클래스 포인터로 재정의된 함수를 호출합니다.
	(*iter)->blink();
}

 

NOTE
① 가급적이면 캐스팅을 사용하지 않는다. 특히 성능이 중요한 코드에서 dynamic_cast는 사용하지 않는다.
② 캐스팅이 꼭 필요한 경우, 함수 안에 캐스팅을 숨겨 사용자가 직접 캐스팅을 사용하지 않게 한다.
③ C 스타일 캐스팅보다 C++ 스타일 캐스팅을 선호하자. 의도가 더 분명하고 발견하기도 쉽다.

댓글