명시적 캐스트

명시적 캐스트 (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 — (가능하면 한정된) 식별자 (템플릿 식별자 포함).

임의 개수의 값을 대상 타입의 값 하나로 명시적으로 변환해요.

  1. 명시적 타입 변환 (캐스트 표기), C 스타일 캐스트라고도 불러요. 2-7) 명시적 타입 변환 (함수 표기), 함수 스타일 캐스트라고도 불러요.

설명

  1. 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-idunary-expression의 타입이 둘 다 불완전 클래스 타입에 대한 포인터라면, static_castreinterpret_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.
  • 그렇지 않고 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} 덕분에 yq의 별칭이 아니라 복사본이 되어 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_castconst_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)