프로그래밍 언어/C++

built-in operator를 통해 살펴보는 value category

opencpp 2024. 2. 7. 19:59
반응형

설명에 앞서

게시글에서 설명이 다소 많아 원활한 진행을 위해 자주 등장하는 용어를 먼저 설명하겠다.

cv-qualifier 표기법 이해

const과 volatile 키워드를 합해 cv라고 축약해 부른다. cv를 조합해 만들어진 구문을 cv-qualifier라고 한다.

결국, cv는 const volatile, const, volatile, non-cv 구문을 의미한다. 공집합을 포함한 cv로 조합할 수 있는 모든 경우를 의미한다.
예를 들어, vq는 cv-qualifier가 volatile이거나 non-cv 구문을 의미한다. 설명 과정에서 non-const로 언급한다.
예를 들어, cq는 cv-qualifier가 const이거나 non-cv 구문을 의미한다. 설명 과정에서 non-volatile로 언급한다.
예를 들어, cv12는 cv1과 cv2의 합집합 cv-qualifier를 의미한다. 같은 레벨에서 cv가 은연 중에 중첩되는 경우가 있다.

예를 들어 T cv2 구문에서
T가 P cv1이면 결과 cv는 cv1 cv2이다.
T가 *를 포함하면 결과 cv는 T의 top cv-qualifier과 cv2의 합집합이다.
이 두 경우 모두를 설명 과정에서 t-cv cv2라고 언급한다.

cv1의 모든 요소가 cv2의 모든 요소를 포함하면 cv1를 more cv-qualifier라고 부르고, 역으로 cv2를 less cv-qualifier라고 부른다.

빈 cv 구문을 스펙 문서에서 cv-unqualifier로 표현하는데, 설명 과정에서는 non-cv로 언급한다.

위에서 언급했듯, T 타입의 top cv-qualifer를 설명 과정에 t-cv로 언급한다. *로 nested cv-qualifier를 형성하더라도 오직 마지막 * 이후 cv-qualifier를 t-cv로 표현하고, *가 없다면 해당 타입의 cv가 t-cv가 된다. t-cv cv1로 언급하면 최종 cv-qualifier는 t-cv, cv1의 합집합을 의미한다.

포인터 타입에 적용된 cv는 상위 레벨에서 하위 레벨로 중첩 구조를 갖는데, 마지막 * 심블 뒤에 오는 cv를 top cv-qualifier라고 하고, 본 파트에서는 대부분의 설명 내용은 top cv-qualifer에만 관심을 갖는다.

cv 산술 타입의 아규먼트에 대한 특별한 조항이 스펙에 존재한다. cv 산술 타입의 아규먼트를 prvalue category의 파라미터로 전달되면 non-cv 산술 타입이 된다. 즉 cv를 모두 제거된 산술 타입의 prvalue category를 갖는다. 산술 타입의 prvalue 파라미터는 cv-unqualified 주석 유무과 관계없이 non-cv 산술 타입이다. 반환 타입으로 사용될 때는 cv는 그대로 유지된다.

산술 타입, promoted 타입, alias 산술 타입

부동소수점 타입과 정수형 타입을 묶어 산술 타입(arithmetic type)이라고 한다. 파라미터로 prvalue catgory를 요구하는 산술 타입이 사용되면, 앞으로 진행할 연산 과정에서 overflow가 발생하지 않도록 promoted 타입의 prvalue category로 암묵적으로 변경한 후 연산을 진행한다.

그에 비해 함수 선언의 반환 타입은 연산을 마무리하고 반환되는 타입이기 때문에, 추가로 진행될 연산이 없다. 따라서 prvalue category를 갖더라도 원래 산술 타입을 그대로 유지한다.

정수형 타입의 promoted 타입은 일반적으로 int 타입이고, 부동소수점의 promoted 타입은 일반적으로 float 타입이다.
예를 들어 char, unsigned char, signed char, byte, bool, short int 타입은 int 타입으로 promoted된다.

float 타입으로 promotion이 필요한 부동소수점 타입이 아직 없다. C++23부터 BFloat, Half 타입을 지원하기 시작했고, 조만간 promotion이 필요한 타입으로 조만간 추가될 것이다.

스펙 문서에서 floating-point or promoted integral type로 표현한 타입을, 설명 과정에서 promoted 산술 타입으로 언급한다.
스펙 문서에서 promoted integral type로 표현한 타입을, 설명 과정에서 promoted 정수형 타입으로 언급한다.
floating-point타입이 prvalue category의 파라미터인 경우, 설명 과정에서 promoted 부동소수점 타입으로 언급한다.

산술 타입은 설명 편의를 위해, 산술 타입 또는 non-promoted 산술 타입으로 언급한다.

스펙 문서를 통해 산술 타입의 요구 조건을 만족하는 일부 타입을 alias 산술 타입으로 지정하도록 요구하고 있다. 예를 들어 std::ptrdiff_tstd::size_t 타입이 대표적이다. alias 산술 타입은 이름만 다르게 부를 뿐, 타입 자체는 본질적으로 원본 타입과 같다.

usual arithmetic conversion, common type, composition pointer type

