암시적 변환

암시적 변환 (Implicit conversions)

어떤 표현식이 그 타입 T1을 받아주지 않는 문맥에서 쓰였을 때, 그 문맥이 받아주는 다른 타입 T2로 값이 자동으로 바뀌는 경우가 있어요. 이걸 우리는 암시적 변환(implicit conversion) 이라고 불러요. 크게 이럴 때 일어납니다.

  • T2를 매개변수 타입으로 선언한 함수를 호출하면서 그 표현식을 인자로 넘길 때
  • T2를 기대하는 연산자의 피연산자로 표현식을 쓸 때
  • T2 타입의 새 객체를 초기화할 때 (그 타입을 반환하는 함수의 return 문도 포함해요)
  • switch 문에서 쓸 때 (T2는 정수 계열 타입)
  • if 문이나 반복문에서 쓸 때 (T2bool)

출처: cppreference

T1에서 T2로 가는 명확한 암시적 변환 시퀀스(implicit conversion sequence) 가 하나라도 존재해야 프로그램이 컴파일돼요. 호출 대상 함수나 연산자에 오버로드가 여러 개라면, 각각의 T2로 가는 변환 시퀀스를 만든 뒤 오버로드 결정(overload resolution) 규칙이 어떤 오버로드를 컴파일할지 골라요.

참고로 이항 연산자의 피연산자에 적용되는 암시적 변환의 목적지 타입은 별도 규칙인 통상적 산술 변환(usual arithmetic conversions) 으로 정해져요.

본문

변환의 순서

암시적 변환 시퀀스는 다음 순서로 구성돼요.

  1. 표준 변환 시퀀스 0개 또는 1개
  2. 사용자 정의 변환 0개 또는 1개
  3. 표준 변환 시퀀스 0개 또는 1개 (사용자 정의 변환을 사용한 경우에만)

사용자 정의 변환 시퀀스란 사용자 정의 변환을 포함하는 암시적 변환 시퀀스예요. 이 시퀀스의 변환 대상이 다른 사용자 정의 변환의 변환 생성자의 매개변수나 변환 함수의 암시적 객체 매개변수라면 사용이 금지돼요 (암시적 변환 시퀀스의 무한 재귀를 막기 위해서예요). 비클래스 타입에서 비클래스 타입으로 변환할 때는 항상 표준 변환 시퀀스만으로 구성돼요.

표준 변환 시퀀스는 다음 순서로 구성돼요.

  1. 다음 집합에서 0개 또는 1개: lvalue-to-rvalue 변환, 배열-포인터 변환, 함수-포인터 변환
  2. 숫자 승격(promotion) 또는 숫자 변환 0개 또는 1개
  3. 함수 포인터 변환 0개 또는 1개 (C++17부터)
  4. 자격 변환(qualification conversion) 0개 또는 1개

사용자 정의 변환은 비명시적 단일 인자 변환 생성자 호출 또는 비명시적 변환 함수 호출 0개 또는 1개로 이뤄져요.

어떤 표현식 eT2암시적으로 변환될 수 있다는 것은, T2e로부터 복사 초기화(copy-initialize)될 수 있다는 뜻이에요. 즉 어떤 임시 변수 t에 대해 T2 t = e;라는 선언이 컴파일될 수 있을 때를 말해요. 참고로 이건 직접 초기화 T2 t(e)와는 달라요. 직접 초기화에서는 명시적 생성자와 변환 함수도 추가로 고려되거든요.

문맥 변환 (Contextual conversions)

다음 문맥에서는 bool 타입이 기대되는데, bool t(e);라는 선언이 성립하면 (즉 explicit T::operator bool() const; 같은 명시적 변환 함수까지 고려하면) 문맥 변환이 수행돼요. 이런 표현식 ebool문맥상 변환되었다(contextually converted to bool) 고 말해요.

  • if, while, for의 제어 표현식
  • 내장 논리 연산자 !, &&, ||의 피연산자
  • 조건 연산자 ?:의 첫 번째 피연산자
  • static_assert 선언의 조건
  • noexcept 지정자 안의 표현식
  • explicit 지정자 안의 표현식 (C++20부터)

