static_cast 변환

static_cast 변환 (static_cast conversion)

어떤 타입을 다른 타입으로 바꿀 때, 암묵적 변환과 사용자 정의 변환을 조합해서 변환하고 싶을 때 쓰는 게 static_cast예요. 이 페이지에서는 static_cast가 어떤 변환들을 처리할 수 있는지, 그리고 어떤 경우에 쓸 수 없는지를 규칙별로 정리할게요.

출처: cppreference

본문

static_cast암묵적 변환과 사용자 정의 변환의 조합으로 타입 사이를 변환해요.

문법

static_cast<target-type>(expression)

target-type 타입의 값을 반환해요.

동작 방식 (Explanation)

constness(혹은 휘발성)를 없애는 변환이 되지 않는 이상, 다음 변환들만 static_cast로 할 수 있어요.

  1. expression이 타입 "cv1Base"의 lvalue이고 target-type이 "Derived에 대한 참조"일 때, 다음 조건을 모두 만족하면 결과는 expression을 둘러싼 Derived 타입의 객체를 가리켜요:
  • Derived는 완전 클래스 타입이어야 해요.
  • BaseDerived의 기반 클래스이어야 해요.
  • cv1cv2보다 더 큰 cv-한정이면 안 돼요.

다음 조건 중 하나라도 만족하면 프로그램은 ill-formed예요:

  • BaseDerived가상 기반 클래스인 경우.
  • BaseDerived의 어떤 가상 기반 클래스의 기반 클래스인 경우.
  • "Derived에 대한 포인터"에서 "Base에 대한 포인터"로 가는 유효한 표준 변환이 존재하지 않는 경우.

expression이 실제로 Derived 타입 객체의 기반 클래스 하위 객체가 아니라면 동작은 정의되지 않아요(UB).

struct B {};
struct D : B { B b; };

D d;
B& br1 = d;
B& br2 = d.b;

static_cast<D&>(br1); // OK, 원래 "d" 객체를 가리키는 lvalue
static_cast<D&>(br2); // UB: "b" 하위 객체는 기반 클래스 하위 객체가 아님
2) target-type이 "Derived에 대한 rvalue 참조"이고 expression이 "Derived의 기반 클래스인 (cv-한정일 수 있는) Base" 타입의 xvalue일 때, 이 변환의 결과와 제약은 위 "Base lvalue를 Derived 참조로" 변환과 동일해요. 3) target-type이 rvalue 참조 타입이고, 참조되는 타입이 expression의 타입과 참조 호환일 때, static_cast는 expression의 glvalue, 클래스 prvalue, 또는 배열 prvalue(C++17 이전) / 모든 lvalue(C++17부터)를, 같은 객체(또는 target-type에 따라 그 기반 클래스 하위 객체)를 가리키는 xvalue로 변환해요.[1] target-type이 expression 타입의 접근 불가 또는 모호한 기반 클래스면 프로그램은 ill-formed예요. expression이 비트 필드 lvalue면 먼저 기본 타입의 prvalue로 변환돼요. (C++11부터)
  1. target-type이 (cv-한정일 수 있는) void면 이 변환은 결과가 없어요. 이때 expression은 버려진 값 표현식이 돼요.

  2. 그 외에, expression은 다음 조건 중 하나를 만족하면 target-type으로 명시적으로 변환될 수 있어요:

어떤 가상의 임시 변수 temp에 대해 target-typetemp(expression);선언이 잘 구성되어 있을 때. 이런 명시적 변환의 효과는 그 선언과 초기화를 수행한 뒤temp`를 변환 결과로 쓰는 것과 같아요. (C++17 이전)
다음 조건 중 하나를 만족할 때 — · expression에서 target-type으로 가는 암묵적 변환 시퀀스가 존재한다. · target-type 타입의 객체나 참조에 대한 직접 초기화오버로드 결정이 적어도 하나의 viable 함수를 찾는다. · target-type이 첫 요소 x를 갖는 집합 타입이고, expression에서 x의 타입으로 가는 암묵적 변환 시퀀스가 존재한다. (C++20부터). 명시적 변환은 다음과 같이 정의된다: · target-type이 참조 타입이면, 어떤 가상의 임시 변수 temp에 대해 target-typetemp(expression);선언·초기화를 수행한 뒤temp`를 결과로 쓰는 것과 같다. · 그렇지 않으면 결과 객체를 expression에서 직접 초기화한다. (C++17부터)
--- ---

