CPP Module 06 (2)

“CPP Module 06에 대하여”

C++ casts

reinterpret cast를 다시 쓸 날이 올까?

ex01 Serialization

본 과제는 사실 본질적인 의미의 Serialization과는 차이가 있다고 한다…

intptr_t와 reinterpret_cast

  • intptr_t와 uintptr_t는 포인터의 주소를 저장하는데 사용되는 자료형이다. 두 타입은 다른 환경으로 이식이 가능하고, 안전한 포인터 선언 방법을 제공한다. 시스템 내부에서 사용하는 포인터와 같은 크기이다.

  • 포인터를 정수 표현으로 변환할 때 유용하게 사용할 수 있다.

  • uintptr_t 타입은 intptr_t의 부호 없는 형태이다.

  • 대부분의 연산에서 intptr_t를 사용하는 것이 좋으며, uintptr_t는 intptr_t 타입만큼 유연하지 않고 사용이 제한적이다.

  • 아래는 본인이 과제를 진행하면서 확인한 사항들이다.

#include <iostream>
#include <cstdint>
#include "../00/color.hpp"

int main(void)
{
    int num = 100; 
    // 평범한 정수

    intptr_t	i = reinterpret_cast<intptr_t>(&num);
    // 의 포인터를 reinterpret cast한 정수 i (intptr_t도 일종의 정수)

    intptr_t	*pi = reinterpret_cast<intptr_t*>(&num);
    // 평범한 정수 포인터(int*)를 intptr_t*로 변환 (포인터 -> 포인터 변환)

    std::cout<< "num : " << num << std::endl;
    // 평범한 정수

    std::cout<< "&num : " << &num << std::endl;
    // 의 포인터

    std::cout<< "intptr_t : " << i << std::endl;
    // 평범한 정수의 포인터를 reinterpret cast한 값 i

    std::cout<< "Hexed intptr_t : " << std::hex << "0x" << i << std::endl;
    // 을 16진수로 나타낸 것

    std::cout<< "intptr_t* : " << pi << std::endl;
    // 정수의 포인터를 intptr_t*로 변환한 값 pi

    std::cout<< "*(intptr_t*) : " << *pi << std::endl;
    // 를 역참조한 값 (원본 intptr_t와 다른 값임)

    std::cout<< "Dec *(intptr_t*) : " << std::dec << *pi << std::endl;
    // 을 십진수로 나타낸 것 (마찬가지로 원본과 다른 값)

    std::cout<< "&intptr_t : "<< &i <<std::endl;
    // 평범한 정수 num을 reinterpret cast한 intptr_t i의 주소값

    std::cout<< "intptr_max : " << std::dec << INTPTR_MAX <<std::endl;
    // 궁금해서 그냥 넣어본 INTPTR_MAX 값 (long long max와 동일)

    int		*dp_num = reinterpret_cast<int*>(pi);
    // 평범한 정수 num을 reinterpret cast한 intptr_t i의 포인터 pi를 
    // 다시 reinterpret cast한 정수 포인터 dp_num

    int		*d_num = reinterpret_cast<int*>(i);
    // 평범한 정수 num을 reinterpret cast한 intptr_t i를 
    // 다시 reinterpret cast한 정수 포인터 d_num

    std::cout<< "DeSerial &num : " << dp_num << std::endl;
    // 원본 정수의 포인터

    std::cout<< "DeSerial num : " << *dp_num << std::endl;
    // 를 역참조한 값

    std::cout<< "DeSerial &num : " << d_num << std::endl;
    // 마찬가지로 원본 정수의 포인터

    std::cout<< "DeSerial num : " << *d_num << std::endl;
    // 를 역참조한 값
}


  • 보기 쉽게 정리해보자면 아래와 같다.
<< Output >>
num : 100
&num(int*) : 0x7ffee7119b0c
intptr_t : 140732775111436
Hexed intptr_t : 0x7ffee7119b0c
intptr_t* : 0x7ffee7119b0c

// ---- Undefined behaviors ---- //
	*(intptr_t*) : ebb0fb2800000064
	Dec *(intptr_t*) : -1463393729678147484