usual arithmetic conversion는 산술 타입 L, R에 대해, 두 피연산자가 표현할 수 있는 데이터 표현을 충분히 표현할 수 있는 데이터 표현으로 설계한 타입한 LR 타입으로 변환한다. LR 타입 계산 과정을 기술한 조항이 스펙의 usual arithmetic conversion 파트다. usual arithmetic conversion는 promoted 산술 타입을 소스 타입으로 받는다. 따라서 usual arithmetic conversion 결과는 rank가 promoted 산술 타입보다 크거나 같은 타입으로 결정된다.

C++ 타입에서 usual arithmetic conversion를 조금 더 확장해 클래스 레벨에서 정의할 수 있는데 이를 common type이라고 한다. usual arithmetic conversion과 common type 모두 데이터 표현을 손실없이 보전하는 것을 설계 목표로 한다. 데이터 손실 여부는 역변환으로 원본 데이터를 다시 복원할 수 있다면 손실이 아니다.

유사한 개념이 두 포인터 타입간에도 적용되는데, 이를 composition pointer type이라고 한다.
composition pointer type는 입력으로 주어진 두 포인터 타입을 불완전하게 사용할 수 없도록 more cv-qualifier 형태로 결정한다.

underlying type

기존 타입을 설계한 데이터 표현을 가져와 새로운 데이터 타입을 설계하는데 사용하는 경우가 있다. 이 때 차용된 기존 타입을 underlying type이라고 한다. alias 산술 타입과 다른 점은 underlying type과 underlying type를 사용하는 타입은 완전히 서로 구분되는 타입이다.

대표적인 경우가 enumeration 타입이다. enumeration 타입은 이미 사용되고 있는 정수형 타입의 데이터 표현을 가져와, 자체 타입을 구축한다. 따라서 enumeration 타입은 underlying type를 갖고 있다. underlying type으로 전환해 일부 연산을 진행한다.

파라미터 선언과 요구하는 value category 유형 맵핑

built-in operator 중 하나인 assignment operator =를 value category 관점으로 언급하고 있는 다음 스펙 문장에서 설명을 시작하자.

For example, the built-in assignment operators expect that
    the left operand is an lvalue and that 
    the right operand is a prvalue and 
    yield an lvalue as the result.

할당 연산자의 왼쪽 피연산자는 lvalue category이고, 오른쪽 피연산자는 prvalue category이고, computation 결과는 lvalue category이다. 스펙 문서에서 xvalue category를 제외한 두 value category를 한번에 언급하고 있다.

위 글에 대응하는 built-in operator function 선언 스펙 문서 내용은 다음과 같다.

For every triple (L, vq, R), 
 where
  L is an arithmetic type,
  R is a floating-point or promoted integral type,

 vq L& operator=(vq L&, R); 

앞선 문장에서 첫번째 피연산자를 lvalue category로 언급했고, 실제 구현 함수 선언에서는 vq L&로 표현하고, 앞선 문장에서 두번째 피연산자를 prvalue category로 언급했고, 실제 구현 함수 선언에서는 R로 표현하고 있다. 앞선 문장에서 computation expression 결과를 lvalue category로 언급했고, 실제 구현 함수 선언에서는 vq L&로 표현하고 있다.

두 문장으로부터 다음과 같은 결론에 도달할 수 있다. lvalue category를 요구하는 연산자를 정의할 때, 대응하는 함수 파라미터에 심블 &를 사용하고, prvalue category를 요구하는 연산자를 정의할때, 대응하는 함수의 파라미터에 심블 &없이 plain type를 사용한다. 그리고 xvalue category를 요구하는 연산자를 정의할 때, 대응하는 함수의 파라미터로 심블&&를 사용한다고 짐작할 수 있다.

실제 연산자가 요구한 value category과 아규먼트의 value category가 다르면, 연산자가 요구하는 value category로 아규먼트 value category를 변환한다. 자세한 내용은 이전 게시글에서 설명했다. 이전 게시글을 읽어보면, 왜 이런 식으로 맵핑될 수 밖에 없는지 은연중에 설명하고 있다.

함수 선언 설계자 입장에서는 정밀하게 기술해야 하지만, 사용자 입장에서는 필요한 유연성를 확보해야 한다. 정밀한 의도는 정밀한 파라미터 선언을 통해 제어하고, 아규먼트의 value category에서 파라미터의 value category로의 암묵적 변환을 지원함으로써 유연성을 확보한다.

여기까지 이해했다면, 함수 선언을 통해 value category를 요구하는 구문 표기법을 이해한 것이다.

스펙의 built-in operator function 파트에서 구체적으로 value category를 어떤 식으로 제어하고 사용하지 구체적으로 알아보자. 스펙의 built-in operator function 파트는 C++ 언어의 가장 밑단 기술이기 때문에, 전반적인 C++ 프로그래밍을 이해하는데 크게 도움이 된다.

전체 설명 과정은 함수 설계자 의도한 value category를 먼저 파악하고, value category에 적용된 데이터 타입을 알아보고, 마지막으로 해당 연산자 용도를 설명한다. 본 게시글의 주제는 타입이 아닌 value category다.