프로그램이 static_cast를 사용해 ill-formed 표준 변환 시퀀스의 역변환을 수행하면 ill-formed예요.

  1. 그 외에, lvalue-to-rvalue, 배열-to-포인터, 함수-to-포인터 변환이 expression에 적용돼요. 이 변환들 이후에 static_cast로 수행할 수 있는 변환은 다음뿐이에요:

a) 범위 있는 열거 타입의 값을 정수 또는 부동소수점 타입으로 변환:

· target-type이 (cv-한정일 수 있는) bool이면, expression의 원래 값이 0이면 결과는 false, 그 외 모든 값은 true. · target-type이 (cv-한정일 수 있는) bool 이외의 정수 타입이면, expression의 원래 값을 target-type이 표현할 수 있다면 값은 그대로, 그렇지 않으면 결과 값은 미지정. (C++20 이전)
· target-type이 정수 타입이면, 결과는 열거의 기본 타입으로 변환한 뒤 다시 target-type으로 변환한 것과 같다. (C++20부터) · target-type이 부동소수점 타입이면, 결과는 원래 값을 target-type으로 변환한 것과 같다. (C++11부터)
--- ---

b) 정수 또는 열거 타입의 값을 임의의 완전 열거 타입으로 변환할 수 있어요.

  • target-type에 고정 기본 타입이 있으면, expression은 필요하다면 정수 승격이나 정수 변환으로 먼저 그 타입으로 바뀐 뒤 target-type으로 변환돼요.
  • target-type에 고정 기본 타입이 없으면, 원래 값이 열거 값의 범위 안에 있으면 값은 그대로, 그렇지 않으면 동작은 정의되지 않아요(UB).

c) 부동소수점 타입의 값도 임의의 완전 열거 타입으로 변환할 수 있어요. 결과는 expression의 원래 값을 먼저 target-type의 기본 타입으로 변환한 뒤 다시 target-type으로 변환한 것과 같아요.

d) 부동소수점 타입의 prvalue는 다른 부동소수점 타입으로 명시적으로 변환될 수 있어요. · expression의 원본 값을 target-type이 정확히 표현할 수 있으면 그 값은 바뀌지 않는다. · 그렇지 않고, 원본 값이 target-type이 표현 가능한 두 값 사이에 있으면, 변환 결과는 그 두 값 중 하나로 구현 정의되는 선택이다.[2] · 그 외에는 동작이 정의되지 않는다(UB). (C++23부터)

e) 타입 "cv1Base에 대한 포인터"의 값은 다음 조건을 모두 만족하면 "cv2Derived에 대한 포인터" 타입으로 명시적으로 변환될 수 있어요:

  • Derived는 완전 클래스 타입이어야 해요.
  • BaseDerived의 기반 클래스이어야 해요.
  • cv1cv2보다 더 큰 cv-한정이면 안 돼요.

expression이 널 포인터 값이면 결과는 target-type의 널 포인터 값이에요. 그렇지 않으면 결과는 expression이 가리키던 Base 타입 객체를 둘러싼 Derived 타입 객체를 가리키는 포인터예요.

다음 조건 중 하나라도 만족하면 프로그램은 ill-formed예요:

  • BaseDerived가상 기반 클래스인 경우.
  • BaseDerived의 어떤 가상 기반 클래스의 기반 클래스인 경우.
  • "Derived에 대한 포인터"에서 "Base에 대한 포인터"로 가는 유효한 표준 변환이 존재하지 않는 경우.

expression이 널 포인터 값이 아니면서 실제로 Derived 타입 객체의 기반 클래스 하위 객체를 가리키지 않으면 동작은 정의되지 않아요(UB).

f) 타입 "타입 cv1TDerived 멤버에 대한 포인터"의 값은 다음 조건을 모두 만족하면 "타입 cv2TBase 멤버에 대한 포인터" 타입으로 명시적으로 변환될 수 있어요:

  • Derived는 완전 클래스 타입이어야 해요.
  • BaseDerived의 기반 클래스이어야 해요.
  • cv1cv2보다 더 큰 cv-한정이면 안 돼요.

expression이 널 멤버 포인터 값이면 결과는 target-type의 널 멤버 포인터 값이에요. 그렇지 않으면 결과는 Base 클래스의 원래 (직접 또는 간접) 멤버를 가리키는 포인터예요.

"타입 TBase 멤버에 대한 포인터"에서 "타입 TDerived 멤버에 대한 포인터"로 가는 유효한 표준 변환이 존재하지 않으면 프로그램은 ill-formed예요.

