명시적 캐스트
명시적 캐스트 (Explicit cast)
명시적 변환과 암시적 변환의 조합을 사용해 타입 사이를 변환하는 문법이에요. C 스타일 캐스트라고 부르는 (type)expr 표기와, 함수 호출처럼 쓰는 함수형 표기 두 가지가 있어요.
출처: cppreference
본문
문법
( type-id ) unary-expression (1)
simple-type-specifier ( expression-list (optional) ) (2) (C++11 이전/이후)
simple-type-specifier ( initializer-list (optional) ) (2) (C++11 이후)
simple-type-specifier { initializer-list (optional) } (3) (C++11 이후)
simple-type-specifier { designated-initializer-list } (4) (C++20 이후)
typename identifier ( initializer-list (optional) ) (5) (C++11 이후)
typename identifier { initializer-list (optional) } (6) (C++11 이후)
typename identifier { designated-initializer-list } (7) (C++20 이후)
type-id— type-id.unary-expression— 단항 표현식 (그 최상위 연산자의 우선순위가 C 스타일 캐스트보다 높지 않은 것).simple-type-specifier— 단순 타입 지정자.expression-list— 쉼표로 구분된 표현식 목록 (괄호로 묶이지 않은 콤마 표현식 제외).initializer-list— 쉼표로 구분된 초기화 절 목록.designated-initializer-list— 쉼표로 구분된 지정 초기화 절 목록.identifier— (가능하면 한정된) 식별자 (템플릿 식별자 포함).
임의 개수의 값을 대상 타입의 값 하나로 명시적으로 변환해요.
- 명시적 타입 변환 (캐스트 표기), C 스타일 캐스트라고도 불러요. 2-7) 명시적 타입 변환 (함수 표기), 함수 스타일 캐스트라고도 불러요.
설명
- C 스타일 캐스트를 만나면 컴파일러는 다음과 같은 캐스트 표현식으로 해석하려고 시도해요 (이 순서대로):
a)
const_cast<type-id>(unary-expression);b)static_cast<type-id>(unary-expression);— 확장 포함: 기본 클래스가 접근 불가능한(private 상속) 경우에도, 파생 클래스에 대한 포인터/참조를 모호하지 않은 기본 클래스에 대한 포인터/참조로 (그 반대도) 캐스팅하는 것이 추가로 허용돼요. 멤버 포인터를 모호하지 않은 비-가상 기본의 멤버 포인터로 캐스팅하는 것도 마찬가지예요. c)static_cast(확장 포함) 뒤에const_cast. d)reinterpret_cast<type-id>(unary-expression);e)reinterpret_cast뒤에const_cast. 해당 캐스트 연산자의 요구 사항을 만족시키는 첫 번째 선택이 선택돼요, ill-formed여도요 (예제 참고).static_cast뒤에const_cast를 쓴 것이, 그런 변환으로 두 가지 이상의 방식으로 해석될 수 있다면 그 변환은 ill-formed예요. 게다가 C 스타일 캐스트는 불완전 클래스 타입에 대한 포인터로/에서/사이에 캐스팅할 수 있어요.type-id와unary-expression의 타입이 둘 다 불완전 클래스 타입에 대한 포인터라면,static_cast와reinterpret_cast중 어느 것이 선택되는지는 미지정이에요.
2-7) 함수 스타일 캐스트는 타입(simple-type-specifier 또는 identifier(C++11 이후))과 초기화자(나머지 부분)를 지정해요. 그것은 대상 타입 T의 값을 구성하며, T는 지정된 타입과 초기화자로부터 결정돼요 (C++17 이후):
- (C++17 이전)
T는 지정된 타입. - (C++17 이후)
T는 다음과 같이 결정돼요:- 지정된 타입이 추론된 클래스 타입의 자리 표시자이면,
T는 클래스 템플릿 추론을 위한 오버로드 해석으로 선택된 함수의 반환 타입. - (C++23 이후) 그렇지 않고 지정된 타입이 자리 표시자 타입을 포함하면,
T는 추론된 타입. - 그렇지 않으면
T는 지정된 타입. 변환 결과는 다음과 같이 결정돼요:
- 지정된 타입이 추론된 클래스 타입의 자리 표시자이면,
- 함수 스타일 캐스트가 문법 (2)이고, 괄호 안에 표현식이 정확히 하나라면, 이 캐스트는 대응하는 C 스타일 캐스트와 동등해요.
- 그렇지 않고
T가 (가능하면 cv 한정된)void라면, 결과는 초기화를 수행하지 않는void타입의 rvalue(C++11 이전) / prvalue(C++11 이후)예요.- (C++11 이전) 초기화자가
()가 아니면 프로그램은 ill-formed. - (C++11 이후) 초기화자가 (팩 확장 후에)
()또는{}가 아니면 프로그램은 ill-formed.
- (C++11 이전) 초기화자가
- 그렇지 않고
T가 참조 타입이면, 함수 스타일 캐스트는 발명된 변수t(타입T)를 지정된 초기화자로 직접 초기화한 것과 같은 효과를 가지며, 결과는 초기화된t예요.- (C++11 이전) 결과는 lvalue.
- (C++11 이후)
T가 lvalue 참조 타입이거나 함수 타입에 대한 rvalue 참조이면 결과는 lvalue. 그렇지 않으면 결과는 xvalue.
- 그렇지 않으면 결과는
T타입의 rvalue(C++11 이전) / prvalue(C++11 이후)예요. 임시 객체를 가리키며(C++17 이전) 그 결과 객체는(C++17 이후) 지정된 초기화자로 직접 초기화돼요.
모호성 해결 (Ambiguity Resolution)
모호한 선언 문장
함수 스타일 캐스트 표현식을 가장 왼쪽 하위 표현식으로 가진 표현식 문장과 선언 문장 사이의 모호성에서는, 선언으로 취급해서 해결해요. 이 구분은 순수하게 구문적이에요. 문장에 나타나는 이름이 타입 이름인지 아닌지 외에 그 의미를 고려하지 않아요:
struct M {};
struct L { L(M&); };
M n;
void f()
{
M(m); // declaration, equivalent to M m;
L(n); // ill-formed declaration, equivalent to L n;
L(l)(m); // still a declaration, equivalent to L l((m));
}
(C++11 이후) 하지만 모호한 선언 문장의 가장 바깥쪽 선언자에 후행 반환 타입이 있으면, 후행 반환 타입이 auto로 시작할 때만 문장을 선언 문장으로 취급해요:
struct M;
struct S
{
S* operator()();
int N;
int M;
void mem(S s)
{
auto(s)()->M; // expression (S::M hides ::M), invalid before C++23
}
};
void f(S s)
{
{
auto(s)()->N; // expression, invalid before C++23
auto(s)()->M; // function declaration, equivalent to M s();
}
{
S(s)()->N; // expression
S(s)()->M; // expression
}
}
모호한 함수 매개변수
위의 모호성이 선언의 맥락에서도 발생할 수 있어요. 그 맥락에서는, 함수 스타일 캐스트를 초기화자로 가진 객체 선언과, 매개변수 이름 주위에 불필요한 괄호 세트를 가진 함수 선언자를 포함한 선언 사이의 선택이 문제예요. 해결은 선언이 될 수 있을 법한 그 어떤 구조체(잠재적 매개변수 선언 같은)도 선언으로 간주하는 것이에요:
struct S
{
S(int);
};
void foo(double a)
{
S w(int(a)); // function declaration: has a parameter `a` of type int
S x(int()); // function declaration: has an unnamed parameter of type int(*)()
// that is adjusted from int()
// Ways to avoid ambiguity:
S y((int(a))); // object declaration: extra pair of parentheses
S y((int)a); // object declaration: C-style cast
S z = int(a); // object declaration: no ambiguity for this syntax
}
(C++11 이후) 하지만 모호한 매개변수 선언의 가장 바깥쪽 선언자에 후행 반환 타입이 있으면, auto로 시작할 때만 선언으로 취급해 모호성을 해결해요:
typedef struct BB { int C[2]; } *B, C;
void foo()
{
S a(B()->C); // object declaration: B()->C cannot declare a parameter
S b(auto()->C); // function declaration: has an unnamed parameter of type C(*)()
// that is adjusted from C()
}
모호한 type-id
함수 스타일 캐스트와 type-id의 유사성에서 모호성이 생길 수 있어요. 해결은, 구문 맥락에서 type-id가 될 수 있을 법한 그 어떤 구조체도 type-id로 간주해야 한다는 것이에요:
// 'int()' and 'int(unsigned(a))' can both be parsed as type-id:
// 'int()' represents a function returning int
// and taking no argument
// 'int(unsigned(a))' represents a function returning int
// and taking an argument of type unsigned
void foo(signed char a)
{
sizeof(int()); // type-id (ill-formed)
sizeof(int(a)); // expression
sizeof(int(unsigned(a))); // type-id (ill-formed)
(int()) + 1; // type-id (ill-formed)
(int(a)) + 1; // expression
(int(unsigned(a))) + 1; // type-id (ill-formed)
}
(C++11 이후) 하지만 모호한 type-id의 가장 바깥쪽 추상 선언자에 후행 반환 타입이 있으면, auto로 시작할 때만 type-id로 취급해 해결해요:
typedef struct BB { int C[2]; } *B, C;
void foo()
{
sizeof(B()->C[1]); // OK, sizeof(expression)
sizeof(auto()->C[1]); // error: sizeof of a function returning an array
}
Notes
| 기능 테스트 매크로 | 값 | 표준 | 기능 |
|---|---|---|---|
__cpp_auto_cast |
202110L |
(C++23) | auto(x)와 auto{x} |
예제
여기서 확인할 것: C 스타일 캐스트와 함수 스타일 캐스트의 쓰임, 불완전 타입 사이의 캐스트, C++23의 auto{q}로 prvalue를 만들어 별칭(alias)이 아니라 복사본을 만드는 패턴을 보는 코드예요.
#include <cassert>
#include <iostream>
double f = 3.14;
unsigned int n1 = (unsigned int)f; // C-style cast
unsigned int n2 = unsigned(f); // function-style cast
class C1;
class C2;
C2* foo(C1* p)
{
return (C2*)p; // casts incomplete type to incomplete type
}
void cpp23_decay_copy_demo()
{
auto inc_print = [](int& x, const int& y)
{
++x;
std::cout << "x:" << x << ", y:" << y << '\n';
};
int p{1};
inc_print(p, p); // prints x:2 y:2, because param y here is an alias of p
int q{1};
inc_print(q, auto{q}); // prints x:2 y:1, auto{q} (C++23) casts to prvalue,
// so the param y is a copy of q (not an alias of q)
}
// In this example, C-style cast is interpreted as static_cast
// even though it would work as reinterpret_cast
struct A {};
struct I1 : A {};
struct I2 : A {};
struct D : I1, I2 {};
int main()
{
D* d = nullptr;
// A* a = (A*)d; // compile-time error
A* a = reinterpret_cast<A*>(d); // this compiles
assert(a == nullptr);
cpp23_decay_copy_demo();
}
출력:
x:2 y:2
x:2 y:1
여기서 주의할 점은, auto{q} 덕분에 y가 q의 별칭이 아니라 복사본이 되어 x:2 y:1이 나온다는 거예요.
결함 보고 (Defect reports)
다음의 동작 변경 결함 보고는 이전에 발표된 C++ 표준에 소급 적용됐어요.
| DR | 적용 대상 | 발표 당시 동작 | 올바른 동작 |
|---|---|---|---|
| CWG 1223 (P2915R0) | C++11 | 후행 반환 타입의 추가가 더 많은 모호성을 도입했음 | 해결함 |
| CWG 1893 | C++11 | 함수 스타일 캐스트가 팩 확장을 고려하지 않았음 | 고려함 |
| CWG 2351 | C++11 | void{}는 ill-formed였음 |
well-formed가 됨 |
| CWG 2620 | C++98 | 모호한 함수 매개변수의 해결이 잘못 해석될 수 있었음 | 명확히 개선 |
| CWG 2828 | C++98 | static_cast 뒤 const_cast의 여러 해석이 (실제로 쓰이는지와 무관하게) 존재하면 C 스타일 캐스트가 ill-formed였음 |
실제로 쓰일 수 있는 변환만 고려 |
| CWG 2894 | C++98 | 함수 스타일 캐스트가 참조 rvalue를 만들 수 있었음 | 참조 lvalue만 만들 수 있음 |
참고 문헌
- C++23 표준 (ISO/IEC 14882:2024): 7.6.1.4 Explicit type conversion (functional notation) [expr.type.conv], 7.6.3 Explicit type conversion (cast notation) [expr.cast]
- C++20 표준 (ISO/IEC 14882:2020): 7.6.1.4 Explicit type conversion (functional notation) [expr.type.conv], 7.6.3 Explicit type conversion (cast notation) [expr.cast]
- C++17 표준 (ISO/IEC 14882:2017): 8.2.3 Explicit type conversion (functional notation) [expr.type.conv], 8.4 Explicit type conversion (cast notation) [expr.cast]
- C++14 표준 (ISO/IEC 14882:2014): 5.2.3 Explicit type conversion (functional notation) [expr.type.conv], 5.4 Explicit type conversion (cast notation) [expr.cast]
- C++11 표준 (ISO/IEC 14882:2011): 5.2.3 Explicit type conversion (functional notation) [expr.type.conv], 5.4 Explicit type conversion (cast notation) [expr.cast]
- C++03 표준 (ISO/IEC 14882:2003): 5.2.3 Explicit type conversion (functional notation) [expr.type.conv], 5.4 Explicit type conversion (cast notation) [expr.cast]
- C++98 표준 (ISO/IEC 14882:1998): 5.2.3 Explicit type conversion (functional notation) [expr.type.conv], 5.4 Explicit type conversion (cast notation) [expr.cast]
더 알아보기 (Learn more)
- const_cast 변환 — const 추가/제거
- static_cast 변환 — 기본 변환 수행
- dynamic_cast 변환 — 검사된 다형성 변환
- reinterpret_cast 변환 — 일반적인 저수준 변환
- 표준 변환 — 한 타입에서 다른 타입으로의 암시적 변환