다음 문맥에서는 문맥에 특화된 타입 T가 기대되는데, 클래스 타입 E의 표현식 e는 다음 조건에서만 허용돼요.

  • (C++14까지) E가 허용 가능한 타입으로 가는 단일 비명시적 사용자 정의 변환 함수를 가질 때
  • (C++14부터) 허용 가능한 타입 중 정확히 하나의 타입 T가 존재해서, E가 (cv-한정될 수 있는) T 또는 (cv-한정될 수 있는) T에 대한 참조를 반환 타입으로 갖는 비명시적 변환 함수를 갖고, 동시에 eT로 암시적으로 변환될 때

이런 표현식 e를 지정 타입 T문맥상 암시적으로 변환되었다고 말해요. 단 bool로의 문맥 변환에서는 명시적 변환 함수가 고려되지만, 이 경우에는 명시적 변환 함수가 고려되지 않아요.

  • delete 표현식의 인자 (T는 임의의 객체 포인터 타입)
  • 정수 상수 표현식 (리터럴 클래스를 쓸 때, T는 정수 또는 unscoped 열거 타입, 선택된 사용자 정의 변환 함수는 constexpr이어야 함)
  • switch 문의 제어 표현식 (T는 정수 또는 열거 타입)
#include <cassert>

template<typename T>
class zero_init
{
    T val;
public:
    zero_init() : val(static_cast<T>(0)) {}
    zero_init(T val) : val(val) {}
    operator T&() { return val; }
    operator T() const { return val; }
};

int main()
{
    zero_init<int> i;
    assert(i == 0);

    i = 7;
    assert(i == 7);

    switch (i) {}     // error until C++14 (more than one conversion function)
                      // OK since C++14 (both functions convert to the same type int)
    switch (i + 0) {} // always okay (implicit conversion)
}

값 변환 (Value transformations)

값 변환은 표현식의 값 카테고리(value category) 를 바꾸는 변환이에요. 표현식이 다른 값 카테고리를 기대하는 연산자의 피연산자로 나타날 때마다 일어나요.

  • glvalue가 피연산자로 prvalue를 요구하는 연산자의 피연산자로 나타나면, lvalue-to-rvalue, 배열-포인터, 함수-포인터 표준 변환이 적용돼 표현식을 prvalue로 바꿔요.
  • 달리 명시되지 않는 한, prvalue가 피연산자로 glvalue를 요구하는 연산자의 피연산자로 나타나면, 임시 객체 구체화 변환(temporary materialization)이 적용돼 xvalue로 바꿔요. (C++17부터)

Lvalue-to-rvalue 변환

임의의 비함수·비배열 타입 T의 lvalue(glvalue, C++11부터)는 rvalue(prvalue, C++11부터)로 암시적 변환될 수 있어요.

  • T가 클래스 타입이 아니면, prvalue의 타입은 T의 cv-비한정 버전이에요.
  • 그 외에는 prvalue의 타입이 T예요.

불완전 타입에서 lvalue-to-rvalue 변환이 요구되면 그 프로그램은 규칙 위반(ill-formed)이에요.

glvalue가 가리키는 객체를 obj라고 할게요.

  • sizeof의 피연산자 내에서 lvalue-to-rvalue 변환이 일어나면, obj에 담긴 값은 접근되지 않아요. 이 연산자는 피연산자를 평가하지 않기 때문이에요.

  • 변환 결과는 obj에 담긴 값이에요. Tobj의 타입 중 하나가 부호 있는 정수 타입이고 다른 하나가 그에 대응하는 부호 없는 정수 타입이면, 결과는 obj와 같은 값 표현을 갖는 T 타입의 값이에요. (C++11까지)

  • (C++11부터) 어떤 표현식 E에 lvalue-to-rvalue 변환이 적용될 때, 다음 경우에는 obj에 담긴 값이 접근되지 않아요.

    • E가 잠재적으로 평가되지 않거나, 또는
    • E의 평가가 E의 잠재 결과 집합의 어떤 멤버 Ex의 평가로 이어지고, ExEx에 의해 odr-사용되지 않는 변수 x를 가리킬 때

    변환 결과는 이렇게 정해져요.

    • T가 (cv-한정될 수 있는) std::nullptr_t이면 결과는 null 포인터 상수예요. obj는 변환에 의해 접근되지 않으므로 Tvolatile-한정돼도 부수 효과가 없고, glvalue가 union의 비활성 멤버를 가리킬 수 있어요.
    • T가 클래스 타입이면, (C++17까지) 변환이 glvalue에서 T 타입의 임시 객체를 복사 초기화하고 변환 결과는 그 임시 객체에 대한 prvalue예요. (C++17부터) 변환이 glvalue에서 결과 객체를 복사 초기화해요.
    • obj가 유효하지 않은 포인터 값을 담고 있으면, 동작은 구현 정의예요.
    • obj의 값 표현의 비트가 obj의 타입에 유효하지 않으면, 동작은 미정의예요.
    • 그 외에는 obj가 읽히고 결과는 obj에 담긴 값이에요. 앞서 말한 부호 정수/부호 없는 정수 조합이면 그 값 표현을 유지한 T 타입 값이 결과예요.

