"어떤 일이 있어도 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를 꼭 써야 할 때가 있긴 하다.
- 객체가 Derived 객체임이 분명할 때
- 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를 회피하는 방법이 두개 나왔다.
- 타입 안정성을 갖춘 컨테이너 사용(처음부터 Derived*를 넣는 방법)
- 가상 함수를 기본 클래스쪽으로 올려두는 방법
이 두 방법 중 상황에 맞게 사용하면 될 것 같다.
======================================================================
정말 피해야 하는 설계가 하나 있다. '폭포식 dynamic_cast'

만약 여기서 Window의 클래스 계통이 바뀌면? 항상 이런 코드는 뭐 또 넣고 뺄거 없나? 하고 검토 대상이 된다. 파생 클래스가 하나 추가되면 분기문 하나 추가임
=> 어떻게든 위 두개 방법 중 하나를 써서 dynamic_cast를 없애야됨
======================================================================
잘짜여진 C++ 코드는 cast가 거의 없다.
cast 같이 막 쓰기에 꺼림직한 문법들은 다르게 처리하는 방법이 다 있다. => 최대한 격리시키는 방법
예를 들어, 캐스팅을 해야 하는 코드를 내부 함수 속에 몰아 놓고(private 함수), 그 안에서 일어나는 '천한' 일들은 이 함수를 호출하는 외부에서 알 수 없도록 인터페이스로 막아두는 식으로 해결하면 된다.
- 다른 방법이 가능하다면 캐스팅은 피하자. 특히 수행 성능에 민감한 코드에서 dynamic_cast는 몇 번이고 다시 생각하자. 설계 중에 캐스팅이 필요해졌다면, 캐스팅을 쓰지 않는 다른 방법을 시도해보자.
- 캐스팅이 어쩔 수 없이 필요하다면, 함수 안에 숨길 수 있도록 하자. 이렇게 하면 최소한 사용자는 자신의 코드에서 캐스팅을 넣지 않고 이 함수를 호출할 수 있게 된다.
- 구형 스타일의 캐스트를 쓰려거든 C++ 스타일의 캐스트를 선호하자. 발견하기도 쉽고, 설계자가 어떤 역할을 의도했는지가 더 자세하게 드러난다.
'Effective C++ > 5. 구현' 카테고리의 다른 글
| 항목31: 파일 사이의 컴파일 의존성을 최대한 줄이자 (0) | 2021.04.24 |
|---|---|
| 항목 30: 인라인 함수는 미주알고주알 따져서 이해해 두자. (0) | 2021.04.24 |
| 항목 29: 예외 안전성이 확보되는 그날 위해 싸우고 또 싸우자! (0) | 2021.04.24 |
| 항목 28: 내부에서 사용하는 객체에 대한 '핸들'을 반환하는 코드는 되도록 피하자. (0) | 2021.04.24 |
| 항목 26: 변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자 (0) | 2021.04.24 |