전치 증감(++, --) 연산자

For every pair (T, vq),
 where
  T is a cv-unqualified arithmetic type other than bool or 
       a cv-unqualified pointer to (possibly cv-qualified) object type,

// 전치 증감 연산자
 vq T& operator++(vq T&);
 vq T& operator--(vq T&);

전치 증감 연산자의 첫 파라미터를 vq T&로 명시했음으로 non-const lvalue category를 피연산자에게 요구한다. 반환 타입을 vq T&로 명시했음으로 computation expression 결과는 non-const lvalue category이고, 첫 파라미터와 같다.

T 타입 자체는 non-cv 타입이다. 따라서 겉으로 보여지는 cv에 영향을 주지 않는다. 첫 피연산자로 const 타입이 전달되면 컴파일 오류다.
T가 산술 타입이면 파라미터의 prvalue category로 되지 않았기 때문에, promotion이 필요없는 bool 타입이 제외한 non-promoted 산술 타입이다. bool 타입에 대한 증감 연산은 컴파일 오류다.

T는 object type의 pointer 타입이 될 수 있다. 따라서 포인터 타입에 대해서도 증감 연산을 수행한다.

전치 증감 연산의 피연산자로 volatile 객체사용은 deprecated 상태다.

int main() {
    int a{ 10 };
    ++a = 100;
    --a = 100;

    bool b{ true };
    volatile int c{ 10 };
    const int d{ 10 };
    // ++b;    //error C2428: '++': not allowed on operand of type 'bool'
    // ++c;    //error: increment of object of volatile-qualified type 'volatile int' is deprecated [-Wdeprecated-volatile]
    // ++d;    //error: cannot assign to variable 'd' with const-qualified type 'const int'
    return 0;
}

후치 증감(++,--) 연산자

For every pair (T, vq),
 where
  T is a cv-unqualified arithmetic type other than bool or 
       a cv-unqualified pointer to (possibly cv-qualified) object type,

// 후치 증감 연산자
 T operator++(vq T&, int); 
 T operator--(vq T&, int);

후치 증감 연산자의 첫 파라미터를 vq T&로 명시했음으로 non-const lvalue category를 피연산자에 요구한다. 두번째 파라미터는 후치 연산자를 표현하기 위해 int 타입이다. 반환 타입을 T로 명시했음으로 computation expression 결과는 전치 연산자와 다르게 non-cv prvalue category다.

T 타입 자체는 non-cv 타입이다. 따라서 겉으로 보여지는 cv에 영향을 주지 않는다. 첫 피연산자로 const 타입이 전달되면 컴파일 오류다.

T가 산술 타입이면 파라미터의 prvalue category로 되지 않았기 때문에, promotion이 필요없는 bool 타입이 제외한 non-promoted 산술 타입이다. bool 타입에 대한 증감 연산은 컴파일 오류다.

T는 object type의 pointer 타입이 될 수 있다. 따라서 포인터 타입에 대해서도 증감 연산을 수행한다.

후치 증감 연산자의 첫 피연산자로 volatile 객체사용은 deprecated 상태다.

int main(){
    int a{ 10 };
    // a++ = 200;  //error C2106: '=': left operand must be l-value
    // a-- = 100;  //error C2106: '=': left operand must be l-value

    bool b{ true };
    volatile int c{ 10 };
    const int d{ 10 };
    // b++;    //error C2428: '++': not allowed on operand of type 'bool'
    // c++;    //error: increment of object of volatile-qualified type 'volatile int' is deprecated [-Wdeprecated-volatile]
    // d++;    //error: cannot assign to variable 'd' with const-qualified type 'const int'
    return 0;
}

built-in dereference 연산자

For every (possibly cv-qualified) object type T and
for every function type T that has neither cv-qualifiers nor a ref-qualifier, 

  T& operator*(T*)

T 타입이 cv 타입이더라도 최종적으로 T* 타입 형태로 사용되면 전체 cv는 마지막 * 이후로 판단하기 때문에 non-cv가 된다는 사실에 주의하자.

연산자의 파리미터를 T*로 명시했음으로 non-cv prvalue category를 피연산자로 요구하고, 반환 타입을 T& 명시했음으로 computation expression 결과는 t-cv lvalue category이다.

T는 t-cv object 타입이 될 수 있다. dereference 연산자를 통해 t-cv lvalue category를 가진 object를 얻어낼 수 있다.

T는 멤버 함수를 제외한 함수 타입이 될 수 있다. 오직 멤버 함수만 cv-qualifier나 ref-qualifier를 가질 수 있다. 따라서 함수 포인터로부터 lvalue category를 가진 function를 얻어낼 수 있다. function 타입은 항상 lvalue category를 갖는다.

해당 연산자는 포인터에서 포인터를 만든 원본 타입을 얻어낼 때 사용한다. 흔히 역참조 연산자라고 부른다.

#include <print>

int main() {
    int     a{ 10 },
         * pa = &a ;

    int const     ca{ 10 },
               * pca = &ca ;

    *pa;
    *pa = 100;
    std::print("{}", a);

    *pca;
    // *pca = 100; // error C3892: 'pca': you cannot assign to a variable that is const
    return 0;
}
[출력 결과]
100