// ----------------------------- //

&intptr_t : 0x7ffee7119b00
intptr_max : 9223372036854775807
DeSerial &num : 0x7ffee7119b0c (intptr_t* -> int*, 포인터 -> 포인터 연산)
DeSerial num : 100 (intptr_t* -> int* -> int)
DeSerial &num : 0x7ffeeab2eb0c (intptr_t -> int*, 정수 -> 포인터 연산)
DeSerial num : 100 (intptr_t -> int* -> int)

!주의할 부분!

(1) int*와 intptr_t*의 차이점
 ⇒ int* 는 int 값을 가리키는 포인터 타입이고, intptr_t*는 intptr_t 값을 가리키는 포인터 타입이다.
  이게 무슨 당연한 소리냐는 말이 나올 수 있지만, 위의 Output에 정리된 Undefined behavior를 참고해보면……?

(2) *(intptr_t*) ≠ intptr_t
 ⇒ int num의 주소 &num은 int*이다. 그것을 reinterpret_cast를 통해 intptr_t*(포인터 → 포인터)와
  intptr_t(포인터 → 정수)로 변환할 수 있다 (int* → intptr_t*, int* → intptr_t). 그런데 int*을
  intptr_t*로 변환한 후, 그 값을 역참조하는 행위는 정의되지 않은 행동으로, 예상하지 않은 결과가 도출될 수 있다.

reinterpret cast란?

reinterpret_cast<new_type>(expression)

⇒ reinterpret_cast는 임의의 포인터 타입끼리 변환을 허용하는 캐스트 연산자 이다. 또한 정수형을 포인터로 바꿀 수 있으며, 그 반대의 연산 또한 가능하다.

⇒ expression의 값을 new_type으로 변환한다. 다른 형태의 포인터로 변경이 가능하기 때문에 결과 값이 잘못 사용하는 경우에는 결과 값이 컴파일러에 따라 다를 수도 있고, 예상하지 않는 값이 나올 수 있다.

#include <iostream>
#include <cstdint>

int main(void)
{
    int num = 1;

    int* Danger = reinterpret_cast<int*>(num);
    std::cout << "Dangerous : " << Danger<< std::endl;

    intptr_t    ptr = reinterpret_cast<intptr_t>(&num);
    int* dPtr = reinterpret_cast<int*>(ptr);

    std::cout << "num : " << num << '\n' 
              <<"copied num : " << *dPtr << std::endl;

    std::cout << \
    "num pointer : " << &num << '\n' << \
    "Serialized : " << ptr << '\n' << \
    "Serialized hex : " <<std::hex<<"0x" << ptr<<'\n'<< \
    "Deserialized copied num pointer : " << dPtr<<std::endl;
    return 0;
}


<< output >>
Dangerous : 0x1 -> 조작하는 것은 위험! (정수 -> 포인터 연산)
num : 1
copied num : 1
num pointer : 0x7ffee3599b08
Serialized : 140732712721160
Serialized hex : 0x7ffee3599b08
Deserialized copied num pointer : 0x7ffee3599b08

** 예외사항

  • int* -> char -> int* 같은 형태의 변환

⇒ 결과 : 주소 값이 파괴되어 nullptr을 가리키게 된다.

⇒ 이유 : char형은 1바이트 크기여서 주소 값을 다 표현하지 못하기 때문이다. 이는 reinterpret_cast의 특징 때문인데, 자료를 그대로 변수로 변환하게 되기 때문에 char와 같이 바이트 수가 작은 데이터를 옮기는 과정에서는 데이터가 dump가 되어 원본 데이터가 파괴된다.

ex02 Identify

기본적으로 static이라는 의미는 컴파일 타임에 파악이 가능하다라는 의미고, dynamic이라는 의미는 런 타임이 되어서야 파악이 가능하다 정도로 이해할 수 있을 것이다. dynamic_cast의 dynamic도 이 의미를 크게 벗어나지 않는다.

