본문 바로가기

Effective C++/5. 구현

항목 27: 캐스팅은 절약, 또 절약! 잊지 말자

"어떤 일이 있어도 type error 생기지 않도록 보장한다" => C++ 동작 규칙이다.

 

, 이론적으로는 컴파일만 되면, 이후엔 어떤 객체에 대해서도 불완전한 연산, 말도 안되는 연산 등을 수행하지 않는다는 것이다.

 

하지만!!! cast 시스템 때문에, 이런 보장이 깨질 있다..

 

C++에서 cast 항상 조심 조심 해야한다.

 

일단 C++에서 제공하는 casting 문법부터 정리하면

  • (T) expression
  • T(expression)

 

여기까지가 구형 캐스트 이다.

 

  • const_cast<T>(expression)
  • dynamic_cast<T>(expression)
  • reinterpret_cast<T>(expression)
  • static_cast<T>(expression)

 

4개는 C++에서 독자적으로 제공하는 casting 이다.

 

  • const_cast

객체의 상수성을 없애는 용도, const -> non-const 바꾸는 용도

오로지 cast로만 가능하다

 

  • dynamic_cast

안전한 down-casting 쓰인다. inheritance 상황에서 쓰이며, 주어진 객체가 어떤 클래스 상속 계통에 속한 특정 타입인지 아닌지를 결정하는 작업에 쓰인다. (구형 캐스트로 불가능, 런타임 비용이 매우 높다.)

Derived => Base 안전하기 때문에 static

Base => Derived 안전하지 않기 때문에 dynamic

 

  • reinterpret_cast

포인터->int 바꾸는 등의 하부 수준 캐스팅을 위해 만들어진 연산자이다. => 적용 결과가 구현환경에 의존적(이식성이 없다), 이런 캐스트는 하부 수준 코드 외에는 없어야 한다.( 일이 거의 없다)

 

  • static_cast

암시적 변환을 강제로 진행할 사용한다. 빈번히 이루어지는 타입 변환들을 거꾸로 수행하는 용도(예를 들어 void* -> int* , base* -> derived*) , 이걸로 non-const -> const 가능하지만 const->non-const 안된다.

 

==============================================================

 

구형 캐스트는 요새도 쓰일 수는 있지만, C++ 스타일의 캐스트를 쓰는것이 바람직하다. 왜냐하면 알아보기 쉽고, 캐스트를 사용한 목적을 좁혀서 지정할 있기 때문이다.

 

그리고 구형 캐스트, 솔직히 군데 밖에 안쓰임 -> 객체를 인자로 받는 함수에 객체를 넘기기 위해 명시호출 생성자를 호출하고 싶을 경우

 

 

 

casting으로 객체를 생성하는 .. 뭔가 구형이 그럴 해보이지만, 그래도 신형을 쓰는 것이 편할 것이다.

 

 

=================================================================

 

캐스팅 <- 그냥 어떤 타입을 다른 타입으로 처리하라고 컴파일러에게 알려 주는 역할

=> 크나큰 오해!!!!

 

일단 타입 변환 발생 => 런타임에 실행되는 코드가 생성되는 경우가 많음 => 수행 성능에 영향

 

만약에 이런 코드가 있다고 생각하면

 

 

x, y int 임에도 x double cast되면서 double 나눗셈을 사용한다.

아니면 이런 코드는 어떤가?

 

 

여기서 포인터의 값이 같지 않을 때도 가끔 있다. 이런 경우가 되면, 포인터의 변위(offset) Derived* 적용하여 실제 Base* 포인터의 값을 구하는 동작이 런타임에 이루어진다.

 

=================================================================

 

객체 하나(Derived) 가질 있는 주소가 오직 개가 아니라, 이상이 수도 있다.

이건 C++에서만 보이는 특징임

 

다중 상속이 사용되면 이런 현상이 항상 생기고, 단일 상속인데도 이런 현상이 생기는 경우가 간혹 있다.

 

만약 이런 객체의 주소를 char* casting 포인터 산술 연산을 적용하는 등의 코드는 거의 항상 미정의 동작을 낳을 있다.

 

그리고 간혹이란 말에 주의를 해야 하는게, 객체의 메모리 배치구조를 결정하고, 객체의 메모리 주소를 계산하는 방법은 컴파일러마다 천차만별이다. -> 플렛폼에 따라 때도 있고, 안될 때도 있기 때문에 그냥 casting 자체를 안하는게 낫다.

 

=================================================================

 

 

코드는 제대로 동작할까? => no

 

Window 캐스팅했으니, 동작하는 onResize() Window 것이 호출되야 것이다. 근데 함수 호출이 이루어지는 객체가 현재의 객체가 아니다!!

 

? => *this cast => *this 기본 클래스에 대한 Copy 만들어짐!!! -> 임시 Copy onResize() 호출 => logical error!!!

 

실제로 확인해보자.

 

 

진짜 copy 만들어진다.

 

만약 상태에서, Window::onResize() 객체를 수정하도록 만들어져있었으면, 실제론 수정이 반영되지 않을 것이다.

 

=> 그냥 캐스팅을 빼면

 

"캐스트 연산자가 입맛 당기는 상황이면, 뭔가 꼬여가는 징조이다."

 

