타입

타입 (Type)

객체, 참조, 함수(함수 템플릿 특수화 포함)와 표현식은 **타입(type)**이라는 속성을 가져요. 타입은 그 엔티티에 허용되는 연산을 제한하고, 그 외에는 무의미한 비트 나열에 의미를 부여해요. C++에서 "이 값은 정수다", "이건 포인터다" 같은 분류가 왜 중요한지, 그리고 그 분류가 어떤 기준으로 나뉘는지를 정리해 볼게요.

출처: cppreference

본문

타입 분류

C++ 타입 시스템은 다음 타입들로 이루어져요.

  • 기본 타입(fundamental types, std::is_fundamental 참고):
    • void 타입(std::is_void 참고)
    • std::nullptr_t 타입(std::is_null_pointer 참고) (since C++11)
    • std::meta::info 타입(std::is_reflection 참고) (since C++26)
    • 산술 타입(arithmetic types, std::is_arithmetic 참고):
      • 정수 타입(integral types, cv-한정 버전 포함, std::is_integral 참고. 정수 타입과 같은 뜻으로 integer type이라고도 함):
        • bool 타입
        • 문자 타입(character types):
          • 좁은 문자 타입(narrow character types):
            • 보통 문자 타입(ordinary character types): char, signed char, unsigned char¹
            • char8_t 타입 (since C++20)
          • 넓은 문자 타입(wide character types): char16_t, char32_t (since C++11), wchar_t
        • 부호 있는 정수 타입(signed integer types):
          • 표준 부호 있는 정수 타입: signed char, short, int, long, long long (since C++11)
          • 확장 부호 있는 정수 타입(구현 정의) (since C++11)
        • 부호 없는 정수 타입(unsigned integer types):
          • 표준 부호 없는 정수 타입: unsigned char, unsigned short, unsigned, unsigned long, unsigned long long (since C++11)
          • 확장 부호 없는 정수 타입(각각 확장 부호 있는 정수 타입에 대응하고 그 반대도 마찬가지) (since C++11)
      • 부동소수점 타입(floating-point types, std::is_floating_point 참고):
        • 표준 부동소수점 타입: float, double, long double 및 그 cv-한정 버전
        • 확장 부동소수점 타입(cv-한정 버전 포함):
          • 고정 폭 부동소수점 타입
          • 기타 구현 정의 확장 부동소수점 타입 (since C++23)
  • 복합 타입(compound types, std::is_compound 참고):
    • 참조 타입(reference types, std::is_reference 참고):
      • lvalue 참조 타입(std::is_lvalue_reference 참고):
        • 객체 타입에 대한 lvalue 참조
        • 함수 타입에 대한 lvalue 참조
      • rvalue 참조 타입(std::is_rvalue_reference 참고): (since C++11)
        • 객체 타입에 대한 rvalue 참조
        • 함수 타입에 대한 rvalue 참조
    • 포인터 타입(pointer types, std::is_pointer 참고):
      • 객체 포인터 타입(object pointer types):
        • 객체에 대한 포인터 타입
        • void에 대한 포인터 타입
      • 함수에 대한 포인터 타입
      • 멤버 포인터 타입(std::is_member_pointer 참고):
        • 데이터 멤버에 대한 포인터 타입(std::is_member_object_pointer 참고)
        • 멤버 함수에 대한 포인터 타입(std::is_member_function_pointer 참고)
    • 배열 타입(array types, std::is_array 참고)
    • 함수 타입(function types, std::is_function 참고)
    • 열거형 타입(enumeration types, std::is_enum 참고):
      • 스코프 없는 열거형 타입
      • 스코프 있는 열거형 타입(std::is_scoped_enum 참고) (since C++11)
    • 클래스 타입(class types):
      • 비공용체 타입(std::is_class 참고)
      • 공용체 타입(std::is_union 참고)
  1. signed char와 unsigned char는 좁은 문자 타입이지만 문자 타입(character type)은 아니에요. 다시 말해 좁은 문자 타입의 집합은 문자 타입 집합의 부분집합이 아니에요.

참조·함수를 제외한 모든 cv-한정이 없는 타입에 대해, 타입 시스템은 그 타입의 cv-한정 버전 세 가지(const, volatile, const volatile)를 추가로 지원해요.

기타 분류

객체 타입(object type, std::is_object 참고)은 (cv-한정일 수 있는) 타입으로, 함수 타입도 참조 타입도 (cv-한정일 수 있는) void도 아닌 타입이에요.

다음 타입들을 통틀어 스칼라 타입(scalar types, std::is_scalar 참고)이라 해요.

  • 산술 타입
  • 열거형 타입
  • 포인터 타입
  • 멤버 포인터 타입
  • std::nullptr_t (since C++11)
  • std::meta::info (since C++26)
  • 이 타입들의 cv-한정 버전

다음 타입들을 통틀어 암시적 수명 타입(implicit-lifetime types)이라 해요.

  • 스칼라 타입
  • 암시적 수명 클래스 타입
  • 배열 타입
  • 이 타입들의 cv-한정 버전
