
객체의 값은 비트시퀀스로 저장되고 타입은 컴파일러에게 해당 비트를 의미 있는 값으로 해석하는 방법을 알려준다
float f{ 3 };
컴파일러는 정수3을 그대로 변수f에 할당된 메모리에 저장하지 않고 float이기 때문에 3.0으로 형변환 후 저장한다
형변환은 원래의 값이나 타입을 변환하지 않는다, 변환된 새 값이 결과로 생성되는것이다
(원래의 값인 3자체를 3.0으로 변환하는게 아니고 3.0으로 변환된 새로운 값이 결과로 생성되는것임)
형변환은 명시적, 암시적 두 가지 방법 중 하나를 사용할 수 있다
암시적 형변환
암시적 형변환은 서로 타입이 다를때 컴파일러가 자동으로 수행하는 형변환이다
대표적으로 다음과 같은 케이스들이 있다
double d{ 3 }; //3이 double 타입인 3.0으로 암시적 형변환된다
float foo()
{
return 10.0; //10.f로 암시적 형변환된다
}
6.0 / 3; //3은 3.0으로 암시적 형변환된다
if(10) //10은 0이 아니기때문에 true로 암시적 형변환된다
{
}
void foo(int a)
foo(10.f); //10.f가 10으로 암시적 형변환된다
형변환이 호출되면 우선 컴파일러는 해당 타입으로 변환이 가능한지 여부를 판단한다, 이때 가능하다면 컴파일러가 자동으로 새로운 타입에 대한 값을 생성한다
만약 변환이 가능하지 않다면 컴파일 에러를 발생시키거나 data손실이 일어날 수 있다는 warning이 발생하게 된다 (리스트 초기화는 타입이 맞지 않다면 무조건 에러)
그렇다면 컴파일러는 해당 타입으로 변환이 가능한지 여부를 어떻게 알아낼까?
C++표준은 타입을 다른 타입으로 변환할 수 있는 방법을 정의하는데 이러한 변환 규칙을 표준 변환이라고 한다 (standard conversion)
크게 4가지로 Numeric Promotion(숫자 승격), Numeric Conversion(숫자 변환), Arithmetic Conversion, 기타 다른 Conversion으로 나뉜다
C++은 각 타입에 대해 최소 보장 크기가 있다, 물론 실제 크기는 아키텍쳐나 컴파일러에 따라 다를 수 있다
32-bit 컴퓨터에서는 일반적으로 CPU가 한번에 처리할 수 있는 데이터 크기가 32bit라는 의미이며 따라서 int의 크기도 보통 32bit로 처리된다 (최적화, 32bit단위로 처리하는게 제일 빠르다 -> 오히려 8bit, 16bit 처리가 더 느릴 수 있다 (8, 16bit도 32bit으로 변환 후 처리하고 다시 변환하는 과정이 생길 수 있기 때문))
이렇게 작은 크기의 타입을 더 큰 타입으로 변환하여 처리 속도를 최적화하는걸 Numeric Promotion (숫자 승격) 이라고 한다, 데이터 손실이 일어나지 않는 안전한 타입변환이다
ex) char -> int로 변환하여 연산
이러한 숫자 승격은 코드 중복에도 효과적이다
void foo(int a);
void foo(double b);
이렇게 인자를 int, double로 받는 함수 두개만 만든다면 char, short, int, double, float, wchar_t, char8_t 등등에 전부 대응할 수 있는 함수가 되어 중복을 줄일 수 있다
크게 두개의 범주로 나눌 수 있다
floating point promotion (부동 소수점 승격)
float을 double 타입으로 승격하는 것이다
void foo(double d)
foo(10.5f); //float -> double로 가는 숫자 승격
foo(10.3);
integral promotion (정수 승격)
작은 정수타입 char, short, bool 등을 더 큰 정수타입인 int나 unsigned int로 승격하는 것이다
이때 모든 확대 변환이 숫자 승격에 해당하지는 않는다 (int -> long은 포함되지 않는다)
이러한 변환은 더 큰 타입으로 변환해서 효율적으로 처리되지 않기 때문이다
위의 숫자 승격에 포함되는 형변환은 숫자 변환이 아닌 숫자 승격이라고 표현한다
숫자 변환에는 다섯가지 유형이 있다
short s = 3;
long l = 3;
char ch = 3;
unsigned int ui= 3;
float f = 3.0;
long double ld = 3.0;
int i = 1.5f;
int i = 1.5;
float f = 3;
double d = 10;
bool b = 3.f;
bool b = 10;
중괄호 초기화는 축소 변환을 엄격히 허용하지 않기 때문에 축소 변환에서는 사용할 수 없다
Safe, Unsafe Conversion
숫자 승격과 달리 많은 숫자 변환은 안전하지 않다, 여기서 안전하지 않은 변환이라는것은 형변환 중 데이터 손실이 발생할 수 있다는 의미이다
value-preserving conversion (값 보존 변환)
//int -> long
int i = 10;
long l = i;
//short -> double
short s = 5;
double d = s;
컴파일러는 값 보존 변환에 대한 경고를 일반적으로 하지 않는다, 이렇게 값 보존 변환이 된 값은 다시 원래 타입으로 변환될 수 있고 원래의 값과 동일한 값이 생성된다
int i = static_cast<int>(static_cast<long>(10)); //10
char c = static_cast<char>(static_cast<double>('c')); //c
reinterpretive conversion (재해석 변환)
재해석 변환은 형변환 후 값이 원래 값과 다를 수 있지만 데이터 손실이 없는 안전하지 않은 변환이다
대표적으로 unsigned와 signed의 변환이 있다
ex) 1111 1111 이 unsigned에서는 255로 값이 나오지만 signed에서는 -1이 나오게 된다
int i{ 5 };
unsigned int ui{ i };
int i{ -5 };
unsigned int ui{ i };
권장되지 않는 타입 변환으로 의도치 않은 동작이 발생할 확률이 높다, 둘 다 컴파일 에러 발생할 수 있음
재해석 변환도 원래 값으로 변환이 가능하며 동일한 값이 생성된다 (심지어 값의 범위를 벗어난 경우에도 -> 이것이 원래 값과 다를 수 있지만 데이터 손실이 없다는 근거임)
int u = static_cast<int>(static_cast<unsigned int>(-5)); //-5
C++20이전에는 signed <-> unsigned 사이의 변환에서 값이 초과되면 컴파일러의 구현에 따라 각각 다르게 동작을 했지만 C++20 이후에는 modulo wrapping이 발생하여 예측가능한 값이 생성되게 된다 (예를들어 unsigned값이 너무 커서 signed의 범위를 넘게 되면 modulo wrapping으로 범위 내 값으로 돌려놓는것)
Lossy Conversion (손실 변환)
손실 변환은 말그대로 데이터 손실이 일어날 수 있는 안전하지 않은 숫자 변환이다
int i = 3.5; //3으로 나오기 때문에 0.5 데이터가 손실됨
float d = 1.23456789; //float이 담을 수 있는 소수점보다 더 뒤에 있는 데이터들은 손실된다
이렇게 손실 변환된 값은 다시 변환해도 원래의 값으로 돌아갈 수 없다
double d { static_cast<double>(static_cast<int>(3.5)) };
std::cout << d << '\n'; //3
이러한 손실 변환은 최대한 피하는것이 좋다
숫자 변환에서 주의해야 할 점
int i{ 100000 };
char c { i }; //char은 -128 ~ 127까지의 숫자를 담을 수 있는데 100000이 들어와 오버플로가 발생하여 의도치 않은 값이 나오게 됨
float f = 0.123456789;
std::cout << std::setprecision(9) << f << '\n';
//double만큼의 소수점을 담을 수 없기 때문에 반올림되어 데이터 손실이 발생함 (정밀도 이슈)
int i = 3.5; //3
소수점 단위 데이터 손실 발생
일반적으로 컴파일러들은 이러한 손실 변환에 대해 경고를 해준다