+, - 일항 연산자

For every type T

  T* operator+(T*);

연산자의 파리미터를 T*로 명시했음으로 non-cv prvalue category를 피연산자로 요구한다. 반환 타입을 T* 명시했음으로 computation expression 결과는 non-cv prvalue category다.

T는 모든 타입이 가능하다.

연산자에 숨어있는 의미는 T 타입이 암묵적으로 T*로 전환될 수 있다면, 해당 연산자를 호출할 수 있다. 대표적으로 배열 타입(다중 배열 타입도 가능), 함수 타입, 람다 함수 등이 그 후보 타입이다. 특히, 다중 배열 타입의 배열 포인터 타입을 쉽게 얻어낼 수 있다.

#include <print>

void sum(int, int) {};

int main() {
    do {
        int a[]{ 1,2,3 };
        std::println("{:50} +:{:20}", typeid(a).name(), typeid(+a).name());
    } while (false);

    do {
        int a[10][20][30]{};
        std::println("{:50} +:{:20}", typeid(a).name(), typeid(+a).name());
    } while (false);

    do {
        auto fn = [](int a, int b) { return a + b; };
        std::println("{:50} +:{:20}", typeid(fn).name(), typeid(+fn).name());
    } while (false);

    std::println("{:50} +:{:20}", typeid(sum).name(), typeid(+sum).name());
    return 0;
}
[출력 결과]
int [3]                                            +:int * __ptr64
int [10][20][30]                                   +:int (* __ptr64)[20][30]
class `int __cdecl main(void)'::`8'::<lambda_1>    +:int (__cdecl*)(int,int)
void __cdecl(int,int)                              +:void (__cdecl*)(int,int)
For every cv-unqualified floating-point or promoted integral type T,

  T operator+(T);
  T operator-(T);

연산자의 파라미터를 T로 명시했음으로 non-cv prvalue category를 피연산자로 요구한다. 반환 타입을 T 명시했음으로 computation expression 결과는 non-cv prvalue category다. 파라미터 타입과 반환 타입은 같은 타입이다.

T가 파라미터의 non-cv prvalue category로 사용되기 때문에, T는 promoted 산술 타입이고, 같은 타입을 사용하는 반환 타입도 덩달아 promoted 산술 타입이 된다. 따라서 해당 연산자는 단순히 부호만 변경하는 것이 아니라 타입도 promotion한다.

#include <print>

int main() {
    char a{ 'a' };
    std::println("orginal type:'{}' promoted type:'{}'", typeid(a).name(), typeid(+a).name());    

    const int b1{ 10 };
    volatile int b2{ -10 };
    std::println("+:'{}' +:'{}'", +b1, +b2);
    std::println("-:'{}' -:'{}'", -b1, -b2);
    return 0;
}
[출력 결과]
orginal type:'char' promoted type:'int'
+:'10' +:'-10'
-:'-10' -:'10'

%연산자, 비트 연산자

For every pair of promoted integral types L and R,
 LR is the result of the usual arithmetic conversions between types L and R,

// % 연산자
 LR operator%(L, R);

// 비트 연산자
 LR operator&(L, R);
 LR operator^(L, R);
 LR operator|(L, R);
 L operator<<(L, R);
 L operator>>(L, R);

연산자의 두 파라미터를 L,R로 명시했음으로 non-cv prvalue category를 피연산자로 요구한다. 반환 타입을 LR, L로 명시했음으로 computation expression 결과는 non-cv prvalue category다.

파라미터로 사용되는 L, R는 promoted 정수형 타입이고, LR는 L,R의 usual arithmetic conversion 결과 타입임으로 promted 정수형 타입 중 하나다.

shift 연산자에 사용된 반환 타입 L는 첫번째 파라미터 타입과 같은 promoted 정수형 타입이다.

이들 연산자는 %, &, ^, |, <<, >> 비트 연산을 수행하고, 연산 과정에서 promotion 연산도 함께 수행한다.

For every triple (L, vq, R),
 where
  L is an integral type,
  R is a promoted integral type,

// compound 비트 연산
 L vq & operator%=(L vq &, R);
 L vq & operator&=(L vq &, R);
 L vq & operator|=(L vq &, R);
 L vq & operator^=(L vq &, R);
 L vq L& operator<<=(L vq &, R);
 L vq & operator>>=(L vq &, R);  

연산자의 첫 파라미터를 L vq&로 명시했음으로 non-const lvalue category를 피연산자로 요구한다. 두번째 파라미터를 R로 명시했음으로 non-cv prvalue category를 피연산자로 요구한다. 반환 타입을 L vq &로 명시했음으로 computation expression 결과는 non-const lvalue category이고, 첫 파라미터과 같은 타입이다.

L 타입은 non-promoted 정수형 타입이고, R는 promoted 정수형 타입이다. 반환 타입 L는 첫번째 파라미터 타입에 사용된 정수형 타입이다.

%, &=, |=, ^=, <<=, >>= 연산을 각각 수행하고, 첫 파라미터에 대해서는 promotion 연산을 수행하지 않는다.

