reference type, binding, related type, compatible type
본 게시글을 이해하는데 알고 있어야 할 정보
본 게시글은 reference 관련 용어를 설명한다. 편이상 원활한 진행을 위해, 이전 게시글에서 설명한 내용 일부를 다시 한번 언급한다.
conversion 구현방법
conversion의 첫번째 구현방법은 변환하려는 대상을, 그대로 두고 관점만 바꾸는 변환이다.
conversion의 두번째 구현방법은 새로 만든 목적 타입을, 소스 타입 정보로 덥혀쓰기하는 변환이 있다. 첫번째 방법으로 불가능한 모든 변환을 두번째 방법으로 구현 가능하다. 목적 타입에 소스 타입 정보를 덥혀쓰기하는 방법은 copy-initialization 방법과 direct-initialization 방법으로 구분된다.
conversion의 세번째 구현방법은 메모리가 아닌 위치로 복사 변환한다. glvalue-to-prvalue 변환이나 prvalue간 변환 과정에서 사용한다.
cv-qualification 표기법
int const* volatile* const m1{ nullptr };
m1
의 타입을 cv-qualification 순서 집합과 pointer 순서 집합으로 구분할 수 있다.
cv-qualification 순서 집합
: ({const}, {volatile}, {const})pointer 순서 집합
: (int
,int*
,int**
)
코딩 순서를 따라 *
를 만나면, 체크하는 *
바로 이전 cv-qualification를 cv-qualification 순서 집합에 추가하고, 체크하는 *
이전에 cv-qualification를 제거한 타입 이름을 pointer 순서 집합에 추가한다. 배열 인덱서 []
를 만나거나 declarator를 만나면 곧바로 뒤에 가상의 *
가 있는 것처럼 순서 집합에 추가한다. 동일한 개념을 멤버 포인터에 대해서도 동일하게 적용할 수 있다.
따라서 pointer 순서 집합은 *
또는 []
가 순차적으로 증가하고, []
의 대응하는 cv-qualification는 공집합이다.
cv-qualification 순서 집합의 마지막 cv-qualification를 해당 타입의 top-level cv-qualification
라 한다. top-level cv-qualification
를 제외한 나머지 cv-qualification 순서 집합을 해당 타입의 cv-qualification signature
라 한다.
역순으로 배치하면 두 집합을 해당 타입의 qualification-decomposition
라 한다.
reference 타입의 reference 심블을 제거하고, reference 타입의 두 순서 집합을 구성한다. 즉 reference 심블이 두 순서 집합 구성에 영향을 주지 않는다.
pointer과 reference
pointer type를 중첩하면 두 순서 집합의 요소 갯수가 하나씩 증가하지만, reference는 중첩하면 참조 축약 규칙에 따라, 하나의 reference 심블로 축약되고, 두 순서 집합에는 변화가 없다.
참조 축약은 같은 reference 유형을 중첩하면 하나의 같은 reference 유형으로 축약하고, 서로 다른 reference 유형은 lvalue reference로 축약한다.
similar-type(T1, T2) : bool
similar-type(T1, T2)
연산 결과는 bool 타입을 반환한다.
pointer 순서 집합이 같은 T1과 T2에 대한 similar-type(T1, T2)
결과는 true다.
대응하는 bounded array과 unknown bounded array의 요소 타입이 같으면, 같은 타입으로 판단한다. cv-qualification 순서 집합
은 결과에 영향을 주지 않는다. 따라서 similar-type(T1, T2)
와 similar-type(cv1 T1, cv2 T2)
는 결과와 같다.
대응하는 pointer 순서 집합
이 상등인지 여부로 연산 결과를 결정하기 때문에, similar-type(T1, T2)
의 두 아규먼트를 교환하더라도 연산 결과는 변함이 없다. 즉 교환 법칙이 성립한다.
base-class (T1, T2) : bool
T1가 T2의 accessible base class이면 base-class (T1, T2)
결과는 true다. T1가 T2의 accessible base class인지 여부가 연산 결과이기 때문에, base-class (T1, T2)
의 두 아규먼트를 교환하면 다른 결과가 나온다. 즉 교환 법칙이 성립하지 않는다.
reference binding 관련 용어
아래에 언급하는 내용은 모두 reference binding과 관련된다. reference binding는 binding되는 reference 유형에 따라 lvalue reference binding과 rvalue reference binding으로 구분한다.
reference-related(cv1 T1, cv2 T2) : bool
similar-type(T1, T2)
연산이 true이거나 base-class(T1, T2)
연산이 true이면 reference-related(cv1 T1, cv2 T2)
연산은 true다.
교환 법칙이 성립하지 않는 base-class
연산을 포함하기 때문에, reference-related
연산은 교환 법칙이 성립하지 않는다.
similar-type
연산나 base-class
연산 모두에서 cv-qualification 순서 집합
을 무시하고, reference-related
연산에서도 cv1
, cv2
에 제약 조건이 없음으로, reference-related
연산 역시, cv-qualification 순서 집합
을 무시한다. reference-related(cv1 T1, cv2 T2)
결과와 reference-related(T1, T2)
결과는 같다.
similar-type(T1, T2)
연산이 true이면 T1과 T2는 같은 타입이기 때문에 T2 객체를 T1 객체로 바라 볼 수 있다.base-class(T1, T2)
연산이 true이면, T2 객체의 subobject를 T1 객체로 바라 볼 수 있다.reference-related(cv1 T1, cv2 T2)
연산이 true이면, conversion 첫번째 구현방법으로, T2 객체를 곧바로 T1 객체로 reference binding를 구현하거나, conversion 두번째 구현방법으로, T2 객체의 T1 객체 정보를 복사한 후 reference binding을 구현할 수 있다.
similar-type(T1, T2)
연산이 true이면, 본질적으로 같은 타입간 변환을 의미한다.
similar-type(T1, T2):true
인 예제 코딩
double d = 2.0;
const double& rcd = d;
[코딩 분석]
rcd:cv1 T1 두 순서 집합
- cv-qualification 순서 집합: ({const})
- pointer 순서 집합: (double)
d:cv2 T2 두 순서 집합
- cv-qualification 순서 집합: ({})
- pointer 순서 집합: (double)
reference-related(T1, T2):true ∵ similar-type(T1, T2):true
cv1가 cv2보다 more cv-qualification하다.
base-class(A, B):true
인 예제 코딩
struct A { };
struct B : A { };
B b{};
A& ra = b;
[코딩 분석]
ra:cv1 T1 두 순서 집합
- cv-qualification 순서 집합: ({})
- pointer 순서 집합: (A)
b:cv2 T2 두 순서 집합
- cv-qualification 순서 집합: ({})
- pointer 순서 집합: (B)
reference-related(T1, T2): true ∵ base-class(A, B):true
cv1가 cv2와 same cv-qualification다.
reference-compatible(cv1 T1, cv2 T2) : bool
standard conversion sequence로 cv2 T2
의 pointer prvalue category에서 cv1 T1
의 pointer로 변환할 수 있다면 reference-compatible(cv1 T1, cv2 T2)
연산은 true다.
standard conversion sequence는 역변환을 허용하지 않기 때문에 reference-compatible(cv1 T1, cv2 T2)
연산은 교환 법칙이 성립할 수 없다.
reference-compatible(cv1 T1, cv2 T2)
연산이 true이면, standard conversion sequence로 cv2 T2
의 pointer prvalue category에서 cv1 T1
의 pointer로 변환 가능하다. 결국 포인터 레벨 변환, 포인터 레벨 변환에 대한 dereference opeartor 적용 버전을 등치로 정의함으로써, 유효한 reference binding를 정의한다.
cv2 T2* → cv1 T1* ≡ *(cv2 T2*) → *(cv1 T1*)
qualification conversion
, pointer conversion
, pointer-to-member conversion
, function pointer conversion
이 pointer prvalue category에서 다른 pointer 타입으로의 standard conversion를 정의하고 있다.
이들 표준 변환은 서로 다른 타입의 pointer간 변환을 정의한다.
reference-compatible(cv1 T1, cv2 T2)
연산이 true이면, 서로 다른 두 타입간 변환을 의미한다.
reference-related(cv1 T1, cv2 T2) 연산과 reference-compatible(cv1 T1, cv2 T2) 연산 차이점
reference-related(cv1 T1, cv2 T2)
는 본질적으로 같은 타입간 reference binding를 정의하고, reference-compatible(cv1 T1, cv2 T2)
는 서로 다른 타입간 reference binding를 정의한다. 따라서 두 연산이 동시에 true가 될 수 없다.
reference binding 연산 과정
다음 순서를 따라, cv2 T2
expression에서 cv1 T1
로 reference binding한다. reference binding을 완료하면 더 이상 분석하지 않는다.
cv1 T1
expression는 lvalue reference이고,cv2 T2
expression이 lvalue category이고reference-compatible(cv1 T1, cv2 T2)
연산이 true이거나,cv2 T2
의 initializer expression이 class type이고reference-related(cv1 T1, cv2 T2)
연산이 false이고,cv2 T2
expression를cv3 T3
의 lvalue category로 변환할 수 있고,reference-compatible(cv1 T1, cv3 T3)
연산이 true이면,
cv2 T2
의 initializer expression에서cv1 T1
로 lvalue reference binding한다.#include <print> int sum(int a, int b) noexcept { return a + b; } int main() { //function pointer conversion: reference-compatible(&sum, p):true int(*p)(int, int) = ∑ // reference binding int(&fn)(int, int) = sum; std::print("{}", fn(10, 20)); return 0; } [출력 결과] 30
위 코딩과 동일한 의미를 갖고 있는 initializer expression이 class type인 버전.
struct A { using FN = int(int, int) noexcept; operator FN&(); }; int main() { //function pointer conversion: reference-compatible(a, p):true A a{}; int(*p)(int, int) = a; // reference binding int(&fn)(int, int) = a; std::print("{}", fn(10, 20)); return 0; } int sum(int a, int b) noexcept { return a + b; } A::operator FN& () { return sum; } [출력 결과] 30
cv1 T1
expression는 lvalue reference이고,cv2 T2
expression이 rvalue category이거나,reference-compatible(cv1 T1, cv2 T2)
연산이 false이고,cv1 T1
의 cv-qualification가 {Ø, volatile, const volatile} 중 하나이면, 컴파일 오류다.cv1 T1
의 cv-qualification가 {const}이면, lvalue reference binding한다.
double& rd2 = 2.0; // ERROR ∵ initializer expression: rvalue category, cv-qualification:Ø int i = 2; double const volatile& rd3 = i; // ERROR ∵ reference-compatible(double, int):false, cv-qualification:const volatile double const& rd4 = i; // OK ∵ reference-compatible(double, int):false, cv-qualification:const double const& rd5 = 2.0; // OK ∵ reference-compatible(double, double):false, cv-qualification:const
cv1 T1
expression는 rvalue reference이고,cv2 T2
의 initializer expression이 rvalue category나 function lvalue이고,reference-compatible(cv1 T1, cv2 T2)
연산이 true이거나,cv2 T2
의 initializer expression이 class type이고reference-related(cv1 T1, cv2 T2)
연산이 false이고,cv2 T2
의 initializer expression이cv3 T3
의 rvalue category나 function lvalue로 변환할 수 있고,reference-compatible(cv1 T1, cv3 T3)
연산이 true이면
cv2 T2
의 initializer expression에서cv1 T1
로 rvalue reference binding한다.
참고로 function 이름 자체는 대표적인 lvalue category임에도 불구하고, 1 단계에서 lvalue reference binding할 수 있고, 3 단계에서 rvalue reference binding할 수 있다.cv1 T1
의 rvalue reference를 초기화하기 위해 사용된 initializer가T4
의 prvalue category이면,cv1 T4
타입으로 조정한 후,temporary materialization conversion
를 적용한다. 이렇게 만들어진 결과 xvalue category에서cv1 T1
로 rvalue reference binding한다.#include <print> int sum(int a, int b) noexcept { return a + b; } int main() { int(&&f)(int, int) = sum; // initializer: function lvalue cagtegory int a{ 10 }, && ri1 = 10, // initializer: prvalue category && ri2 = std::move(a); // initializer: xvalue category std::print("{}", f(10, 20)); return 0; } [출력 결과] 30
위 코딩과 동일한 의미를 갖고 initializer expression이 class type인 버전.
#include <print> struct A { using FN = int(int, int) noexcept; int value{10}; operator FN& (); operator int() &; operator int&& () &&; }; int main() { A a{}; int(&& f)(int, int) = a; // initializer: function lvalue cagtegory int && ri1 = a, // initializer: prvalue category && ri2 = std::move(a); // initializer: xvalue category std::print("{}", f(10, 20)); return 0; } int sum(int a, int b) noexcept { return a + b; } A::operator FN& () { return sum; } A::operator int() & { std::println("A::operator int() &"); return value; } A::operator int&& () && { std::println("A::operator int&& () &&"); return std::move(value); } [출력 결과] A::operator int() & A::operator int&& () && 30
4-1. 3 단계의 첫 항목에서 reference-compatible(cv1 T1, cv2 T2)
이 false이면
cv2 T2
expression를T1
의 prvalue category로 암묵적 변환하고,cv1 T1
타입으로 조정한 후,temporary materialization conversion
를 적용한다. 이렇게 만들어진 만들어진 결과의 xvalue category에서cv1 T1
로 rvalue reference binding한다.int main() { double&& rrd = 2; // rrd refers to temporary with value 2.0 return 0; } [코딩 분석] double&& rrd = 2; rrd: cv1 T1, 2:cv2 T2 reference-compatible(T1, T2): false ∵ int* -> double* :Error T1과 T2 모두 non-class type이다. 암묵적 변환: 2 -> 2.0 temporary materialization conversion 결과: double temp(2.0); double&& rrd = std::move(temp);
4-2. 3 단계의 두번째 항목에서 reference-compatible(cv1 T1, cv3 T3)
연산이 false이면,
T1
또는T2
가 class type이면,cv1 T1
타입의 object를copy-initialization
하기 위해,user-defined conversion
를 고려한다.user-defined conversion
호출 결과 타입(T3
)으로부터direct-initialization
함으로써 rvalue reference binding한다. rvalue reference로direct-initialization
할 때는user-defined conversion
를 호출할 수 없다.class X { private: int v_{ }; public: X(int v) :v_{ v } {} operator int& () { return v_; } }; int main() { X x{ 10 }; double&& rrd = x; return 0; } [코딩 분석] double&& rrd = x; rrd:cv1 T1, x:cv2 T2 T1 또는 T2가 class type이다. reference-related(cv1 T1, cv2 T2): false ∵ similar-type(T1, T2):false and base-classed(T1, T2):false copy-initialization: double temp = x; // x.operator int& () reference-compatible(double, int): false ∵ int* -> double*:Error direct-initialization: double&& rrd{std::move(temp)};
이후 연산 과정은
reference-related(cv1 T1, cv2 T2)
연산이 true인 연산이다. 즉 같은 타입간 변환을 의미한다.
reference-related(cv1 T1, cv2 T2)
연산이 true이면,- cv1가 cv2보다 same or more cv-qualification이고,
cv1 T1
expression이 rvalue reference일 때,cv2 T2
expression이 lvalue category가 아니면,
cv2 T2
expression에서cv1 T1
로 reference binding한다.int main() { double m1{2.0}; double&& r1 = m1; // ERROR double&& r2 = std::move(m1); // OK double&& r3 = 2.0; // OK return 0; } [코딩 분석] double&& r1 = m1; r1:cv1 T1, m1:cv2 T2 reference-related(T1, T2): true ∵ similar-type(T1, T2):true cv1과 cv2는 같다. m1: lvalue category -> ERROR