→ (기본적으로 dynamic cast는 컴파일러의 RTTI 설정을 켜둬야 사용할 수 있는 등, 런타임과 밀접한 관계가 있다.)

  • 지난 모듈에서 넌지시 화두를 던졌던 dynamic cast의 조건에 대한 이야기를 마무리지을 시간이다. 동적 바인딩에 대한 이야기가 이번 모듈에서 종결된다고 보면 된다.
class Base
{
    public:
        virtual ~Base();
};

class A : public Base {};
class B : public Base {};
class C : public Base {};

Base*   generate(void)
{
	srand((unsigned int)time(NULL));
	int per = rand() % 9;
	
	if (per >= 0 && per <= 2)
		return new A;
	else if (per >= 3 && per <= 5)
		return new B;
	else
		return new C;
}

int main(void)
{
    Base    *bp = generate();    
    std::cout<<bp<<std::endl;

    delete bp;

    return 0;
}
  • 위 코드는 Base 포인터에 랜덤으로 생성된 객체 A, B, C를 생성하는 코드이다. 기본적으로 업 캐스팅된 포인터 bp가 실제로 가리키는 객체를 판별할 수 있는 방법은 무엇일까?

dynamic cast

  • 바로 다이나믹 캐스트를 사용하여 객체를 판별할 수가 있다.
<포인터를 사용하는 경우>

void	identify(Base *bp)
{
    if (A* a = dynamic_cast<A*>(bp))
    {
        std::cout << BOLDGREEN << std::setw(13) \
					<< "A" << std::endl;
    }
    else if (B* b = dynamic_cast<B*>(bp))
    {
        std::cout << BOLDGREEN << std::setw(13) \
					<< "B" << std::endl;
    }
    else if (C* c = dynamic_cast<C*>(bp))
    {
        std::cout << BOLDGREEN << std::setw(13) \
					<< "C" << std::endl;
    }
		else
		{
				std::cout << BOLDGREEN << "  Identification failed" \
				<<	std::endl;
		}
}

<참조자를 사용하는 경우>

void	identify(Base &br)
{
    try
    {
        A& a = dynamic_cast<A&>(br);
        std::cout << BOLDRED << std::setw(13) << "A" << std::endl;
        static_cast<void>(a);
    }
    catch (std::exception&) // std::bad_cast&
    {
        try
        {
            B& b = dynamic_cast<B&>(br);
            std::cout << BOLDRED << std::setw(13) << "B" << std::endl;
            static_cast<void>(b);
        }
        catch (std::exception&) // std::bad_cast&
        {
            try
            {
                C& c = dynamic_cast<C&>(br);
                std::cout << BOLDRED << std::setw(13) << "C" << std::endl;
                static_cast<void>(c);
            }
            catch (std::exception&) // std::bad_cast&
            {
                std::cout << BOLDRED << "  Identification failed" \
                << std::endl;            
            }
        }
    }
}

** 포인터는 NULL로 초기화할 수 있지만, 레퍼런스는 NULL로 초기화하는 것이 불가능하다. 따라서 참조자를 사용하는 함수에서 다이나믹 캐스트가 실패할 경우, NULL로 초기화되는 것이 아니라 std::bad_cast로 throw()된다. 따라서 그에 적합한 예외 처리를 해줘야 한다.

⇒ 그런데 reference에 따르면 std::bad_cast는 를 include해야 사용할 수 있다고 한다. 따라서 범용적인 예외 처리를 위해 std::exception을 사용하였다

< source code >
 => https://cplusplus.com/reference/typeinfo/bad_cast/

#include <iostream>       // std::cout
#include <typeinfo>       // std::bad_cast

class Base {virtual void member(){}};
class Derived : Base {};

int main () 
{
    try
    {
        Base b;
        Derived& rd = dynamic_cast<Derived&>(b);
    }
    catch (std::bad_cast& bc)
    {
        std::cerr << "bad_cast caught: " << bc.what() << '\n';
    }
    return 0;
}

static cast와 dynamic cast

(1) Static Cast