For every promoted integral type T,

  T operator~(T);

연산자의 파라미터를 T로 명시했음으로 non-cv prvalue category를 피연산자로 요구한다. 반환 타입을 T로 명시했음으로 computation expression 결과는 non-cv prvalue category이고, 첫 파라미터과 같은 타입이다.

T는 promoted 정수형 타입이고, NOT 비트 연산 수행하고, promotion 연산도 함께 수행한다.

#include <print>
#include <limits>

int main() {
    do {
        int a{ 1024 },
            ba{ -a - 1 };

        std::println("{} {}", a, ba);
        std::println("{} {}", ~a, ~ba);
    } while (false);

    do {
        unsigned int a{ 1024 },
                    ba{ std::numeric_limits<unsigned int>::max() - a};

        std::println("{} {}", a, ba);
        std::println("{} {}", ~a, ~ba);
    } while (false);

    return 0;
}
[출력 결과]
1024 -1025
-1025 1024
1024 4294966271
4294966271 1024

대수 연산자

대수 연산자에는 사칙 연산자, 등호 연산자, 비교 연산자 등을 포함한다. C++ 최근 버전에 <=> 연산자를 포함되었다.

For every pair of types L and R,
 where 
  each of L and R is a floating-point or promoted integral type
  LR is the result of the usual arithmetic conversions between types L and R

// 사칙 연산
  LR operator*(L, R);
  LR operator/(L, R);
  LR operator+(L, R);
  LR operator-(L, R);

// 등호 연산
  bool operator==(L, R);
  bool operator!=(L, R);

// 비교 연산
  bool operator<(L, R);
  bool operator>(L, R);
  bool operator<=(L, R);
  bool operator>=(L, R);

연산자의 두 파라미터를 L, R로 명시했음으로 non-cv prvalue category를 피연산자로 요구한다. 반환 타입을 LR, 또는 bool로 명시했음으로 computation expression 결과는 non-cv prvalue category이다.

L,R는 promoted 산술 타입이고, LRL, R에 대한 usual arithmetic conversion 타입이다. 파라미터로 전달되는 모든 아규먼트에 대해 promotion 연산이 수행된다.

promoted 산술 타입에 대한 사칙 연산, 등호 연산, 비교 연산을 각각 수행한다.

promoted 산술 타입에 대한 사칙 연산, 등호 연산, 비교 연산을 수행한다. 모든 아규먼트에 대해 promotion 연산이 수행된다

For every integral type T

  std::strong_ordering operator<=>(T, T);

연산자의 두 파라미터를 T로 명시했음으로 non-cv prvalue category를 피연산자로 요구한다. 반환 타입을 std::strong_ordering로 명시했음으로 computation expression 결과는 non-cv prvalue category이다.

정수형 타입에 대한 <=> 과정에서 non-cv prvalue category 파라미터를 사용하기 때문에 다른 여타 연산처럼 T는 promoted 정수형 타입이 되어야 한다. cppinsigts.io 사이트로 promoted 정수형 타입이 사용됨을 간단히 확인할 수 있다.

#include <compare>

int main() {
    char a{'a'}, b{'b'};
    auto&& p =  (a<=>b);
    return 0;
}
[cppinsigts.io 결과]
#include <compare>

int main()
{
  char a = {'a'};
  char b = {'b'};
  std::strong_ordering && p = (static_cast<int>(a) <=> static_cast<int>(b));
  return 0;
}

promoted 정수형 타입의 서로 다른 데이터 표현을 가진 a, b에 대해서 항상 정렬 가능하기 때문에 std::strong_ordering 타입을 반환한다.

For every pair of floating-point types L and R,

  std::partial_ordering operator<=>(L, R);

연산자의 두 파라미터를 L,R로 명시했음으로 non-cv prvalue category를 피연산자로 요구한다. 반환 타입을 std::partial_ordering로 명시했음으로 computation expression 결과는 non-cv prvalue category이다.

LR는 promoted 부동소수점 타입이다. 부동소수점 타입의 서로 다른 데이터 표현을 가진 a, b에 대해 정렬이 불가능한 경우가 있습니다. 예를 들어 nan 값의 데이터 표현은 하나 이상 존재하고 있는데, 이들은 비교 연산 모두에서 false를 반환한다. 따라서 정렬할 수 없다. 부동소수점 타입 전체를 보면, 일부는 정렬 가능하고, 일부는 정렬 불가능하다.

아래 코딩에서 !=true이기 때문에 f1과 f2는 서로 다른 데이터 표현이라고 볼 수 있지만, 비교 연산 모두에 false를 반환한다.

#include <print>
#include <compare>

int main() {
    float f1 = std::nanf("0"), f2 = std::nanf("1");
    std::print(
        "!=:{:10} >:{:10} <:{:10} >=:{:10} <=:{:10}",
        f1 != f2, f1 > f2, f1 < f2, f1 >= f2, f1 <= f2
    );   
    return 0;
}
[출력 결과]
!=:true       >:false      <:false      >=:false      <=:false
For every T,
 where
  T is an enumeration type or a pointer type,
  R is the result type specified in 7.6.8

 R operator<=>(T, T);