참고로 수정되거나 읽힐 수 있는 객체는 스칼라 타입뿐이에요. 클래스 타입 객체에서는 항상 함수 호출로 나타나요 (그러나 그 연산 자체는 수정이나 읽기가 아니에요). 이 변환은 메모리 위치에서 CPU 레지스터로 값을 읽어오는 동작을 모델링해요.

배열-포인터 변환 (Array-to-pointer conversion)

타입 "array of N T" 또는 "array of unknown bound of T"의 lvalue 또는 rvalue는 타입 "pointer to T"의 prvalue로 암시적 변환될 수 있어요. 배열이 prvalue면 임시 객체 구체화가 일어나요 (C++17부터). 결과 포인터는 배열의 첫 번째 요소를 가리켜요.

함수-포인터 변환 (Function-to-pointer conversion)

함수 타입의 lvalue는 그 함수를 가리키는 prvalue 포인터로 암시적 변환될 수 있어요. 비정적 멤버 함수에는 적용되지 않아요. 비정적 멤버 함수를 가리키는 lvalue는 존재하지 않기 때문이에요.

임시 객체 구체화 (Temporary materialization)

임의의 완전 타입 T의 prvalue는 같은 타입 T의 xvalue로 변환될 수 있어요. 이 변환은 prvalue를 결과 객체로 삼아 평가해 T 타입의 임시 객체를 초기화하고, 그 임시 객체를 나타내는 xvalue를 만들어요.

T가 클래스 타입 또는 클래스 타입의 배열이면 접근 가능하고 삭제되지 않은 소멸자를 가져야 해요.

struct S { int m; };
int i = S().m; // member access expects glvalue as of C++17;
               // S() prvalue is converted to xvalue

임시 객체 구체화는 다음 상황에서 일어나요.

  • prvalue에 참조를 바인딩할 때
  • 클래스 prvalue의 비정적 데이터 멤버에 접근할 때
  • 클래스 prvalue의 암시적 객체 멤버 함수를 호출할 때
  • 배열 prvalue에 배열-포인터 변환을 수행하거나 첨자 연산을 할 때
  • 중괄호로 감싼 초기화 목록에서 std::initializer_list<T> 타입 객체를 초기화할 때
  • prvalue가 sizeoftypeid의 비평가 피연산자로 나타날 때
  • prvalue가 버려진 값 표현식(discarded-value expression)으로 나타날 때

참고로 같은 타입의 prvalue로부터 객체를 초기화할 때(직접 초기화든 복사 초기화든)는 임시 객체 구체화가 일어나지 않아요. 그런 객체는 이니셜라이저에서 직접 초기화되거든요. 이게 바로 "보장된 복사 생략(guaranteed copy elision)"을 보장하는 장치예요. (C++17부터)

정수 승격 (Integral promotion)

char 같은 작은 정수 타입과 unscoped 열거 타입의 prvalue는 int 같은 더 큰 정수 타입의 prvalue로 변환될 수 있어요. 특히 산술 연산자는 int보다 작은 타입을 인자로 받지 않으므로, 해당되면 lvalue-to-rvalue 변환 뒤에 정수 승격이 자동 적용돼요. 이 변환은 항상 값을 보존해요.

