때때로 한 유형의 값을 다른 유형의 값으로 변환해야 한다. 이러한 변환은 언어 규칙에 따라 암시적으로 수행된다.
double d = 1234567890; // integer to floating-point
int i = d; // floating-point to integer
다른 경우에는 명시적이어야 한다.
논리적이고 역사적인 이유로 C++는 다양한 편의성과 안전성을 가진 명시적 유형 변환 작업을 제공한다.
- {} 표기법을 사용하여 새 값의 유형 안전 구성을 제공하는 구성
- 다양한 수준의 변환을 제공하는 명명된 변환
- const로 선언된 항목에 대한 쓰기 액세스 권한을 얻기 위한 const_cast
- 잘 정의된 암시적 변환을 되돌리기 위한 static_cast
- 비트 패턴의 의미를 변경하기 위한 reinterpret_cast
- 동적으로 확인된 클래스 계층 탐색을 위한 dynamic_cast - 명명된 변환 및 이러한 변환의 일부 조합을 제공하는 C 스타일 캐스트
- C 스타일 캐스트에 대해 다른 표기법을 제공하는 기능적 표기법
{} 구성 표기법을 제외하고는 어느 것도 마음에 든다고 말할 수 없지만 적어도 dynamic_cast는 run-time에 확인된다. 두 스칼라 숫자 유형 간의 변환을 위해 직접 만든 명시적 변환 함수인 narrow_cast를 사용하는 경향이 있다.
template<class Target, class Source>
Target narrow_cast(Source v)
{
auto r = static_cast<Target>(v); // convert the value to the target type
if (static_cast<Source>(r)!=v)
throw runtime_error("narrow_cast<>() failed");
return r;
}
즉, 값을 대상 유형으로 변환하고 결과를 소스 유형으로 다시 변환하고 원래 값을 다시 얻을 수 있다면 결과에 만족한다. 이는 언어가 {} 초기화의 값에 적용되는 규칙의 일반화다.
void test(double d, int i, char∗ p)
{
auto c1 = narrow_cast<char>(64);
auto c2 = narrow_cast<char>(−64); // will throw if chars are unsigned
auto c3 = narrow_cast<char>(264); // will throw if chars are 8-bit and signed
auto d1 = narrow_cast<double>(1/3.0F); // OK
auto f1 = narrow_cast<float>(1/3.0); // will probably throw
auto c4 = narrow_cast<char>(i); // may throw
auto f2 = narrow_cast<float>(d); // may throw
auto p1 = narrow_cast<char∗>(i); // compile-time error
auto i1 = narrow_cast<int>(p); // compile-time error
auto d2 = narrow_cast<double>(i); // may throw (but probably will not)
auto i2 = narrow_cast<int>(d); // may throw
}
부동 소수점 숫자 사용에 따라 부동 소수점 변환에 != 대신 범위 테스트를 사용하는 것이 좋다. 이는 전문화 또는 type trait을 사용하여 쉽게 수행된다.
Construction
값 e에서 유형 T 값의 구성은 T{e} 표기법으로 표현할 수 있다.
auto d1 = double{2}; // d1==2.0
double d2 {double{2}/4}; // d1==0.5
T{v} 표기법의 매력 중 하나는 올바른 변환만 수행한다는 것이다.
void f(int);
void f(double);
void g(int i, double d)
{
f(i); // call f(int)
f(double{i}); // error : {} doesn’t do int to floating conversion
f(d); // call f(double)
f(int{d}); // error : {} doesn’t truncate
f(static_cast<int>(d)); // call f(int) with a truncated value
f(round(d)); // call f(double) with a rounded value
f(static_cast<int>(lround(d))); // call f(int) with a rounded value
// if the d is overflows the int, this still truncates
}
부동 소수점의 숫자의 소수점 이하 버림이 잘 작동한다고 할 수 없으므로 원할 때 명시적으로 표시하는 것이 좋다. 반올림이 필요한 경우 표준 라이브러리 함수인 round()를 사용할 수 있다.
{}-구성이 int에서 이중 변환을 허용하지 않는다. 그러나 int의 크기가 double의 크기와 같으면 그러한 변환 중 일부는 정보를 잃는다.
static_assert(sizeof(int)==siz eof(double),"unexpected sizes");
int x = numeric_limits<int>::max(); // largest possible integer
double d = x;
int y = x;
x==y일 수 없다. 그러나 정확히 표현할 수 있는 정수 리터럴로 double을 여전히 초기화할 수 있다.
double d { 1234 }; // fine
원하는 유형의 명시적 자격은 잘못된 변환을 가능하게 하지 않는다.
void g2(char∗ p)
{
int x = int{p}; // error : no char* to int conversion
using Pint = int∗;
int∗ p2 = Pint{p}; // error : no char* to int* conversion
// ...
}
T{v}의 경우, 잘 작동하는 것은 v에서 T로의 'non-narrowing' 변환 또는 T에 대한 적절한 생성자를 갖는 것으로 정의된다. 생성자 표기법 T{}는 T 유형의 기본값을 표현하는 데 사용된다.
template<class T> void f(const T&);
void g3()
{
f(int{}); // default int value
f(complex<double>{}); // default complex value
// ...
}
built-in type에 대한 생성자의 명시적 사용 값은 해당 형식으로 변환된 0이다. 따라서 int{}는 0을 쓰는 또 다른 방법이다. 사용자 정의 유형 T의 경우 T{}는 기본 생성자에 의해 정의되고, 그렇지 않은 경우 각 멤버의 기본 생성자에 의해 정의된다.
명시적으로 생성된 명명되지 않은 객체는 임시 객체이며(참조에 바인딩되지 않은 한) 수명은 사용되는 전체 표현식으로 제한된다. 이점에서 new를 사용하고 만든 이름 없는 객체와 다르다.
Named Cast
일부 유형 변환은 제대로 작동하지 않거나 유형 확인이 쉽지 않다. 그것들은 잘 정의된 인수 값 세트에서 값의 단순한 구성이 아니다.
IO_device∗ d1 = reinterpret_cast<IO_device∗>(0Xff00); // device at 0Xff00
컴파일러가 정수 oxff00가 (I/O장치 레지스터의) 유효한 주소인지 여부를 알 수 있는 방법은 없다. 결과적으로 변환의 정확성은 전적으로 프로그래머의 손에 달려 있다. 종종 캐스팅이라고 하는 명시적 유형 변환은 때때로 필수적이다. 그러나 심각하게 남용하게 되면 오류의 주요 원인이 된다.
명시적 유형 변환의 필요성에 대한 또 다른 고전적인 예는 "raw memory", 즉 컴파일러에 알려지지 않은 유형의 객체를 보유하거나 보유할 메모리를 처리하는 것이다. 예를 들어, 메모리 할당자(예:operator new())는 새로 할당된 메모리를 가리키는 void*를 반환할 수 있다.
void∗ my_allocator(siz e_t);
void f()
{
int∗ p = static_cast<int∗>(my_allocator(100)); // new allocation used as ints
// ...
}
컴파일러는 void*가 가리키는 객체의 유형을 알 수 없다.
명명된 캐스트의 기본 아이디어는 유형 변환을 더 가시적으로 만들고 프로그래머가 캐스트의 의도를 표현할 수 있도록 하는 것이다.
- static_cast는 동일한 클래스 계층에서 한 포인터 유형을 다른 포인트 유형으로, 정수 유형을 열거형으로 또는 부동 소수점 유형을 정수 유형으로 변환하는 것과 같은 관련 유형 간을 변환한다. 또한 생성자 및 변환 연산자에 의해 정의된 변환을 수행한다.
- reinterpret_cast는 포인터에 대한 정수 또는 관련되지 않은 포인터 유형에 대한 포인터와 같은 관련되지 않은 유형 간의 변환을 처리한다.
- const_cast는 const 및 volatile 한정자만 다른 유형 간 변환한다.
- dynamic_cast는 포인터와 참조를 클래스 계층 구조로 런타임 검사 변환을 수행한다.
명명된 캐스트 간의 이러한 구분을 통해 컴파일러는 최소한의 유형 검사를 적용하고 프로그래머가 reinterpret_cast로 표시되는 더 위험한 변환을 더 쉽게 찾을 수 있다. 일부 static_cast는 이식 가능하지만 reinterpret_cast는 거의 불가능하다. reinterpret_cast에 대해 어떤 보증도 쉽지 않지만 일반적으로 인수와 동일한 비트 패턴을 가진 새 유형의 값을 생성한다. 대상이 원래 값만큼 비트를 가지고 있으면 결과를 원래 유형으로 다시 reinterpret_cast하고 사용할 수 있다. reinterpret_cast의 결과는 그 결과가 정확한 원래 유형으로 다시 변환되는 경우에만 사용 가능하도록 보장된다. reinterpret_cast는 함수에 대한 포인터에 사용해야 하는 일종의 변환이다.
char x = 'a';
int∗ p1 = &x; // error : no implicit char* to int* conversion
int∗ p2 = static_cast<int∗>(&x); // error : no implicit char* to int* conversion
int∗ p3 = reinterpret_cast<int∗>(&x); // OK: on your head be it
struct B { /* ... */ };
struct D : B { /* ... */ }; // see §3.2.2 and §20.5.2
B∗ pb = new D; // OK: implicit conversion from D* to B*
D∗ pd = pb; // error : no implicit conversion from B* to D*
D∗ pd = static_cast<D∗>(pb); // OK
명시적 형식 변환을 사용하고 싶은 생각이 든다면 시간을 내어 그것이 정말로 필요한지 고려해야 한다. C++에서 명시적 형식 변환은 C가 필요할 때 대부분의 경우 필요하지 않으며 이전 버전의 C++에서 필요했던 경우도 마찬가지다. 많은 프로그램에서 명시적 형식 변환을 완전히 피할 수 있다. 그 외에는 그 사용이 몇 가지 루틴에 국한될 수 있다.
C-스타일 캐스트
C에서 C++는 식 e에서 유형 T의 값을 만들기 위해 static_cast, reinterpret_cast, const_cast들의 조합으로 표현될 수 있는 모든 변환을 수행하는 표기법 (T)e를 상속했다. 불행하게도 C-스타일 캐스트는 클래스에 대한 포인터에서 해당 클래스의 의 private 기반에 대한 포인터로 캐스트 할 수도 있다. 절대 그렇게 하지 말아야 한다. C-스타일 캐스트는 명명된 연산자보다 훨씬 더 위험하다. 그 이유는 표기법이 대형 프로그램에서 발견하기 어렵고 프로그래머가 의도한 변환 유형이 명시적이지 않기 대문이다. 즉 (T)e는 관련 유형 간의 이식 가능한 변환, 관련 없는 유형 간의 이식 불가능한 변환 또는 포인터 유형에서 const 수정자를 제거할 수 있다. T와 e의 정확한 유형을 모르면 알 수 없다.
함수-스타일 캐스트
값 e에서 유형 T 값의 구성은 기능 표기법 T(e)로 표현할 수 있다.
void f(double d)
{
int i = int(d); // truncate d
complex z = complex(d); // make a complex from d
// ...
}
T(e) 구문은 때대로 함수-스타일 캐스트라고 한다. 불행히도 built-in 유형의 T의 경우 T(e)는 (T)e와 동일하다. 이것은 많은 built-in 유형의 경우 T(e)가 안전하지 않다는 것을 의미한다.
void f(double d, char∗ p)
{
int a = int(d); // truncates
int b = int(p); // not portable
// ...
}
더 긴 정수 유형을 더 짧은 유형 (예:long에서 char로)으로 명시적으로 변환하더라도 이식할 수 없는 구현 정의 동작이 발생할 수 있다.
올바르게 작동하는 construction에는 T{v} 변환을 선호하고 다른 변환에는 명명된 캐스트(예:static_cast)를 선호한다.
'Program Language > C & C++' 카테고리의 다른 글
[C++] constexpr 함수 (0) | 2022.01.13 |
---|---|
[C++] 함수의 지정자와 수정자 (0) | 2022.01.13 |
[C++] lambda expression (0) | 2022.01.07 |
[C++] Constant Expressions (0) | 2022.01.06 |
[C++] token 요약 (0) | 2022.01.06 |