연산자의 파라미터를 T로 명시했음으로 non-cv prvalue category를 피연산자로 요구한다. 반환 타입을 R로 명시했음으로 computation expression 결과는 non-cv prvalue category이다.

T가 같은 타입의 enumeration type이면, 반환 타입 R은 std::strong_ordering 타입이고, underlying type의 <=> 연산처럼 취급한다.

서로 다른 similar type의 pointer type의 <=> 연산에서 T는 두 타입의 composition type다. 반환 타입 Rstd::strong_ordering 타입이고, 포인터의 <=> 연산처럼 취급한다. 두 pointer type에 대한 composition type를 결정할 수 없다면 컴파일 오류다.

For every T,
 where
  T is an enumeration type or a pointer type,

 // 등호 연산
 bool operator==(T, T);
 bool operator!=(T, T);

 // 비교 연산
 bool operator<(T, T);
 bool operator>(T, T);
 bool operator<=(T, T);
 bool operator>=(T, T);

연산자의 두 파라미터를 T로 명시했음으로 non-cv prvalue category를 피연산자로 요구한다. 반환 타입을 bool로 명시했음으로 computation expression 결과는 non-cv prvalue category이다.

T가 하나의 enumeration type이면, underlying type에 대한 등호 연산, 비교 연산으로 연산한다.
서로 다른 similar type의 pointer type의 연에서 T는 두 타입의 composition type이고 포인터에 대한 등호 연산, 비교 연산처럼 연산한다. 두 pointer type에 대한 composition type를 결정할 수 없다면 컴파일 오류다.

For every T,
 where
  T is a pointer-to-member type or std::nullptr_t,
// 등호 연산자
 bool operator==(T, T);
 bool operator!=(T, T);

연산자의 두 파라미터를 T로 명시했음으로 non-cv prvalue category를 피연산자로 요구한다. 반환 타입을 bool로 명시했음으로 computation expression 결과는 non-cv prvalue category이다.

T가 std::nullptr_t 타입이면 모든 데이터 표현의 == 연산은 항상 true다.

#include <print>

int main() {
    std::nullptr_t m1{}, m2{};

    std::print(
        "==:{:10} !={:10}",
        m1 == m2, m1 != m2
    );

    return 0;
}
[출력 결과]
==:true       !=false

T가 pointer-to-member type 타입이면 같은 멤버를 의미하는 두 member pointer에 대해서만 == 연산은 true다.

#include <print>
struct A {
    int a, b;
};

struct B : A { };

int main() {       
    int A::*a1 = &A::a,
        A::*a2 = &A::b,
        B::*b1 = &B::a,
        B::*b2 = &B::b;

    std::print(
        "==:{:10} ==:{:10}",
        a1 == b1, b1 == b1
    );

    return 0;
}
[출력 결과]
==:true       ==:true

!= 연산 결과는 == 연산 결과의 NOT 연산이다.

For every T,
 where
  T is a pointer to object type,

  std::ptrdiff_t operator-(T, T);

연산자의 두 파라미터를 T로 명시했음으로 non-cv prvalue category를 피연산자로 요구한다. 반환 타입을 std::ptrdiff_t로 명시했음으로 computation expression 결과는 non-cv prvalue category이다.

T는 object type의 pointer 타입이고, 서로 다른 similar type의 pointer type의 연산에서 T는 두 포인터 타입의 composition type이다. 두 포인터에 대한 - 연산 결과 타입은 alias 정수 타입 중 하나인 std::ptrdiff_t 타입이다.

해당 연산자는 포인터간 - 연산을 수행하고 결과로 std::ptrdiff_t 타입을 반환한다.

#include <print>
int main() {
    int a{ 10 },
        * pa = &a;
    int const   b{ 10 },
        * const pb = &b;

    std::print(" pa:{}, pb:{}\n pa - ba:{} type:{}",
        typeid(pa).name(), typeid(pb).name(),
        pa - pb,
        typeid(pa-pb).name()    
    );
    return 0;
}
[출력 결과]
 pa:int * __ptr64, pb:int const * __ptr64
 pa - ba:-16 type:__int64
For every cv-qualified or cv-unqualified object type T

 T* operator+(T*, std::ptrdiff_t);
 T* operator+(std::ptrdiff_t, T*);
 T* operator-(T*, std::ptrdiff_t); 

연산자의 두 파라미터를 T* 또는 std::ptrdiff_t로 명시했음으로 non-cv prvalue category를 피연산자로 요구한다. 반환 타입을 T*로 명시했음으로 computation expression 결과는 non-cv prvalue category이다.

T 타입은 파라미터에서 T* 형태로 사용되기 때문에 non-cv이고, T 자체는 t-cv다. T 타입은 모든 object type이 될 수 있다.

해당 연산자는 포인터 타입과 std::ptrdiff_t에 대한 +, - 연산을 수행하고 결과로 T* 타입의 non-cv prvalue category를 반환한다.

For every pair (T, vq),
 where T is a cv-qualified or cv-unqualified object type,

 T * vq & operator+=(T * vq &, std::ptrdiff_t);
 T * vq & operator-=(T * vq &, std::ptrdiff_t);