이 절의 다음 암시적 변환들이 정수 승격으로 분류돼요. 참고로 주어진 소스 타입에 대해 정수 승격의 목적지 타입은 유일하며, 그 외 모든 변환은 승격이 아니에요. 예를 들어 오버로드 결정은 char -> int(승격)를 char -> short(변환)보다 선호해요.

정수 타입에서의 승격

bool 타입의 prvalue는 int 타입의 prvalue로 변환될 수 있는데, false는 0이 되고 true는 1이 돼요.

bool을 제외한 정수 타입 T의 prvalue val에 대해:

  1. val이 비트 필드에 적용된 lvalue-to-rvalue 변환의 결과라면,

    • int가 그 비트 필드의 모든 값을 표현할 수 있으면 valint 타입의 prvalue로 변환될 수 있어요.
    • 그렇지 않고 unsigned int가 모든 값을 표현할 수 있으면 unsigned int로 변환될 수 있어요.
    • 그렇지 않으면 (3)번 규칙에 따라 변환될 수 있어요.
  2. 그 외에는 (비트 필드에서 변환된 값이 아니라면),

    • Tchar8_t(C++20부터), char16_t, char32_t, wchar_t 중 하나라면 (3)번 규칙에 따라 변환될 수 있어요.
    • 그렇지 않고 T의 정수 변환 순위가 int의 순위보다 낮으면, intT의 모든 값을 표현할 수 있으면 int prvalue로, 아니면 unsigned int prvalue로 변환될 수 있어요.
  3. (1)(비트 필드가 unsigned int에 안 들어갈 때) 또는 (2)(T가 주어진 문자 타입일 때)에서 지정한 경우, val은 자신의 기반 타입의 모든 값을 표현할 수 있는 다음 타입 중 첫 번째 타입의 prvalue로 변환될 수 있어요.

    int
    unsigned int
    long
    unsigned long
    long long
    unsigned long long
    T의 기반 타입
    

    (C++11부터)

열거 타입에서의 승격

기반 타입이 고정되지 않은 unscoped 열거 타입의 prvalue는 전체 값 범위를 담을 수 있는 다음 목록 중 첫 번째 타입의 prvalue로 변환될 수 있어요. (C++11부터)

int
unsigned int
long
unsigned long
long long
unsigned long long
확장 정수 타입 (단, 그 정수 변환 순위가 long long의 순위보다 크고,
모든 확장 정수 타입 중 정수 변환 순위가 가장 낮으며, 가장 낮은 정수
변환 순위를 가진 타입이 둘이면 부호 있는 타입)

기반 타입이 고정된 unscoped 열거 타입의 prvalue는 그 기반 타입으로 변환될 수 있어요. 게다가 기반 타입도 정수 승격의 대상이라면 승격된 기반 타입으로도 변환될 수 있어요. 오버로드 결정 목적에서는 승격되지 않은 기반 타입으로의 변환이 더 좋아요. (C++11부터)

부동소수점 승격 (Floating-point promotion)

float 타입의 prvalue는 double 타입의 prvalue로 변환될 수 있어요. 값은 변하지 않아요. 이 변환을 부동소수점 승격이라고 불러요.

숫자 변환 (Numeric conversions)

승격과 달리 숫자 변환은 값을 바꿀 수 있고 정밀도가 손실될 수 있어요.

정수 변환 (Integral conversions)

정수 타입 또는 unscoped 열거 타입의 prvalue는 임의의 다른 정수 타입으로 변환될 수 있어요. 정수 승격 목록에 있는 변환이면 승격이지 변환이 아니에요.

  • 목적지 타입이 부호 없으면, 결과 값은 소스 값을 2ⁿ으로 나눈 나머지와 같은 가장 작은 부호 없는 값이에요 (n은 목적지 타입을 표현하는 비트 수). 즉 목적지 타입이 더 넓거나 좁은지에 따라, 부호 있는 정수는 부호 확장[1]되거나 잘리고 부호 없는 정수는 0 확장되거나 잘려요.
  • 목적지 타입이 부호 있으면, 소스 정수를 목적지 타입이 표현할 수 있으면 값이 변하지 않아요. 그렇지 않으면 (C++20까지) 결과는 구현 정의이고, (C++20부터) 목적지 타입의 고유한 값으로 소스 값을 2ⁿ으로 나눈 나머지와 같은 값이에요 (n은 목적지 타입을 표현하는 비트 수). 참고로 이건 미정의 동작인 부호 있는 정수 산술 오버플로와는 달라요.
  • 소스 타입이 bool이면 false는 0으로, true는 목적지 타입의 1로 변환돼요 (목적지가 int면 이건 정수 변환이 아니라 정수 승격이에요).
  • 목적지 타입이 bool이면 이건 불리언 변환이에요 (아래 참조).