expression이 널 멤버 포인터 값이 아니면서 가리키는 멤버가 Base 클래스의 (직접 또는 간접) 멤버가 아니면 동작은 정의되지 않아요(UB).

g) 타입 "cv1void에 대한 포인터"의 값은, T가 객체 타입이고 cv1cv2보다 더 큰 cv-한정이 아니면 "cv2T에 대한 포인터" 타입으로 명시적으로 변환될 수 있어요.

· expression이 널 포인터 값이면 결과는 target-type의 널 포인터 값. · expression이 메모리의 바이트주소A를 나타내고 AT정렬 요구를 만족하면, 결과 포인터 값도 A를 나타낸다. · 그 외의 포인터 변환 결과는 미지정. · expression이 "타입 cv3T에 대한 포인터" 객체에서의 이전 변환 결과라면, 결과는 원래 값을 가진다. (C++17 이전)
· expression이 메모리의 주소A바이트를 나타내는데 AT정렬 요구를 만족하지 않으면, 결과 포인터 값은 미지정. · 그렇지 않고 expression이 객체 a를 가리키고, a와 포인터-상호변환(아래 참고) 가능한 타입 T(cv-한정 무시)의 객체 b가 있으면, 결과는 b를 가리키는 포인터. · 그 외에는 변환에 의해 포인터 값이 바뀌지 않는다. (C++17부터)
--- ---

모든 캐스트 표현식과 마찬가지로, 결과는 다음과 같아요:

  • target-type이 lvalue 참조 타입이거나 함수 타입에 대한 rvalue 참조 타입이면 lvalue(C++11부터).
  • target-type이 객체 타입에 대한 rvalue 참조 타입이면 xvalue(C++11부터).
  • 그 외에는 prvalue.

포인터-상호변환 가능한 객체 (Pointer-interconvertible objects)

두 객체 ab는 다음 중 하나를 만족하면 포인터-상호변환 가능(pointer-interconvertible) 하다고 해요:

  • 같은 객체이거나,
  • 하나는 유니온 객체이고 다른 하나는 그 객체의 비-static 데이터 멤버이거나,
  • 하나는 표준 레이아웃 클래스 객체이고 다른 하나는 그 객체의 첫 비-static 데이터 멤버 또는 그 객체의 임의의 기반 클래스 하위 객체이거나,
  • ac가 포인터-상호변환 가능하고 cb가 포인터-상호변환 가능한 객체 c가 존재한다.
union U { int a; double b; } u;

void* x = &u;                    // x의 값은 "u에 대한 포인터"
double* y = static_cast<double*>(x);  // y의 값은 "u.b에 대한 포인터"
char* z = static_cast<char*>(x);      // z의 값은 "u에 대한 포인터"

주의할 점 (Notes)

기반→파생 변환(downcast)을 static_cast로 하면, 가리키는/참조하는 객체의 동적 타입Derived인지 런타임 검사를 하지 않아요. 이 전제 조건이 다른 수단으로 보장될 때만(예: 정적 다형성 구현 시) 안전하게 쓸 수 있어요. 안전한 downcast는 dynamic_cast로 할 수 있어요.

static_cast는 특정 타입으로의 함수-to-포인터 변환을 수행해 함수 오버로드를 명확히 구분하는 데도 쓸 수 있어요:

std::for_each(files.begin(), files.end(),
              static_cast<std::ostream&(*)(std::ostream&)>(std::flush));

키워드

static_cast

예제

여기서는 static_cast가 처리하는 여러 변환들을 실제 코드로 확인해 볼게요. 특히 downcast, lvalue→xvalue, void* 변환 등이 어떻게 동작하는지 보여줘요.

#include <iostream>
#include <vector>

struct B
{
    int m = 42;
    const char* hello() const { return "Hello world, this is B!\n"; }
};

struct D : B
{
    const char* hello() const { return "Hello world, this is D!\n"; }
};

enum class E { ONE = 1, TWO, THREE };
enum EU { ONE = 1, TWO, THREE };

