암시적인 형 변환과 명시적인 형 변환
암시적인 형 변환( type conversion )은 변수의 타입이 일치하지 않는 경우, 컴파일러가 자동으로 필요한 타입으로 변경하는 것을 말합니다.
아래의 예제가 암시적인 형 변환을 보여줍니다.
int main(){
double val = 10 / 4; // 암시적인 형 변환
cout << "double type value: " << val << endl;
}
▼출력
double type value: 2
컴파일러는 10 / 4의 int 값을 double 타입의 변수에 대입하기 위해서, 자동으로 double 값으로 타입을 변경합니다.
그런데, 문제가 좀 있네요.
val의 값이 2.5가 되기를 기대했지만, 실제는 2가 되었습니다.
이렇게 된 이유는 10 / 4의 int 연산 수행을 하고, 그 결과를 double 타입으로 변경했기 때문입니다.
따라서, 10이나 4를 ( 혹은 10과 4를 모두 ) double 타입으로 변경하면 나누기의 결과 값이 2.5가 될 것입니다.
( 10이나 4를 double 타입으로 변경하면, C의 연산 규칙에 따라 나머지 숫자가 double 타입으로 변경됩니다. )
이렇게, 사용자가 컴파일러에게 직접 타입의 변환을 요청하는 것을 명시적인 형 변환( explicit type conversion )이라고 합니다.
C++에서는, int를 double 타입으로 변경하는 명시적인 방법으로 C-style 형 변환과 static_cast 연산자를 제공합니다.
int main(){
double val1 = (double)10 / 4; // C-style cast
double val2 = double(10) / 4; // 함수 style 형 변환
double val3 = static_cast<double>(10) / 4; // C++ style 형 변환
cout << "double type value1: " << val1 << endl;
cout << "double type value2: " << val2 << endl;
cout << "double type value3: " << val3 << endl;
}
첫 번째 val1을 계산한 방법이 예전부터 사용되어 온 C-style cast입니다.
이 방식은 다음과 같이 선언합니다.
( 변경하고자 하는 타입 )값을 나타내는 표현식;
그리고, 이 방식을 함수 호출 방식으로 변경한 방법이 두 번째 val2를 계산한 방법입니다.
변경하고자 하는 타입( 값을 나타내는 표현식 );
이 방법은 위의 방식과 같은 기능을 수행합니다.
그리고, 마지막에선 val3를 계산한 방법으로 static_cast 연산자를 사용하는 법을 보여주고 있습니다.
static_cast 사용법은 다음과 같습니다.
static_cast< 변경하고자 하는 타입 >( 값을 나타내는 표현식 );
다음 예제에선, 함수의 값을 명시적으로 변경하는 방법을 보여줍니다.
int add( int a, int b){
return a + b;
}
int main(){
double val = static_cast<double>( add(4, 6) );
cout << "double type value: " << val << endl;
}
이러한 명시적인 형 변환은 원래의 표현식의 값을 변경하는 것이 아니라, 원래의 표현식의 값을 입력값으로 해서 임시적인 변환 값을 생성해서 그 값을 사용하는 것입니다.
위의 예문에서는 표현식의 int 값으로부터 double 타입의 임시의 값 10.0을 생성합니다.
그리고, 그 결과를 double 타입의 변수에 대입하는 것입니다.
그런데, C++은 왜 C-style의 형 변환 방식을 놔두고, static_cast 연산자를 도입했을까요?
실제 사용방법도 C-style이 편해 보이는데 말입니다.
C-style cast와 static_cast
C++에서는 명시적인 형 변환에 static_cast 외에도 const_cast와 reinterpret_cast, 그리고 dynamic_cast 이렇게 세 가지 연산자를 더 제공합니다.
그런데, const_cast와 reinterpret_cast는 제한적인 사용을 해야 할 정도로 프로그램을 위협하는 방법을 제공합니다.
const_cast 연산자
const_cast는 한 변수를 참조하는 변수의 상수 속성을 제거하는 연산자입니다.
좀 더 정확하게 쓰자면, 상수 변수의 상수 속성을 제거하는 것이 아니라,
상수가 아닌 변수를 참조하는, 상수 참조의 상수 속성을 제거하는 것이 목적인 연산자입니다.
다음 예제는 const_cast를 사용법을 보여줍니다.
void func_const_parameter( const int& not_changed){
int& can_changed = const_cast<int&>(not_changed);
can_changed += 10;
}
int main(){
int val = 10;
func_const_parameter(val);
cout << "not changed val: " << val << endl;
}
▼출력
not changed val: 20
이 예제를 보면 알 수 있듯이, 이런 것이 가능하다면 const 키워드를 굳이 사용해야 할 필요가 있을까라고 물어보게 됩니다.
그렇지만, mutable 키워드를 사용할 곳이 있는 것처럼, 특수한 경우에 편리할 수도 있습니다.
mutable는 const 멤버 함수 내에서 멤버 변수의 값을 변경할 수 있도록 만들어 주는 키워드입니다.
mutable에 관한 내용은 여기에서 볼 수 있습니다.
reinterpret_cast 연산자
reinterpret_cast는 한 포인터 타입을 다른 포인터 타입으로 변환하는 연산자입니다.
그런데, 이 연산자의 문제점은 이러한 변환이 아무 관련이 없는 타입의 포인터 간에도 수행이 가능하다는 겁니다.
다음 예제에서 reinterpret_cast의 사용법을 보여줍니다.
struct objA{
void doSomething(){
// do something
}
};
struct objB{
void doSomething(){
// do something have nothing to do objA
}
};
void func_reinterpret( objA* pa){
// objB 타입의 포인터로 변환
objB* pb = reinterpret_cast<objB*>(pa);
pb->doSomething();
}
int main(){
objA a;
func_reinterpret( &a );
}
objA 객체의 구조와 상관없이 objB 객체의 포인터로 변환이 가능합니다.
심지어, 이 연산자는 long long 타입의 변수를 objB 타입의 포인터로 변환할 수도 있습니다.
int main(){
long long n = 0;
objB* pb = reinterpret_cast<objB*>(n);
// pb포인터 사용 결과가 정의되지 않습니다.
int ll_size = sizeof(long long);
int objB_ptr_size = sizeof(objB*);
cout << "long long size: " << ll_size << endl;
cout << "objB pointer size: " << objB_ptr_size << endl;
}
▼출력
long long size: 8
objB pointer size: 8
한 마디로, 그 변수의 크기만 같다면 어떤 변환도 가능하다는 것입니다.
그런데, 놀라운 것은 C-style cast는 이 두 가지 기능을 모두 사용한다는 것입니다.
C-style cast는 필요하다면 다음 순서대로 형 변환을 수행합니다.
( C++ Standard, 5.4 expr.cast paragraph 5 참조 )
- const_cast
- static_cast
- reinterpret_cast
실제로, 위의 const_cast와 reinterpret_cast 연산자를 사용한 예제를 C-style cast로 변경해도 똑같이 동작한다는 것을 알 수 있습니다.
코드가 보편적( generic )일수록 모든 경우를 고려해야 하기 때문에 구현이 점점 어려워집니다.
그런데, 단지 형 변환을 할 때마다 원하지 않는 변환이 발생할 가능성을 고려해야 한다는 것은 정말 불편한 일입니다.
"나는 static_cast로 변경을 할 수 없으면, reinterpret_cast을 호출하겠어"라고 말할 사람이 얼마나 되겠습니까.
그에 반해, static_cast는 명시적인 형 변환이라는 단일 목적만 수행합니다.
다음은, C++ 표준 라이브러리에서 std::move 함수를 구현할 때 사용하는 static_cast의 예입니다.
template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
이 함수의 핵심은 좌측값이나 우측값을 받아서 static_cast를 사용해 우측값 참조로 변경하는 것입니다.
그래서, 이 함수를 다음과 같이 간단히 구현해 볼 수 있습니다.
class TestObj{
public:
TestObj() = default;
TestObj(const TestObj& other){ // 복사 생성자
cout << "called Copy Constructor\n";
}
TestObj( TestObj&& other){ // 이동 생성자
cout << "called Move constructor\n";
}
};
int main(){
TestObj obj;
TestObj& rObj = obj;
TestObj other = static_cast<TestObj&&>(rObj); // move 연산 구현
}
▼출력
called Move constructor
위에서 좌측값을 우측값 참조로 변경해서, 이동 생성자를 호출하는 것을 볼 수 있습니다.
이 과정에서 obj의 모든 데이터가 other 객체로 이동됩니다.
하지만, 아래의 코드와 같이 const 참조 변수는 당연히 우측값 참조로 형 변환할 수 없습니다.
그런데, C-style cast를 사용하면, 이때도 형 변환이 가능합니다.
int main(){
TestObj obj;
const TestObj& crObj = obj;
//TestObj other = static_cast<TestObj&&>(crObj); // 형 변환 실패
TestObj other = (TestObj&&)(crObj); // C-style cast. very good !?
}
▼출력
called Move constructor
그리고, 이동 생성자가 호출돼서 모든 데이터가 이동됩니다.
std::move 함수와 이동 문법( move semantics )에 관한 내용은 여기에서 볼 수 있습니다.
이 글과 관련된 글들
dynamic_cast를 통한 실시간 형 변형( type conversion )
'C, C++ > C++ 언어' 카테고리의 다른 글
[C++] nullptr 리터럴( literal )과 NULL의 차이점 (0) | 2024.08.07 |
---|---|
[C++] dynamic_cast를 통한 실시간 형 변형( type conversion ) (0) | 2024.08.06 |
[C++] noexcept 키워드를 사용하는 이유 (0) | 2024.08.01 |
[C++] namespace가 필요한 이유 (0) | 2024.07.30 |
[C++] const 속성과 mutable 키워드 (0) | 2024.07.26 |