[1] 이 내용은 산술이 2의 보수(two's complement)일 때만 적용돼요. 2의 보수는 정확한 너비 정수 타입에게만 요구되지만, 현재 C++ 컴파일러가 있는 모든 플랫폼이 2의 보수 산술을 써요.

부동소수점 변환 (Floating-point conversions)

  • (C++23까지) 부동소수점 타입의 prvalue는 임의의 다른 부동소수점 타입의 prvalue로 변환될 수 있어요.
  • (C++23부터) 부동소수점 타입의 prvalue는 변환 순위가 더 크거나 같은 임의의 다른 부동소수점 타입의 prvalue로 변환될 수 있어요. 또 표준 부동소수점 타입의 prvalue는 임의의 다른 표준 부동소수점 타입의 prvalue로 변환될 수 있어요. static_cast를 쓰면 부동소수점 타입의 prvalue를 임의의 다른 부동소수점 타입으로 명시적으로 변환할 수 있어요.

부동소수점 승격 목록에 있는 변환이면 승격이지 변환이 아니에요.

  • 소스 값을 목적지 타입이 정확히 표현할 수 있으면 값이 변하지 않아요.
  • 소스 값이 목적지 타입의 두 표현 가능한 값 사이에 있으면 결과는 그 둘 중 하나예요 (어느 쪽인지는 구현 정의인데, IEEE 산술을 지원하면 반올림은 기본적으로 가장 가까운 값이에요).
  • 그 외에는 동작이 미정의예요.

부동소수점–정수 변환 (Floating-integral conversions)

  • 부동소수점 타입의 prvalue는 임의의 정수 타입의 prvalue로 변환될 수 있어요. 소수부는 잘려나가요 (버려져요).
  • 잘려진 값이 목적지 타입에 들어가지 못하면 동작은 미정의예요 (목적지 타입이 부호 없어도 모듈로 산술이 적용되지 않아요).
  • 목적지 타입이 bool이면 이건 불리언 변환이에요 (아래 참조).
  • 정수 또는 unscoped 열거 타입의 prvalue는 임의의 부동소수점 타입의 prvalue로 변환될 수 있어요. 가능하면 결과는 정확해요.
  • 값이 목적지 타입에 들어갈 수는 있지만 정확히 표현할 수 없으면, 가장 가까운 큰 값과 작은 값 중 어느 쪽이 선택될지는 구현 정의예요 (IEEE 산술을 지원하면 반올림은 기본적으로 가장 가까운 값이에요).
  • 값이 목적지 타입에 들어가지 못하면 동작은 미정의예요.
  • 소스 타입이 bool이면 false는 0으로, true는 1로 변환돼요.

포인터 변환 (Pointer conversions)

  • null 포인터 상수는 임의의 포인터 타입으로 변환될 수 있고, 결과는 그 타입의 null 포인터 값이에요. 이런 변환(널 포인터 변환)은 단일 변환으로 cv-한정 타입까지 변환하는 것이 허용돼요. 즉 숫자 변환과 자격 변환의 조합으로 간주되지 않아요.
  • 임의의 (선택적으로 cv-한정된) 객체 타입 T를 가리키는 prvalue 포인터는 (동일하게 cv-한정된) void를 가리키는 prvalue 포인터로 변환될 수 있어요. 결과 포인터는 원래 포인터 값과 같은 메모리 위치를 나타내요.
  • 원래 포인터가 null 포인터 값이면 결과도 목적지 타입의 null 포인터 값이에요.
  • 타입 "pointer to (possibly cv-qualified) Derived"의 prvalue 포인터 ptr은 타입 "pointer to (possibly cv-qualified) Base"의 prvalue로 변환될 수 있어요. 여기서 BaseDerived의 기반 클래스이고 Derived는 완전 클래스 타입이에요. Base가 접근 불가능하거나 모호하면 프로그램은 규칙 위반이에요.
    • ptr이 null 포인터 값이면 결과도 null 포인터 값이에요.
    • 그 외에, BaseDerived의 가상 기반 클래스인데 ptrDerived와 비슷한 타입의 객체로, 그 수명 안이나 생성·파괴 기간 안에 있지 않은 객체를 가리키면 동작은 미정의예요.
    • 그 외에는 결과는 파생 클래스 객체의 기반 클래스 하위 객체를 가리키는 포인터예요.

멤버 포인터 변환 (Pointer-to-member conversions)

  • null 포인터 상수는 임의의 멤버 포인터 타입으로 변환될 수 있고, 결과는 그 타입의 null 멤버 포인터 값이에요. 이런 변환(널 멤버 포인터 변환)은 단일 변환으로 cv-한정 타입까지 변환하는 것이 허용돼요.
  • 타입 "pointer to member of Base of type (possibly cv-qualified) T"의 prvalue는 타입 "pointer to member of Derived of type (identically cv-qualified) T"의 prvalue로 변환될 수 있어요. 여기서 BaseDerived의 기반 클래스이고 Derived는 완전 클래스 타입이에요. Base가 접근 불가능하거나, 모호하거나, Derived의 가상 기반 클래스이거나, Derived의 어떤 중간 가상 기반 클래스의 기반이면 프로그램은 규칙 위반이에요.
  • Derived가 원래 멤버를 포함하지 않고 원래 멤버를 포함하는 클래스의 기반 클래스도 아니면 동작은 미정의예요.
  • 그 외에는 결과 포인터를 Derived 객체로 역참조할 수 있고, 그 Derived 객체 안의 Base 기반 하위 객체 내부의 멤버에 접근해요.

불리언 변환 (Boolean conversions)

정수, 부동소수점, unscoped 열거, 포인터, 멤버 포인터 타입의 prvalue는 bool 타입의 prvalue로 변환될 수 있어요.

  • 0 값(정수, 부동소수점, unscoped 열거)과 null 포인터 값, null 멤버 포인터 값은 false가 돼요. 그 외 모든 값은 true가 돼요.
  • 직접 초기화 맥락에서 bool 객체는 std::nullptr_t 타입의 prvalue(nullptr 포함)로부터 초기화될 수 있는데 결과 값은 false예요. 단 이건 암시적 변환으로 간주되지 않아요. (C++11부터)

자격 변환 (Qualification conversions)

일반적으로 말하면 기본 규칙은 이래요.

  • cv-한정 타입 T를 가리키는 포인터 타입의 prvalue는 더 많이 cv-한정된 같은 타입 T를 가리키는 prvalue 포인터로 변환될 수 있어요 (즉 const와 volatile을 더할 수 있어요).
  • 클래스 X 안의 cv-한정 타입 T의 멤버를 가리키는 포인터 타입의 prvalue는 클래스 X 안의 더 많이 cv-한정된 타입 T의 멤버를 가리키는 prvalue 포인터로 변환될 수 있어요.

"자격 변환"의 형식적 정의는 아래와 같아요.

비슷한 타입 (Similar types)

직관적으로, 두 타입은 최상위 cv-한정을 무시했을 때 다음 중 하나면 비슷(similar) 하다고 해요.

  • 같은 타입이거나
  • 둘 다 포인터이고 가리키는 타입이 비슷하거나
  • 둘 다 같은 클래스의 멤버 포인터이고 가리키는 멤버의 타입이 비슷하거나
  • 둘 다 배열이고 배열 요소 타입이 비슷할 때

예를 들면:

  • const int* const *int**는 비슷해요.
  • int (*)(int*)int (*)(const int*)는 비슷하지 않아요.
  • const int (*)(int*)int (*)(int*)는 비슷하지 않아요.
  • int (*)(int* const)int (*)(int*)는 비슷해요 (사실 같은 타입이에요).
  • std::pair<int, int>std::pair<const int, int>는 비슷하지 않아요.

형식적으로 타입 유사성은 자격 분해(qualification-decomposition) 로 정의돼요.

타입 T의 자격 분해는 구성 요소 cv_iP_i의 시퀀스로, 음이 아닌 정수 n에 대해 T"cv_0 P_0 cv_1 P_1 ... cv_{n−1} P_{n−1} cv_n U"라고 쓸 수 있을 때를 말해요. 여기서

  • cv_iconstvolatile의 집합이고,
  • P_i"pointer to", "pointer to member of class C_i of type", "array of N_i", 또는 "array of unknown bound of" 중 하나예요.

P_i가 배열을 가리키면, 요소 타입의 cv-한정자 cv_{i+1}이 배열의 cv-한정자 cv_i로도 취급돼요.

// T is "pointer to pointer to const int", it has 3 qualification-decompositions:
// n = 0 -> cv_0 is empty, U is "pointer to pointer to const int"
// n = 1 -> cv_0 is empty, P_0 is "pointer to",
//          cv_1 is empty, U is "pointer to const int"
// n = 2 -> cv_0 is empty, P_0 is "pointer to",
//          cv_1 is empty, P_1 is "pointer to",
//          cv_2 is "const", U is "int"
using T = const int**;

// substitute any of the following type to U gives one of the decompositions:
// U = U0 -> the decomposition with n = 0: U0
// U = U1 -> the decomposition with n = 1: pointer to [U1]
// U = U2 -> the decomposition with n = 2: pointer to [pointer to [const U2]]
using U2 = int;
using U1 = const U2*;
using U0 = U1*;

두 타입 T1, T2는 각각 자격 분해가 존재하고, 두 분해가 다음 조건을 모두 만족하면 비슷하다고 해요.

  • 같은 n을 가진다.
  • U가 나타내는 타입이 같다.
  • 대응하는 P_i 구성 요소가 같거나, 모든 i에 대해 하나가 "array of N_i"이고 다른 하나가 "array of unknown bound of"다 (C++20부터).
// the qualification-decomposition with n = 2:
// pointer to [volatile pointer to [const int]]
using T1 = const int* volatile *;

// the qualification-decomposition with n = 2:
// const pointer to [pointer to [int]]
using T2 = int** const;

// For the two qualification-decompositions above
// although cv_0, cv_1 and cv_2 are all different,
// they have the same n, U, P_0 and P_1,
// therefore types T1 and T2 are similar.

cv-한정의 결합 (Combining cv-qualifications)

아래 설명에서 타입 Tn의 가장 긴 자격 분해를 Dn이라 하고, 그 구성 요소를 cvn_i, Pn_i라고 해요.

타입 T1의 prvalue 표현식은 다음 조건을 모두 만족하면 타입 T2로 변환될 수 있어요.

  • T1T2가 비슷하다.
  • 0이 아닌 모든 i에 대해, constcv1_i에 있으면 constcv2_i에 있다 (volatile도 마찬가지).
  • 0이 아닌 모든 i에 대해, cv1_icv2_i가 다르면 [1, i)의 모든 k에 대해 constcv2_k에 있다.

(C++20까지) 두 타입 T1, T2자격 결합 타입(qualification-combined type)T1과 비슷한 타입 T3로,

  • cv3_0은 비어 있고,
  • 0이 아닌 모든 i에 대해 cv3_icv1_icv2_i의 합집합이며,
  • cv3_icv1_icv2_i와 다르면 [1, i)의 모든 kconstcv3_k에 더해진다.

(C++20부터) 두 타입 T1, T2의 자격 결합 타입은 T1과 비슷한 타입 T3로, D3가 다음 조건을 모두 만족해요.

  • cv3_0이 비어 있다.
  • 0이 아닌 모든 i에 대해 cv3_icv1_icv2_i의 합집합이다.
  • P1_i 또는 P2_i"array of unknown bound of"이면 P3_i"array of unknown bound of"이고, 그렇지 않으면 P1_i다.
  • cv3_icv1_icv2_i와 다르거나, P3_iP1_iP2_i와 다르면 [1, i)의 모든 kconstcv3_k에 더해진다.

T1 타입의 prvalue는 T1T2의 자격 결합 타입이 cv-비한정 T2일 때 T2로 변환될 수 있어요. (C++20부터)

// longest qualification-decomposition of T1 (n = 2):
// pointer to [pointer to [char]]
using T1 = char**;

// longest qualification-decomposition of T2 (n = 2):
// pointer to [pointer to [const char]]
using T2 = const char**;

// Determining the cv3_i and T_i components of D3 (n = 2):
// cv3_1 = empty (union of empty cv1_1 and empty cv2_1)
// cv3_2 = "const" (union of empty cv1_2 and "const" cv2_2)
// P3_0 = "pointer to" (no array of unknown bound, use P1_0)
// P3_1 = "pointer to" (no array of unknown bound, use P1_1)
// All components except cv_2 are the same, cv3_2 is different from cv1_2,
// therefore add "const" to cv3_k for each k in [1, 2): cv3_1 becomes "const".

// T3 is "pointer to const pointer to const char", i.e., const char* const *.
using T3 = /* the qualification-combined type of T1 and T2 */;

int main()
{
    const char c = 'c';
    char* pc;
    T1 ppc = &pc;
    T2 pcc = ppc; // Error: T3 is not the same as cv-unqualified T2,
                  //        no implicit conversion.

    *pcc = &c;
    *pc = 'C';    // If the erroneous assignment above is allowed,
                  // the const object "c" may be modified.
}

참고로 C 언어에서는 const/volatile이 첫 수준에만 더할 수 있어요.

char** p = 0;
char * const* p1 = p;       // OK in C and C++
const char* const * p2 = p; // error in C, OK in C++

함수 포인터 변환 (Function pointer conversions)

  • 비-예외(non-throwing) 함수를 가리키는 포인터 타입의 prvalue는 잠재적 예외(potentially-throwing) 함수를 가리키는 prvalue 포인터로 변환될 수 있어요.
  • 비-예외 멤버 함수를 가리키는 포인터 타입의 prvalue는 잠재적 예외 멤버 함수를 가리키는 prvalue 포인터로 변환될 수 있어요.
void (*p)();
void (**pp)() noexcept = &p; // error: cannot convert to pointer to noexcept function

struct S
{
    typedef void (*p)();
    operator p();
};
void (*q)() noexcept = S(); // error: cannot convert to pointer to noexcept function

(C++17부터)

안전한 불리언 문제 (The safe bool problem)

C++11 이전에는 불리언 문맥에서 쓸 수 있어야 하는 클래스(if (obj) { ... } 같은)를 설계하는 게 어려웠어요. T::operator bool() const; 같은 사용자 정의 변환 함수가 있으면, 그 함수 호출 뒤에 표준 변환 시퀀스 하나가 더 허용됐기 때문에 결과 boolint로 변환될 수 있었어요. 그 결과 obj << 1;이나 int i = obj; 같은 코드가 컴파일됐죠.

초기 해결책 중 하나는 std::basic_ios에서 볼 수 있어요. 처음에는 operator void*를 정의해서 if (std::cin) {...} 같은 코드가 컴파일되게 했지만 int n = std::cout;은 컴파일되지 않게 했어요 (void*bool로 변환되지만 int로는 변환되지 않으니까요). 그래도 delete std::cout; 같은 말도 안 되는 코드가 컴파일되는 건 막지 못했어요.

C++11 이전의 많은 서드파티 라이브러리는 안전한 불리언 관용구(Safe Bool idiom) 라는 더 정교한 해법으로 설계됐어요. std::basic_ios도 LWG 이슈 468로 이 관용구를 허용했고, operator void*는 나중에 대체됐어요.

C++11부터는 명시적 bool 변환으로도 안전한 불리언 문제를 해결할 수 있어요.

결함 보고 (Defect reports)

다음 동작 변경 결함 보고(defect report)들은 이미 발표된 C++ 표준에 소급 적용됐어요. 이후 버전에서 이 페이지의 규정이 조정된 주요 이슈로는 lvalue-to-rvalue 변환의 미정의/구현 정의 동작 정리(CWG 616), enum 승격 규칙(CWG 172, CWG 685), 부동소수점-정수 변환의 범위 밖 미정의(CWG 707), std::nullptr_t의 bool 변환 처리(CWG 1423, CWG 1781, CWG 2140), 비트 필드 관련 정수 승격(CWG 2485), 임시 객체 구체화 적용 범위(CWG 2813, CWG 2879) 등이 있어요.

더 알아보기 (Learn more)