int main()
{
    // 1. static downcast
    D d;
    B& br = d; // upcast는 암묵적 변환으로
    std::cout << "1) " << br.hello();
    D& another_d = static_cast<D&>(br); // downcast
    std::cout << "1) " << another_d.hello();

    // 3. lvalue를 xvalue로
    std::vector<int> v0{1, 2, 3};
    std::vector<int> v2 = static_cast<std::vector<int>&&>(v0);
    std::cout << "3) after move, v0.size() = " << v0.size() << '\n';

    // 4. 버려진 값 표현식
    static_cast<void>(v2.size());

    // 5. 초기화 변환
    int n = static_cast<int>(3.14);
    std::cout << "5) n = " << n << '\n';
    std::vector<int> v = static_cast<std::vector<int>>(10);
    std::cout << "5) v.size() = " << v.size() << '\n';

    // 6. 암묵적 변환의 역
    void* nv = &n;
    int* ni = static_cast<int*>(nv);
    std::cout << "6) *ni = " << *ni << '\n';

    // 7a. 범위 있는 열거를 int로
    E e = E::TWO;
    int two = static_cast<int>(e);
    std::cout << "7a) " << two << '\n';

    // 7b. int를 열거로, 열거를 다른 열거로
    E e2 = static_cast<E>(two);
    [[maybe_unused]] EU eu = static_cast<EU>(e2);

    // 7f. 멤버 포인터 upcast
    int D::*pm = &D::m;
    std::cout << "7f) " << br.*static_cast<int B::*>(pm) << '\n';

    // 7g. void*를 객체 포인터로
    void* voidp = &e;
    [[maybe_unused]] std::vector<int>* p = static_cast<std::vector<int>*>(voidp);
}

출력:

1) Hello world, this is B!
1) Hello world, this is D!
3) after move, v0.size() = 0
5) n = 3
5) v.size() = 10
6) *ni = 3
7a) 2
7f) 42

static_cast<D&>(br)로 기반 클래스 참조를 파생 클래스 참조로 내려서 another_d.hello()가 D의 버전을 호출해요. static_cast<std::vector<int>&&>(v0)는 v0를 xvalue로 만들어 v2로 이동하고 v0는 비워지죠. 7a에서 범위 있는 열거 E::TWO를 int로 바꾸면 2가 나오고, 7f에서는 D의 멤버 포인터를 B의 멤버 포인터로 올려서 접근해요.

결함 보고 (Defect reports)

다음 동작 변경 결함 보고는 과거에 발표된 C++ 표준에 소급 적용됐어요.

DR 적용 표준 발표 당시 동작 수정된 동작
CWG 137 C++98 void 포인터의 constness·volatility를 없앨 수 있었음 그런 경우 cv-한정을 없앨 수 없음
CWG 427 C++98 downcast가 직접 초기화와 모호할 수 있었음 이 경우 downcast를 선택
CWG 439 C++98 "객체 포인터"→"void 포인터"→원래로 되돌릴 때, 결과 타입이 같은 cv-한정이어야 값 보존 가능 cv-한정이 달라도 됨
CWG 1094 C++98 부동소수점→열거 변환이 미지정이었음 지정됨
CWG 1320 C++11 범위 있는 열거→bool 변환이 미지정이었음 지정됨
CWG 1412 C++98 "void 포인터"→"객체 포인터" 변환 결과가 불명확했음 명확해짐
CWG 1447 C++11 비트 필드→rvalue 참조 변환이 미지정이었음(비트 필드에 참조를 못 묶음) 지정됨
CWG 1766 C++98 정수/열거→열거 변환에서 range 밖이면 미지정 결과였음 이 경우 정의되지 않은 동작
CWG 1832 C++98 정수/열거→열거 변환이 불완전 target-type을 허용했음 허용 안 함
CWG 2224 C++98 기반 클래스 타입 멤버→파생 완전 객체 변환이 유효했음 이 경우 정의되지 않은 동작
CWG 2254 C++11 데이터 멤버가 없는 표준 레이아웃 클래스가 첫 기반 클래스와만 pointer-interconvertible이었음 임의의 기반 클래스와 pointer-interconvertible
CWG 2284 C++11 비-표준 레이아웃 유니온 객체와 그 비-static 데이터 멤버는 pointer-interconvertible이 아니었음 맞음
CWG 2310 C++98 기반→파생 포인터/멤버 포인터 변환에서 파생 클래스 타입이 불완전할 수 있었음 완전해야 함
CWG 2338 C++11 고정 기본 타입 열거 변환에서 range 밖이면 UB였음 먼저 기본 타입으로 변환(UB 없음)
CWG 2499 C++11 표준 레이아웃 클래스가 모든 기반 하위 객체 주소가 같아도 pointer-interconvertible이 아닌 기반을 가질 수 있었음 가지지 않음
CWG 2718 C++98 기반→파생 참조 변환에서 파생 클래스 타입이 불완전할 수 있었음 완전해야 함
CWG 2882 C++98 static_cast<void>(expr)가 expr에서 void로의 암묵 변환 시퀀스를 형성하려 하는지 불명확했음 이 경우 시도 안 함

더 알아보기