다음 타입들을 통틀어 자명 복사 가능 타입(trivially copyable types)이라 해요.
스칼라 타입, 자명 복사 가능 클래스 타입, 그런 타입의 배열, 이 타입들의 cv-한정 버전
다음 타입들을 통틀어 표준 레이아웃 타입(standard-layout types)이라 해요. (since C++11)
스칼라 타입, 표준 레이아웃 클래스 타입, 그런 타입의 배열, 이 타입들의 cv-한정 버전
다음 타입들을 통틀어 POD 타입(std::is_pod 참고)이라 해요.
스칼라 타입, POD 클래스, 그런 타입의 배열, 이 타입들의 cv-한정 버전
다음 타입들을 통틀어 자명 타입(trivial types, std::is_trivial 참고)이라 해요.
스칼라 타입, 자명 클래스 타입, 그런 타입의 배열, 이 타입들의 cv-한정 버전

프로그램 정의 타입

프로그램 정의 특수화(program-defined specialization)는 C++ 표준 라이브러리의 일부가 아니고 구현이 정의하지도 않은 명시적 특수화 또는 부분 특수화예요.

프로그램 정의 타입(program-defined type)은 다음 중 하나예요.

  • C++ 표준 라이브러리의 일부가 아니고 구현이 정의하지도 않은, non-closure(since C++11) 클래스 타입 또는 열거형 타입
  • 구현이 제공하지 않은 람다 표현식의 클로저 타입 (since C++11)
  • 프로그램 정의 특수화의 인스턴스화

타입 이름 붙이기

이름은 다음 방법으로 타입을 가리키도록 선언될 수 있어요.

  • 클래스 선언
  • 공용체 선언
  • enum 선언
  • typedef 선언
  • 타입 별명 선언

이름이 없는 타입은 C++ 프로그램에서 자주 가리켜야 할 필요가 있는데, 그에 쓰이는 문법을 type-id라고 해요. 타입 T를 가리키는 type-id의 문법은 타입 T의 변수나 함수 선언의 문법에서 식별자를 뺀 것과 정확히 같아요. 단, 선언 문법의 decl-specifier-seqtype-specifier-seq으로 제한되고, type-id가 비템플릿 타입 별명 선언의 오른쪽에 나타날 때만 새 타입을 정의할 수 있어요.

int* p;               // declaration of a pointer to int
static_cast<int*>(p); // type-id is "int*"

int a[3];   // declaration of an array of 3 int
new int[3]; // type-id is "int[3]" (called new-type-id)

int (*(*x[2])())[3];      // declaration of an array of 2 pointers to functions
                          // returning pointer to array of 3 int
new (int (*(*[2])())[3]); // type-id is "int (*(*[2])())[3]"

void f(int);                    // declaration of a function taking int and returning void
std::function<void(int)> x = f; // type template parameter is a type-id "void(int)"
std::function<auto(int) -> void> y = f; // same

std::vector<int> v;       // declaration of a vector of int
sizeof(std::vector<int>); // type-id is "std::vector<int>"

struct { int x; } b;         // creates a new type and declares an object b of that type
sizeof(struct { int x; });   // error: cannot define new types in a sizeof expression
using t = struct { int x; }; // creates a new type and declares t as an alias of that type

sizeof(static int); // error: storage class specifiers not part of type-specifier-seq
std::function<inline void(int)> f; // error: neither are function specifiers

선언 문법에서 이름을 뺀 declarator 부분을 **추상 선언자(abstract-declarator)**라고 불러요.

type-id는 다음 상황에서 쓸 수 있어요.

  • 캐스트 표현식에서 대상 타입을 지정할 때
  • sizeof, alignof, alignas, new, typeid의 인자로
  • 타입 별명 선언의 오른쪽에
  • 함수 선언의 후행 반환 타입으로
  • 템플릿 타입 매개변수의 기본 인자로
  • 템플릿 타입 매개변수의 템플릿 인자로
  • 동적 예외 명세에서 (until C++17)

type-id는 약간의 수정과 함께 다음 상황에서 쓸 수 있어요.

  • 함수의 매개변수 목록에서(매개변수 이름이 생략될 때) type-id는 type-specifier-seq 대신 decl-specifier-seq을 써요(특히 일부 저장 클래스 지정자가 허용돼요)
  • 사용자 정의 변환 함수의 이름에서는 추상 선언자에 함수·배열 연산자가 포함될 수 없어요
이 절은 불완전해요
이유: 8.2 [dcl.ambig.res]를 간결하게 요약할 수 있다면
이 절은 불완전해요
이유: decltype과 auto를 언급하고 링크하기

정교화된 타입 지정자 (Elaborated type specifier)

정교화된 타입 지정자는 이전에 선언된 클래스 이름(class, struct, union)이나 enum 이름을, 그 이름이 비타입 선언에 의해 가려졌더라도 가리키는 데 쓸 수 있어요. 새 클래스 이름을 선언하는 데도 쓸 수 있죠.

자세한 내용은 정교화된 타입 지정자 문서를 보세요.

정적 타입 (Static type)

프로그램의 컴파일 시점 분석에서 얻어지는 표현식의 타입을 그 표현식의 정적 타입이라 해요. 정적 타입은 프로그램이 실행되는 동안 변하지 않아요.