연산자의 첫 파라미터를 T* vq &로 명시했음으로 non-const lvalue category를 피연산자로 요구한다. 두번째 파라미터를 std::ptrdiff_t로 명시했음으로 non-cv prvlaue category를 피연산자로 요구한다. 반환 타입을 T * vq &로 명시했음으로 computation expression 결과는 첫 파라미터과 같은 non-const lvalue category이다.

T 타입이 파라미터 T * vq & 형태로 사용되었음으로 non-const이고, T는 t-cv다. T는 모든 object type이 될 수 있다.

해당 연산자는 포인터 타입과 std::ptrdiff_t에 대한 +=, -= 연산을 수행하고, 결과로 첫번째 파라미터 타입과 동일한 타입의 non-const lvalue category를 반환한다.

For every cv-qualified or cv-unqualified object type T

 T& operator[](T*, std::ptrdiff_t);
 T& operator[](std::ptrdiff_t, T*);

연산자의 파라미터를 T*std::ptrdiff_t로 명시했음으로 non-cv prvalue category를 피연산자로 요구한다. 반환 타입을 T&로 명시했음으로 computation expression 결과는 t-cv lvalue category이다.

파라미터 T*는 non-cv이고, T&는 t-cv다.

포인터에 대한 인덱스 연산을 수행한다.

#include <print>
int main() {
    int a[]{ 1,2 };
    int const b[]{ 1,2 };

    std::println("{} {}",a[0], 1[a]);
    0[a] *= 100;
    a[1] *= 200;
    std::print("{} {}", a[0], 1[a]);

    // 0[b] *= 100; // error C3892: 'b': you cannot assign to a variable that is const
    // b[1] *= 200; // error C3892: 'b': you cannot assign to a variable that is const
    return 0;
}
[출력 결과]
1 2
100 400

assignment 연산자

For every triple (L, vq, R),
 where
  L is an arithmetic type, 
  R is a floating-point or promoted integral type

 L vq & operator=(L vq &, R);
 L vq & operator*=(L vq &, R);
 L vq & operator/=(L vq &, R);
 L vq & operator+=(L vq &, R);
 L vq & operator-=(L vq &, R)

연산자의 첫 파라미터를 L vq &로 명시했음으로 non-const lvalue category를 피연산자로 요구한다.
두번째 파라미터를 R로 명시했음으로 non-cv prvalue category를 피연자로 요구한다. 반환 타입을 L vq &로 명시했음으로 computation expression 결과는 첫 파라미터와 같은 non-const lvalue category이다.

L 타입은 산술 타입이고, R 타입은 promoted 산술 타입이다.

=, *=, /=, +=, -= 연산을 각각 수행한다. 연산 과정에서 첫 파라미터에 대해서는 promotion를 수행하지 않는다.

For every pair (T, vq),
 where
  T is any type,

 T* vq & operator=(T* vq &, T*);

연산자의 첫 파라미터를 T* vq &로 명시했음으로 non-const lvalue category를 피연산자로 요구한다. 두번째 파라미터를 T*로 명시했음으로 non-cv prvalue category를 피연자로 요구한다. 반환 타입을 T* vq &로 명시했음으로 computation expression 결과는 첫 파라미터와 같은 non-const lvalue category이다.

T는 t-cv다.

포인터 변수에 대한 할당 연산을 일반 변수처럼 수행한다.

#include <print>
int main() {
    int a{ 10 },
        b{ 20 },
        * pa{ &a },
        * pb{ &b },
        * pc{ nullptr };

    std::println("pa:{} pb:{}", *pa, *pb);
    pc = pa;
    pa = pb;
    pb = pc;
    std::print("pa:{} pb:{}", *pa, *pb);
    return 0;
}
[출력 결과]
pa:10 pb:20
pa:20 pb:10
For every pair (T, vq),
 where T is an enumeration or pointer-to-member type,

 T vq & operator=(T vq &, T);

연산자의 첫 파라미터를 T vq &로 명시했음으로 non-const lvalue category를 피연산자로 요구한다. 두번째 파라미터를 T로 명시했음으로 non-cv prvalue category를 피연자로 요구한다. 반환 타입을 T vq &로 명시했음으로 computation expression 결과는 첫 파라미터와 같은 non-const lvalue category이다.

T 타입은 같은 enumeration 타입이거나 pointer-to-member type이다.

해당 연산자는 두 타입에 대한 할당 연산을 일반 변수처럼 수행한다.

#include <print>

enum class EA :int {
    A1 = 10, A2 = 20, A3 = 30,
};

int main() {
    EA a{ EA::A1 };

    (a = EA::A2) = EA::A3;
    std::print("{}", static_cast<int>(a));
    return 0;
}
[출력 결과]
30
#include <print>

class A {
    int a{ 10 }, b{ 20 }, c{ 30 };
public:
    void call();
};

void A::call() {
    int A::* p = &A::a;
    (p = &A::b) = &A::c;
    std::print("{}", this->*p);
}

int main() {
    A a{};
    a.call();
    return 0;
}
[출력 결과]
30

논리 연산자

bool operator!(bool);
bool operator&&(bool, bool);
bool operator||(bool, bool);

