캐스트 표기법 및 safe_cast<> 도입
업데이트: 2007년 11월
Visual C++ 2008에서는 캐스트 표기법이 Managed Extensions for C++와 다르게 변경되었습니다.
기존의 구조를 수정하는 일은 새로운 구조를 처음 만드는 일과 다르며 보다 어렵습니다. 자유롭게 수정할 수 있는 여지가 적고, 기존의 구조적 한계 내에서 실현 가능한 요소와 이상적인 재구성 사이에서 절충하여 해결책을 찾아야 하는 경우가 많습니다.
언어 확장을 또 다른 예로 들 수 있습니다. 1990년대 초반, 개체 지향 프로그래밍이 중요한 패러다임으로 자리잡았을 때 C++에서 형식이 안전한 다운캐스트 기능을 요구하는 목소리가 높아졌습니다. 다운캐스팅은 사용자가 기본 클래스 포인터나 참조를 파생 클래스의 포인터나 참조로 명시적으로 변환하는 것입니다. 다운캐스팅에는 명시적 캐스팅이 필요합니다. 그 이유는 기본 클래스 포인터의 실제 형식이 런타임에 확인되므로 컴파일러가 이를 검사할 수 없기 때문입니다. 다시 말해 다운캐스트 기능은 가상 함수 호출과 마찬가지로 일정한 형태의 동적 해결책을 필요로 합니다. 여기서 두 가지 의문점이 떠오를 수 있습니다.
개체 지향 패러다임에 다운캐스트가 필요한 까닭이 무엇일까요? 가상 함수 메커니즘으로 충분하지 않은가요? 즉, 다운캐스트 또는 다른 종류의 캐스트가 필요하다면 디자인이 잘못되었기 때문이 아닐까요?
C++에서 다운캐스트 지원이 문제가 되는 까닭이 무엇일까요? 어쨌거나 Smalltalk 또는 Java와 C# 같은 개체 지향 언어에서는 이 문제가 대두된 적이 없지 않습니까? C++의 어떠한 측면이 다운캐스트 기능에 대한 지원을 어렵게 만드는 것일까요?
가상 함수는 형식 패밀리에 공통되는 형식 의존적 알고리즘을 나타냅니다. ISO-C++에서는 지원되지 않지만 CLR 프로그래밍에서 사용할 수 있는 흥미로운 디자인 대안인 인터페이스는 여기에서 고려하지 않습니다. 이 패밀리의 디자인은 일반적으로 공용 인터페이스(가상 함수)를 선언하는 추상 기본 클래스와 응용 프로그램 도메인의 실제 패밀리 형식을 나타내는 구체적인 파생 클래스 집합으로 이루어진 클래스 계층 구조로 표현됩니다.
예를 들어, CGI(Computer Generated Imagery) 응용 프로그램 도메인의 Light 계층 구조에는 color, intensity, position, on, off 등과 같은 공통 특성이 있습니다. 사용자는 특정 광원이 스폿 조명인지, 지향성 조명인지, 태양과 같은 비지향성 조명인지 또는 차광판 조명인지 여부에 관계없이 공통 인터페이스를 통해 여러 광원을 제어할 수 있습니다. 이러한 경우 해당 가상 인터페이스를 활용하기 위해 특정 광원 유형으로 다운캐스팅할 필요가 없습니다. 그러나 프로덕션 환경에서는 실행 속도가 매우 중요합니다. 가상 메커니즘을 사용하는 대신 다운캐스팅한 다음 각 메서드를 명시적으로 호출하여 인라인으로 호출을 수행할 수 있다면 이러한 방법을 선택할 수 있습니다.
따라서 C++에서 다운캐스팅하는 이유 중 하나는 가상 메커니즘을 적용하지 않는 대신 런타임 성능을 크게 향상시키는 데 있습니다. 이 수동 최적화를 자동화하는 문제는 현재 활발한 연구가 진행되고 있는 분야입니다. 그러나 이는 register 또는 inline 키워드를 명시적으로 사용하지 않는 것보다 해결하기 어려운 과제입니다.
다운캐스팅하는 두 번째 이유는 다형성의 이중적인 성격에 있습니다. 다형성은 수동적 형태와 능동적 형태의 쌍으로 이해할 수 있습니다.
가상 호출과 다운캐스트 기능은 다형성의 능동적인 사용을 나타냅니다. 즉, 프로그램을 실행할 때 특정 인스턴스에서 기본 클래스 포인터의 실제 형식을 기반으로 작업을 수행합니다.
그러나 이 기본 클래스 포인터에 파생 클래스 개체를 할당하는 것은 다형성의 수동적 형태입니다. 이 경우 다형성은 전송 메커니즘으로 사용됩니다. 이는 사전 일반 CLR 프로그래밍 등에서 Object를 사용하는 주된 용도입니다. 수동적으로 사용하는 경우 전송 및 저장을 위해 선택된 기본 클래스 포인터는 일반적으로 너무 추상적인 인터페이스를 제공합니다. 예를 들어, Object는 해당 인터페이스를 통해 약 5개의 메서드를 제공합니다. 더 구체적인 동작을 수행하려면 명시적 다운캐스트가 필요합니다. 즉, 스폿 조명의 각도나 감쇠 비율을 조정하려면 명시적으로 다운캐스팅해야 할 수 있습니다. 하위 형식 패밀리 내의 가상 인터페이스는 사실상 여러 해당 하위 항목의 가능한 모든 메서드에 대한 상위 집합이 될 수 없으므로 개체 지향 언어에서는 다운캐스트 기능이 항상 필요합니다.
개체 지향 언어에서 안전한 다운캐스트 기능이 필요하다면 C++에서 그러한 기능을 추가하는 데 그토록 오랜 세월이 걸린 이유가 무엇일까요? 문제는 포인터의 런타임 형식과 관련하여 정보를 사용할 수 있도록 만드는 방식에 있습니다. 가상 함수의 경우 런타임 정보는 컴파일러에서 두 부분으로 설정됩니다.
클래스 개체에는 해당 가상 테이블을 가리키는 가상 테이블 포인터 멤버가 추가로 들어 있습니다. 이 멤버는 클래스 개체 맨 앞이나 맨 뒤에 있습니다. 예를 들어 스폿 조명 개체는 스폿 조명 가상 테이블을 가리키고, 지향성 조명 개체는 지향성 조명 가상 테이블을 가리키는 방식입니다.
각 가상 함수에는 테이블에 고정된 슬롯이 연결되어 있으며 호출할 실제 인스턴스는 테이블 내에 저장된 주소로 나타냅니다. 예를 들어 가상 Light 소멸자는 슬롯 0에 연결되고, Color는 슬롯 1에 연결되는 식입니다. 이는 컴파일 타임에 설정되고 최소한의 오버헤드를 나타내므로 전략에 융통성이 없는 경우 효율적입니다.
그렇다면 문제는 두 번째 주소를 추가하거나 특정한 유형의 형식 인코딩을 직접 추가하는 방법으로 C++ 포인터의 크기를 변경하지 않은 채 포인터에서 형식 정보를 사용할 수 있게 하는 방법에 있습니다. 당시에 아직 사용자 층이 좁았던 개체 지향 패러다임을 사용하지 않는 경우에는 이 방법이 불가능합니다. 다른 해결책으로 다형 클래스 형식에 대한 특수 포인터를 도입할 수도 있었지만 이 방법은 너무 복잡하며, 특히 포인터 산술 연산 문제와 관련하여 두 가지를 함께 사용하기가 어려워집니다. 각 포인터를 현재 관련된 형식에 연결하는 런타임 테이블을 유지하고 이를 동적으로 업데이트하는 방법도 적절하지 않습니다.
이제 문제는 서로 다르지만 합당한 프로그래밍 관점을 가진 두 사용자 집단을 중재하는 데 있습니다. 각 집단의 관점에 맞으면서도 상호 호환성이 있는 절충안을 찾아야 합니다. 즉, 어느 한쪽에서 제시한 해결책만으로는 실현 가능한 합의점에 도달할 수 없고, 최종적으로 구현되는 해결책에서 완벽한 내용을 기대하기는 어렵습니다. 실제 결론은 다형 클래스의 정의에 대한 고찰에서 출발합니다. 다형 클래스는 가상 함수가 포함된 클래스입니다. 다형 클래스는 형식이 안전한 동적 다운캐스트를 지원합니다. 포인터를 주소로 관리하는 문제에 대한 해결의 실마리는 여기서 찾을 수 있습니다. 모든 다형 클래스에는 관련 가상 테이블에 대한 이 추가 포인터 멤버가 포함되어 있기 때문입니다. 따라서 관련 형식 정보를 확장된 가상 테이블 구조에 저장할 수 있습니다. 형식이 안전한 다운캐스트를 사용하는 대가는 거의 대부분 이 기능을 사용하는 사용자로만 국한됩니다.
형식이 안전한 다운캐스트와 관련된 다음 문제는 구문에 대한 것입니다. 이는 캐스트의 일종이므로 ISO-C++ 위원회에 처음 제안된 내용에서는 다음 예제와 같이 단순한 캐스트 구문을 사용했습니다.
spot = ( SpotLight* ) plight;
그러나 이 제안은 위원회에서 부결되었습니다. 사용자가 캐스트의 대가를 제어할 방도가 없었기 때문입니다. 동적 형식이 안전한 다운캐스트의 구문이 이전의 안전하지 않지만 정적인 캐스트 표기법과 동일하다면 캐스트 방식이 완전히 바뀌게 되어 다운캐스트가 불필요하거나 효율성이 낮은 경우 사용자가 런타임 오버헤드를 억제할 방법이 전혀 없습니다.
일반적으로 C++에는 컴파일러가 지원하는 기능을 억제하기 위한 메커니즘이 항상 제공됩니다. 예를 들어 클래스 범위 연산자(Box::rotate(angle))를 사용하거나 해당 클래스의 포인터나 참조가 아닌 클래스 개체를 통해 가상 메서드를 호출하는 방법으로 가상 메커니즘을 해제할 수 있습니다. 두 번째 억제 방법은 언어의 요구 사항은 아니지만 구현 품질에 관련된 문제이며, 이는 다음과 같은 형식의 선언에서 임시 요소 생성을 억제하는 문제와 비슷합니다.
// compilers are free to optimize away the temporary
X x = X::X( 10 );
따라서 이 제안은 보다 심도있는 논의를 위해 기각되었고 여러 가지 대체 표기법이 논의되었습니다. 그 중 하나가 위원회에 다시 제출되었는데, 이는 자신의 비확정적인 동적 특성을 나타내는 형태(?type)였습니다. 이 표기법을 사용하면 정적 형태와 동적 형태 사이를 전환할 수 있었지만 누구도 이 제안에 만족하지 않았습니다. 결국 처음부터 다시 시작해야 했습니다. 다행히 세 번째 표기법 제안은 성공적이었으며 이는 이제 표준 dynamic_cast<type> 형태로 자리잡고 있습니다. 이 표기법은 네 가지 새로운 스타일의 캐스트 표기법 집합으로 일반화되었습니다.
ISO-C++에서 dynamic_cast는 적절하지 못한 포인터 형식에 적용되는 경우 0을 반환하고, 참조 형식에 적용되는 경우 std::bad_cast 예외를 throw합니다. Managed Extensions for C++에서 dynamic_cast를 관리되는 참조 형식에 적용하면 해당 포인터 표현으로 인해 항상 0이 반환됩니다. __try_cast<type>는 캐스팅에 실패하는 경우 System::InvalidCastException을 throw한다는 점을 제외하고는 dynamic_cast의 변형을 throw하는 예외에 상응하는 것으로 도입되었습니다.
public __gc class ItemVerb;
public __gc class ItemVerbCollection {
public:
ItemVerb *EnsureVerbArray() [] {
return __try_cast<ItemVerb *[]>
(verbList->ToArray(__typeof(ItemVerb *)));
}
};
새 구문에서 __try_cast는 safe_cast로 변경되었습니다. 동일한 코드 조각을 새 구문으로 표현하면 다음과 같습니다.
public ref class ItemVerb;
public ref class ItemVerbCollection {
public:
array<ItemVerb^>^ EnsureVerbArray() {
return safe_cast<array<ItemVerb^>^>
( verbList->ToArray( ItemVerb::typeid ));
}
};
관리되는 프로그래밍 환경에서는 프로그래머가 코드를 비안정형으로 만드는 방식으로 형식 간에 캐스팅할 수 있는 기능을 제한하여 안전형 코드를 생성하는 것이 중요합니다. 이는 새 구문에서 표현하는 동적 프로그래밍 패러다임의 중요한 측면입니다. 따라서 이전 스타일의 캐스트 인스턴스는 다음과 같이 런타임 캐스트로 내부에서 변경됩니다.
// internally recast into the
// equivalent safe_cast expression above
( array<ItemVerb^>^ ) verbList->ToArray( ItemVerb::typeid );
반면, 다형성은 능동적 모드와 수동적 모드를 모두 제공하므로 때로는 하위 형식의 비가상 API에 액세스하기 위해 다운캐스트를 수행해야 할 수도 있습니다. 클래스 멤버에서 계층 구조 내의 형식을 가리켜야 하지만(전송 메커니즘으로서의 수동적 다형성) 특정 프로그램 컨텍스트 내에서 이와 관련된 실제 인스턴스가 알려져 있는 경우를 예로 들 수 있습니다. 이러한 경우 런타임에 캐스트를 검사하면 오버헤드가 너무 커집니다. 새 구문이 관리되는 시스템 프로그래밍 언어로 사용하기 위한 것이라면 컴파일 타임(정적) 다운캐스트를 수행할 수 있는 방법을 제공해야 합니다. 이러한 이유로 인해 static_cast 표기법을 사용한 응용 프로그램에서 컴파일 타임 다운캐스트를 계속 수행할 수 있게 되었습니다.
// ok: cast performed at compile-time.
// No run-time check for type correctness
static_cast< array<ItemVerb^>^>(verbList->ToArray(ItemVerb::typeid));
문제는 static_cast를 수행하는 프로그래머의 판단이 올바르고 합리적이라는 보장이 없다는 데 있습니다. 즉, 관리 코드를 항상 안정형 코드로 만들 수는 없습니다. 이는 네이티브보다는 동적 프로그램 패러다임에서 훨씬 더 시급한 과제이지만 시스템 프로그래밍 언어 내에서 사용자가 정적 캐스트와 런타임 캐스트 사이를 전환하지 못하도록 금지하기에는 부족한 부분이 많습니다.
그러나 새 구문에는 성능과 관련된 주의 사항이 있습니다. 네이티브 프로그래밍의 경우 이전 스타일의 캐스트 표기법과 새 스타일의 static_cast 표기법 사이에 성능 상의 차이가 없습니다. 그러나 새 구문에서는 이전 스타일의 캐스트 표기법이 새로운 스타일의 static_cast 표기법보다 성능이 크게 떨어집니다. 그 이유는 컴파일러에서 이전 스타일 표기법을 예외를 throw하는 런타임 검사로 내부적으로 변환하기 때문입니다. 또한 catch되지 않은 예외가 발생하여 응용 프로그램이 중지될 수 있으므로 코드의 실행 프로필도 변경됩니다. 이는 현명한 조치이지만, static_cast 표기법을 사용하면 같은 오류가 나타나도 이러한 예외가 발생하지 않습니다. 이러한 조치로 인해 사용자가 새로운 스타일의 표기법을 사용하도록 강요된다는 의견이 있을 수도 있습니다. 그러나 이는 오류에 대비하기 위한 조치입니다. 이렇게 하지 않으면 다음 코드 예제와 같이 이전 스타일 표기법을 사용하는 프로그램의 실행 속도가 크게 저하되어도 명확한 이유를 찾을 수 없게 됩니다.
// pitfall # 1:
// initialization can remove a temporary class object,
// assignment cannot
Matrix m;
m = another_matrix;
// pitfall # 2: declaration of class objects far from their use
Matrix m( 2000, 2000 ), n( 2000, 2000 );
if ( ! mumble ) return;