동적 타입 (Dynamic type)

어떤 glvalue 표현식이 다형적 객체를 가리킨다면, 그 가장 파생된 객체의 타입을 동적 타입이라고 해요.

// given
struct B { virtual ~B() {} }; // polymorphic type
struct D : B {};               // polymorphic type

D d; // most-derived object
B* ptr = &d;

// the static type of (*ptr) is B
// the dynamic type of (*ptr) is D

prvalue 표현식에서는 동적 타입이 항상 정적 타입과 같아요.

불완전 타입 (Incomplete type)

다음 타입들은 불완전 타입이에요.

  • (cv-한정일 수 있는) void 타입
  • 불완전하게 정의된 객체 타입:
    • (전방 선언으로) 선언됐지만 정의되지 않은 클래스 타입
    • 알 수 없는 경계의 배열
    • 불완전 타입의 요소로 이루어진 배열
    • 선언 지점부터 기반 타입이 결정될 때까지의 열거형 타입

그 외의 모든 타입은 완전 타입이에요.

다음 문맥 중 하나는 타입 T가 완전할 것을 요구해요.

  • 반환 타입 T 또는 인자 타입 T인 함수의 정의 또는 호출
  • 타입 T의 객체 정의
  • 타입 T의 비정적 클래스 데이터 멤버 선언
  • 타입 T의 객체 또는 요소 타입이 T인 배열에 대한 new 표현식
  • 타입 T의 glvalue에 적용되는 lvalue-to-rvalue 변환
  • 타입 T로의 암시적 또는 명시적 변환
  • 널 포인터 상수나 (cv-한정일 수 있는) void에 대한 포인터에서 변환되는 경우를 제외한, T* 또는 T&로의 표준 변환·dynamic_cast·static_cast
  • 타입 T의 표현식에 적용되는 클래스 멤버 접근 연산자
  • 타입 T에 적용되는 typeid, sizeof, alignof 연산자
  • T에 대한 포인터에 적용되는 산술 연산자
  • 기반 클래스 T를 가진 클래스의 정의
  • 타입 T의 lvalue에 대한 대입
  • 타입 T, T&, T*의 핸들러

(일반적으로 T의 크기와 레이아웃을 알아야 하는 상황이에요.)

이 상황 중 하나라도 번역 단위에서 발생하면 타입의 정의가 같은 번역 단위에 나타나야 해요. 그렇지 않으면 요구되지 않아요.

불완전하게 정의된 객체 타입은 완성될 수 있어요.

  • 클래스 타입(class X처럼)은 번역 단위의 한 지점에서 불완전으로 간주되고 나중에 완전으로 간주될 수 있어요. 그 두 지점에서 타입 class X는 같은 타입이에요.
struct X;            // declaration of X, no definition provided yet
extern X* xp;        // xp is a pointer to an incomplete type:
                     // the definition of X is not reachable

void foo()
{
    xp++;            // ill-formed: X is incomplete
}

struct X { int i; }; // definition of X
X x;                 // OK: the definition of X is reachable

void bar()
{
    xp = &x;         // OK: type is "pointer to X"
    xp++;            // OK: X is complete
}
  • 배열 객체의 선언된 타입은 불완전 클래스 타입의 배열일 수 있어서 그로 인해 불완전할 수 있어요. 클래스 타입이 나중에 번역 단위에서 완성되면 배열 타입도 완전해져요. 그 두 지점의 배열 타입은 같은 타입이에요.
  • 배열 객체의 선언된 타입은 알 수 없는 경계의 배열이라서 한 지점에서 불완전하고 나중에 완전해질 수 있어요. 그 두 지점의 배열 타입("T의 알 수 없는 경계의 배열"과 "N개의 T 배열")은 서로 다른 타입이에요.

알 수 없는 경계의 배열에 대한 포인터나 참조의 타입은 영구적으로 불완전 타입을 가리켜요. typedef 선언으로 이름 붙은 알 수 없는 경계의 배열도 영구적으로 불완전 타입을 가리켜요. 두 경우 모두 배열 타입은 완성될 수 없어요.

extern int arr[];   // the type of arr is incomplete
typedef int UNKA[]; // UNKA is an incomplete type

UNKA* arrp;         // arrp is a pointer to an incomplete type
UNKA** arrpp;

void foo()
{
    arrp++;         // error: UNKA is an incomplete type
    arrpp++;        // OK: sizeof UNKA* is known
}

int arr[10];        // now the type of arr is complete

void bar()
{
    arrp = &arr;    // OK: qualification conversion (since C++20)
    arrp++;         // error: UNKA cannot be completed
}

더 알아보기

  • 타입 특성(type traits)은 타입의 속성을 질의하는 컴파일 시점 템플릿 기반 인터페이스예요. type_traits 라이브러리에서 확인할 수 있어요.
  • 기본 타입의 자세한 분류와 크기는 기본 타입 문서에서 볼 수 있어요.
  • cppreference의 타입 원문에서 결함 보고(defect report) 기록을 더 볼 수 있어요.