static_cast<new_type>(expression)
// 논리적으로 변환 가능한 타입을 변환, complie 타임에 형변환에 대한 타입 오류 체크
  • 실수와 정수, 열거형과 정수형, 실수와 실수 사이의 변환 등을 허용

  • arr → point, function → function pointer로 변경 가능

  • void로의 변환이 있는데, 반환값으로 존재하지 않는 void 값으로의 변환은 주로 값에 대한 사용을 위한 용도가 있다.

< MODULE 05 ex03 >

Intern::Intern(const Intern &ref) 
{
    static_cast<void> (ref);
}
  • 그러나 포인터 타입을 다른 것으로 변환하는 것을 허용하지 않음 reinterpret_cast 참조 ⇒ 그러나, 상속 관계에 있는 포인터끼리 변환이 가능하다.
    downcast 시에 unsafe하게 동작할 수 있다(safe하게 동작시키기 위해 dynamic_cast 사용)

unsafe한거… 이런거…


(2) Dynamic cast

  • 안전한 downcasting을 위해 사용한다. 부모 클래스의 포인터에서 자식 클래스의 포인터로 다운 캐스팅 해주는 연산자이다. 런타임 시간에 다운 캐스팅이 가능한지 검사하기 때문에, 런타임 비용이 높은 연산자이다.

성공할 경우 : new_type의 value를 return

실패할 경우(new_type = pointer) : null pointer

실패할 경우(new_type = reference) : bad_cast (exception)

#include <iostream>

class A
{
    std::string s;

    public:
        A(void)
            :	s("Base") {}

        virtual void text(void)
        {
            std::cout << s << std::endl;
        }
        virtual ~void();
};

class B : public A
{
    std::string s;

    public:
        B(void)
            :	A(), s("Derived") {}

        void text(void)
        {
            std::cout << s << std::endl;
        }
};

int main(void)
{
    A	  	a;

    // --- static cast --- //
    A*		a_ptr = &a;
    B*		b_ptr = static_cast<B*>(a_ptr);

    // --- dynamic cast --- //
    A*		a_ptr = &a;
    B*		b_ptr = dynamic_cast<B*>(a_ptr);

    b_ptr->text();
    return (0);
    }
  • static_cast는 B 클래스의 포인터로 할당을 위해 A 클래스의 포인터를 일시적으로 형 변환한 것이므로, B 클래스의 포인터가 참조하는 공간은 여전히 A 클래스 타입으로 이해된다. 따라서 text를 호출했을 때는 vTable에서 실제 타입인 A 클래스의 text를 호출하게 된다.

  • 반면에 dynamic_cast는 B 클래스의 포인터로 할당을 위해 A 클래스의 포인터를 형 변환하는 과정이 static_cast와는 사뭇 다르다. A 클래스의 포인터로 참조되는 객체가 B 클래스의 포인터로 참조될 수 있는지 먼저 확인하고, 기반 클래스 → 파생 클래스에 해당하므로 변환이 불가능하다는 것을 인지하여 b_ptr에는 NULL이 할당된다. 따라서 NULL의 text를 호출하려 했으므로 SegFault가 발생한다. 이처럼 dynamic_cast는 형 변환 가능 여부를 확인할 수 있도록 되어 있기 때문에, 여러 상황에 대해서 대처할 수 있다.

  • dynamic_cast로 형 변환할 수 없을 때 T가 무엇인지에 따라 반환 값이 다른데, 이는 아래에서 다룬다. 만일 형 변환이 가능하다면 static_cast에서는 일시적으로 형 변환이 되었던 것과 달리, dynamic_cast의 결과는 인식되는 타입을 아예 바꾸게 만든다.

  • dynamic_cast이 위와 같이 동작할 수 있는 것은 RTTI (Runtime Type Information)이라는 C++ 컴파일러의 기능 덕분이다. 이름에서 알 수 있듯이, 객체의 유형을 런 타임에 결정할 수 있도록 만드는 것이다. 이와 같은 기능은 객체가 위치한 메모리 상에 객체 타입에 대한 정보를 추가하면서, 실제 타입을 이용하지 않고 제공된 타입을 이용하면서 이뤄질 수 있다. 따라서 dynamic_cast를 이용하면 실제로는 A 클래스 타입이지만, 형 변환에 따른 결과는 이와 다를 수 있게 되는 것이다. 결론적으로 업 캐스팅을 제외한 상속 구조의 객체 간의 형 변환 (다운 캐스팅, 크로스 캐스팅)에서는 RTTI를 기반으로 동작하는 dynamic_cast를 쓰는 것이 안전하다