연산자의 파라미터를 bool로 명시했음으로 non-const prvalue category를 피연산자로 요구한다. 반환 타입을 bool로 명시했음으로 computation expression 결과는 non-cv prvalue category이다.

bool 변환을 적용할 수 있는 타입을 아규먼트로 받아, 논리 !, &&, || 연산을 각각 수행한다.

삼항 연산자

For every pair of types L and R,
 where
  each of L and R is a floating-point or promoted integral type,
  LR is the result of the usual arithmetic conversions between types L and R

 LR operator?:(bool, L, R);

연산자의 첫 파라미터를 bool로 명시했음으로 non-cv prvalue category를 피연산자로 요구한다. 나머지 파라미터를 LR로 명시했음으로 non-cv prvalue category를 피연산자로 요구한다.
반환 타입을 LR로 명시했음으로 computation expression 결과는 non-cv prvalue category이다.

L과 R는 promoted 산술 타입이고, LR는 L과 R에 대한 usual arithmetic conversion 타입이다.

promoted 산술 타입에 대한 삼항 연산을 수행한다. 두번째 이후 아규먼트에 대해 promotion를 수행한다.

For every type T,
 where
  T is a pointer, pointer-to-member, or scoped enumeration type

 T operator?:(bool, T, T);

연산자의 첫 파라미터를 bool로 명시했음으로 non-cv prvalue category를 피연산자로 요구한다. 나머지 두 파라미터를 T로 명시했음으로 non-cv prvalue category를 피연산자로 요구한다.
반환 타입을 T로 명시했음으로 computation expression 결과는 non-cv prvalue category이다.

두 similar 포인터 타입의 연산에서 T는 두 타입의 composition pointer type이다.
T가 pointer-to-member 타입이면, 두 아규먼트는 cv를 제외하고 같은 타입이어야 한다.
T가 scoped enumeration type 타입이면, 두 아규먼트는 cv를 제외하고 같은 타입이어야 한다.

pointer-to-member 연산자

For every quintuple (C1, C2, T, cv1, cv2),
 where
  C2 is a class type,
  C1 is the same type as C2 or is a derived class of C2,
  T is an object type or a function type,

 T cv12 & operator->*(C1 cv1*, T cv2 C2::*);

연산자의 첫 파라미터를 C1 cv1*로 명시했음으로 non-cv prvalue category를 피연산자로 요구한다. 마지막 * 다음 위치가 non-cv다.

두번째 파라미터를 T cv2 C2::*로 명시했음으로 non-cv prvalue category를 피연산자로 요구한다. 역시 마지막 * 다음 위치가 non-cv다.

반환 타입을 T cv12 &로 명시했음으로 computation expression 결과는 t-cv cv12 lvalue category이다.

T 타입은 object type이거나 function type이 될 수 있다.
C1과 C2 타입은 둘다 클래스 타입이고 C1 타입은 C2 타입과 같거나 C2 타입의 파생 클래스다. 상속 관계로 인해, C1의 모든 멤버 집합은 C2의 모든 멤버 집합을 포함한다.

->* 연산자는 첫번째 아규먼트로 지정한 객체 내부에서 찾길 원하는 멤버를, 두번째 아규먼트로 지정한다. ->* 연산자 결과는 cv12 lvalue category로 반환한다.

첫번째 아규먼트에서 추론 과정을 거쳐 cv1를 얻어내기 때문에 cv1는 아규먼트 객체의 cv과 같아진다. 두번째 아규먼트에서 추론 과정을 거쳐 cv2를 얻어내기 때문에 cv2는 아규먼트 객체의 cv과 같아진다.
반환 타입의 cv는 T 타입의 t-cv, cv12의 합집합이다.

a.->*b 구문은 (*a).*b 구문은 상호 치환 가능하다.

#include <print>

class A {
protected:
    int v1{ 10 };
    int sum(int a, int b) {
        std::println("A::sum");
        return a + b;
    }
};

class B {
protected:
    int v1{ 20 };
    int sum(int a, int b) {
        std::println("B::sum");
        return a + b;
    }
};

class C : public A, public B {
protected:
    int v1{ 30 };

public:
    void call();

    int sum(int a, int b) {
        std::println("C::sum");
        return a + b;
    }
};

void C::call() {
    int (A:: * fa)(int, int) = &C::A::sum,
        (B:: * fb)(int, int) = &C::B::sum,
        (C:: * fc)(int, int) = &C::sum
        ;
    int A::* va = &C::A::v1,
        B::* vb = &C::B::v1,
        C::* vc = &C::v1
        ;

    C c{};
    (c.*fa)(10, 20);
    (&c->*fb)(10, 20);
    (c.*fc)(10, 20);

    std::println("{} {} {}", c.*va, &c->*vb, c.*vc);
    c.*va   *= 10;
    &c->*vb *= 10;
    c.*vc   *= 10;
    std::print("{} {} {}", c.*va, &c->*vb, c.*vc);
}

int main() {
    C c{};
    c.call();
    return 0;
}
[출력 결과]
A::sum
B::sum
C::sum
10 20 30
100 200 300
반응형