특히 dynamic_cast 땡길 => 진짜 거의 안쓰임(속도가 너무 느려서)

 

 

RTTI 이용한 동적 타입 변환

 

dynamic_cast 써야 때가 있긴 하다.

  1. 객체가 Derived 객체임이 분명할
  2. Base->Derived 필요할 (예를 들어, 객체를 조작할 방법이 Base*, Base& 밖에 없을 )

 

이럴 다음과 같이 하지말고 (dynamic_cast 사용중!)

 

 

Window casting 하지 말고, blink SpecialWindow에만 있다고 하면, 그냥 SpecialWindow 바로 쓰자.

 

wrong_example if 문장이 되는지 설명하자면, C++에선 어떤 맴버함수에 참조할 , 실제 객체가 아니라 타입 이름가지고 판단함. 그래서 psw 가르키는 객체는 Window 이지만, 타입이 SpecialWindow이기 때문에 SpeicalWindow blink() 실행시킴.

 

근데 SpecialWindow 지금 정의도 안됐는데, 이게 가능한가? => 맴버함수는 그냥 함수인데 클래스에 유효범위가 제한(this 포함함)되고, 클래스 내의 맴버에 접근할 있는거 뿐임. this 있는 전역함수라고 봐도 된다. => 타입이 맞고 함수가 실제 있으므로 그냥 blink 불러올 있다.

 

근데 this 있어야 하지 않나? 실제 객체가 있어야 this 그걸 받아서 실행되지 않나?

for문에서 생성되는 psw 값을 출력해보니 0 나왔다. => this 0이여도 this 참조하지 않으면 실행은 되는 하다. this 참조하는 순간( *this 하는 순간, 클래스의 다른 데이터에 접근하는 순간) 프로그램이 뻗어버림 => 이런 짓은 안하는게 낫다.

 

 

 

포인터가 0 뜨는지 알았다!!!

 

 

shared_ptr말고 그냥 포인터를 쓰면 바로 알게 된다. vector 생성자가 자동으로 element들을 0으로 초기화함!!!, 그래서 만약 안에 실제 Window 할당해서 넣어주면

 

 

우리가 원하는 의도대로 ;

 

실제 객체는 SpecialWindow 이지만, 기본 클래스의 인터페이스를 사용하여 여러 파생 클래스를 winPtrs 한번에 다루기 위해 Window* casting 해서 저장( 이건 안전한 casting, derived -> base 이기 때문에)

 

======================================================================

 

 

하지만 우리가 사실 dynamic_cast 쓰고 싶었던 이유는, Window에서 파생되는 많은 클래스들을 전부 기본 클래스 인터페이스를 통해 조작하고 싶기 때문일 것이다. 그냥 SpecialWindow 포인터를 사용하면 cast 안해도 되지만, VPSW Window 다른 자손들은 조작할 없다.

 

 

=> 그럼 Window 포인터를 컨테이너에 담아두되, casting 없이 하위 클래스들을 조작할 있는 방법이 없을까?

 

 

blink 기본 클래스에서 virtual 하나 만듬. 물론 아무것도 안하는 형태로

(참고로 가상 함수의 기본 구현은 좋지 않은 아이디어이다. 항목 34 참고)

 

 

지금 dynamic_cast 회피하는 방법이 두개 나왔다.

 

  1. 타입 안정성을 갖춘 컨테이너 사용(처음부터 Derived* 넣는 방법)
  2. 가상 함수를 기본 클래스쪽으로 올려두는 방법

 

방법 상황에 맞게 사용하면 같다.

 

======================================================================

 

정말 피해야 하는 설계가 하나 있다. '폭포식 dynamic_cast'

 

 

 

 

만약 여기서 Window 클래스 계통이 바뀌면? 항상 이런 코드는 넣고 뺄거 없나? 하고 검토 대상이 된다. 파생 클래스가 하나 추가되면 분기문 하나 추가임

 

=> 어떻게든 위 두개 방법 하나를 써서 dynamic_cast 없애야됨

 

======================================================================

 

 

잘짜여진 C++ 코드는 cast 거의 없다.

cast 같이 쓰기에 꺼림직한 문법들은 다르게 처리하는 방법이 있다. => 최대한 격리시키는 방법

예를 들어, 캐스팅을 해야 하는 코드를 내부 함수 속에 몰아 놓고(private 함수), 안에서 일어나는 '천한' 일들은 함수를 호출하는 외부에서 없도록 인터페이스로 막아두는 식으로 해결하면 된다.

 

 

  • 다른 방법이 가능하다면 캐스팅은 피하자. 특히 수행 성능에 민감한 코드에서 dynamic_cast 번이고 다시 생각하자. 설계 중에 캐스팅이 필요해졌다면, 캐스팅을 쓰지 않는 다른 방법을 시도해보자.
  • 캐스팅이 어쩔 없이 필요하다면, 함수 안에 숨길 있도록 하자. 이렇게 하면 최소한 사용자는 자신의 코드에서 캐스팅을 넣지 않고 함수를 호출할 있게 된다.
  • 구형 스타일의 캐스트를 쓰려거든 C++ 스타일의 캐스트를 선호하자. 발견하기도 쉽고, 설계자가 어떤 역할을 의도했는지가 자세하게 드러난다.