dynamic cast의 가상 상속과 <void*>

다중 상속에 대해 아직 공부하지 않아서 설명이 확실하지 않다… 지피티에 의존한 설명이다.

#include <iostream>

class A
{
    public:
        virtual ~A(void) {}
};

class B : public A {};

class C : public A {};

class D : public B, public C {};

int main(void)
{
    D   d;
    B*  b_ptr = &d;
    C*  c_ptr = &d;

    if (dynamic_cast<A*>(b_ptr) == dynamic_cast<A*>(c_ptr))
        std::cout << "Same" << std::endl;
    else
        std::cout<< "Not Same" << std::endl;
    return (0);
}
<output>
-> "Not Same"
  • 왜 not same이냐?


#include <iostream>

class A
{
	public:
		virtual ~A(void) {}
};

class B : public A {};

class C : public A {};

class D : public B, public C {};

int main(void)
{
    D   d;
    B*  b_ptr = &d;
    C*  c_ptr = &d;

    if (dynamic_cast<void*>(b_ptr) == dynamic_cast<void*>(c_ptr))
        std::cout << "Same" << std::endl;
    else
        std::cout<< "Not Same" << std::endl;
    return (0);
}
<output>
-> "Same"
  • 왜 same이냐?


  • 가끔 찌피티가 뻘소리를 하는 경우가 있어서, 여러분들이 확실하게 왜 same, not same인지 안다면 그게 맞을거다… 다중상속 언제공부하나 ex03보너스를 할걸그랫다

결론

포인터와 참조자의 구분

dynamic_cast<T*> -> When Error, NULL returned
dynamic_cast<T&> -> When Error, Exception throwed
  • dynamic_cast의 T로는 포인터가 올 수도 있고, 참조자가 올 수도 있다. dynamic_cast의 형 변환 실패는 기본적으로 NULL을 기반으로 한다고 생각하면 편하다. 포인터의 경우 T*에 대한 NULL은 존재할 수 있으므로 형 변환 실패 시 NULL이 반환되고, 참조자의 경우 NULL에 대한 참조는 불가능하기 때문에 Exception을 던지게 된다.

  • 따라서 dynamic_cast를 이용하여 상속 구조에서 자기 자신의 타입이 무엇인지 알 수 있는 방법은 다음과 같다. T가 포인터로 들어온 경우에는 NULL을 반환하지 않은 경우가 자기 자신의 타입이고, T가 참조자로 들어온 경우에는 Exception이 던져지지 않은 경우가 자기 자신의 타입이다.

RTTI와 Vtable의 관계



  • dynamic cast를 통해 기반 클래스 포인터가 실제로 가리키고 있는 객체를 파악할 수 있는 이유는 런타임에 컴파일러가 동적으로 적절한 객체의 타입을 참조할 수 있기 때문이며(RTTI), vtable은 그러한 RTTI가 가능하도록 하는 요소이다. 객체의 타입을 참조할 때, vptr에 기록된 기반 - 파생 클래스의 정보를 참조하기 때문이다.

  • 앞서 dynamic cast의 조건을 확인한 바 있다 (1. 두 자료형이 상속 관계일 것, 2. virtual 함수가 하나 이상 존재할 것, 3. RTTI 기능이 켜져있을 것). 이러한 조건은 dynamic cast와 RTTI, vtable의 관련성을 여과없이 보여주는 것으로 볼 수 있다.


내가 봤을 때 이게 모듈 6에서 제일 중요한 부분같다. ex02 설명하는데 이거 얘기안나오면 fail줘도 되지 않나 싶기도 하고… 이걸로 모듈 6을 마친다.