개요
computation expression에 참여하는 모든 피연산자에 요구하는 value category는 function로 명시한다.
함수를 통해 value category를 명시하는 기본 규칙
int a, b;
/*...*/
a + b;
피연산자 a, b에 요구되는 value category는 + 연산자 함수를 통해 제어한다. primitive type에 대한 연산자 함수를 built-in operator function
이라 부르고, 스펙 문서의 [over.built] 섹션에서 설명한 내용에 따라 구현 컴파일러가 구현을 일반 함수처럼 제공한다. function overload resolution 과정을 거쳐 최종적으로 호출할 함수를 결정하는 과정 역시 일반 함수과 같다.
// L 과 R는 floating-point 또는 promoted integral type
// LR는 L과 R에 대한 usual arithmetic conversion 결과 타입
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);
a + b; 구문에 대한 function overload resolution 결과로 LR operator+(L, R);를 선택했다면, 두 피연산자로 plain type, 즉 타입 이름 외에 추가한 항목이 없는 순수한 L과 R 타입을 사용하기 때문에 두 피연산자에 대해 prvalue를 요구하게 된다. 반환 타입, LR는 L과 R에 대한 arithmetic conversion 결과 타입으로 역시 plain type이다. 따라서 a + b; 구문에 대한 computation expression 결과의 value 유형은 prvalue다.
앞서 게시글에서도 언급했듯, computation expression 결과는 value과 side effect다. a + b; 구문이 온전히 prvalue간 연산임으로 프로그래머가 인지할 수 있는 side effect를 없다고 하더라도, 명확한 prvalue 결과가 있음으로, 컴파일러는 해당 구문을 함부로 제거할 수 없다.
해당 구문으로 (void) (a + b); 구문으로 변경하면, 명시적으로 computation expression 결과 중 하나인 value를 무시할 수 있다고 알려주면 discarded value 구문이 완성되고, 컴파일러는 해당 구문을 맘놓고 소거할 수 있는 권한을 얻게 된다.
관련 스펙 조항:
The result of a prvalue is the value that the expression stores into its context;
a prvalue that has type cv void has no result.
lvalue과 xvalue 결과는 expression 기술한 entity이 출처라면, prvalue 결과는 expression 기술한 context이 출처다. 결과 타입이 cv void 타입이면 computation expression의 결과가 없다고 따라서 context 출처도 존재하지 않는 코딩으로 볼 수 있다.
위에 요구사항을 2 + 3; 구문은 완벽하게 준수하지만, 예제로 주어진 a + b; 구문에서 + 연산자는 prvalue를 피연산자에 요구하고, 실제 피연산자가 들고있는 패는 lvalue다.
딱 이런 사항을 위해 glvalue(lvalue 또는 xvalue)를 prvalue로 변환하는 glvalue-to-prvalue conversion를 피연산자에 적용함으로써 + 연산자의 요구조건을 들어주게 된다. 즉, 피연산자를 회유하여, 연산자 요구조건을 들어주게 된다. 프로그래밍 세계에서는 함수가 절대 반지를 갖고 있다.
C++ 언어의 primitive type에 대한 연산자 함수 구현은 C 언어의 연산자 함수를 그대로 옮겨오고, 여기에 필요를 따라 C++ 언어에서 추가 요구조건을 명시함으로써 완성한다. 추가한 요구 조건은 C 언어 요구 조건이 그대로 C++에서 수용하도록 1차적으로 고려한다. 따라서 C 언어로 작성한 코딩은 C++ 코딩에서 문법 오류가 없지만, C++ 언어로 작성한 코딩은 C 코딩에서 문법 오류가 발생한다.
결국 C++ 스펙의 built-in operator function 조항은 C 언어의 연산자를 operator function으로 전환하고 일부 조항을 추가로 정의한 문서다. 일부 연산자 함수에 대한 정의는 꽤나 많은 추가 설명이 필요한 이유로, 스펙 문서의 별도 항목으로 추가 설명한다. 삼항 연산자가 대표적인 사례다. C++의 삼항 연산자를 built-in operator function 조항에서 설명하기 위해서는 꽤나 복잡한 constraint expression를 도입해야 한다. 이런 이유로 간략 버전을 설명한 후, 별도 추가 요구사항을 별도로 명시한다.
// L과 R는 floating-point type 또는 promoted integral type
// LR는 L과 R에 대한 usual arithmetic conversions 결과 type
// T는 pointer, pointer-to-member, 또는 scoped enumeration type
LR operator?:(bool, L, R);
T operator?:(bool, T , T );
operator?: 함수 설명 파트를 보면 일단 C++에만 있는 pointer-to-member 타입이 등장한다. 따라서 C++ 언어가 C 언어에 부가 조항을 첨부했음을 알 수 있다. 해당 스펙 문서만 보면 operator?: expression 결과는 prvalue를 갖는다. 하지만, 다음 구문이 C++에서 컴파일 오류없이 작동한다. 결국 삼항 연산자 관련 별도 스펙 문서를 통해 두 피연산자 모두 lvalue이면 operator?: expression 결과는 lvalue라는 조항을 추가해, C++ 언어에서는 컴파일 오류가 없음을 명시한다. 삼항 연산자에 대한 첨부된 요구 사항은 다음 기회에 깊게 언급하겠다.
int a, b;
/* ... */
( (a>b) ? a : b ) = 10;
함수는 value category를 명시한다.
[basic.lval] 스펙 문서 내용 발췌
User-defined operators are functions, and the categories of values they expect and yield are determined by their parameter and return types.
함수를 통해서 어떤 value category를 피연산자로, 또는 아규먼트로 받길 원하는지 명시하고, 함수 expression 결과로 어떤 value가 만들어지는지 명시한다.
compound assignment operator 부분을 확인해보자.
int a, b;
/*...*/
a += b;
C++의 built-in operator function 파트 설명은 다음과 같다.
// L는 arithmetic type
// R는 floating-point 또는 promoted integral type
// vq ∈ { volatile, {}}
vq L& operator=(vq L&, R);
vq L& operator*=(vq L&, R);
vq L& operator/=(vq L&, R);
vq L& operator+=(vq L&, R);
vq L& operator-=(vq L&, R);
assignment operator는 두 피연산자를 요구하고, 첫 피연산자는 const 타입이 될 수 없다. 첫 피연산자는 주어진 타입으로부터 derived type를 구성하는 & 심블이 붙어 있다. 이 경우 피연산자에게 lvalue를 요구하고,따라서 rvalue(xvalue 또는 prvalue)가 첫 피연산자로 제공되면 오류다. 왜냐하면 rvalue에서 lvalue로의 conversion이 작동하지 않기 때문이다. 참고로 prvalue에서 xvalue로 변환이 존재하는데, 이를 temporary materialization conversion이라고 부른다. 이 위치에서 요구하는 conversion는 lvalue로 변환이다.
두번째 피연산자는 plain type임으로 prvalue를 피연산자에게 요구한다.
value category는 prvalue, xvalue 또는 lvalue로 구분한다. 두 value를 묶어 한번에 부르는 이름은 존재하더라도, 하나의 value가 동시에 두 종류에 겹칠 수 없다. 따라서 함수를 통해 value category를 정의하는 방법도 딱 3가지 유형 밖에 없다.
prvalue를 받고 싶다면 derived type이 없이 그냥 해당 타입 이름만 연산자 함수의 파라미터나 반환 타입에 적어주면 된다.
lvalue를 받고 싶다면 derived type 심블 &를 타입 이름과 함께 연산자 함수의 파라미터나 반환 타입에 적어주면 된다.
xvalue를 받고 싶다면 derived type 심블 &&를 타입 이름과 함께 연산자 함수의 파라미터나 반환 타입에 적어주면 된다.
타입 추론 시점에 참조 추론을 유도하는 구문: auto &&
피연산자에 대한 파라미터 선언 결과가 derived type 심블 & 이나 &&를 포함하면, 파라미터 타입이 참조 타입이라고 말한다.참조 추론 유도 구문은 피연산자 전체 구문이 참조 타입을 갖도록 유도하는 구문이다.
auto sum(auto&& a, auto&& b) { return a + b;}
template<typename T>
T f(T&& a, T&& b) { return a + b;}
위 구문에서 auto&&나 T&& 구문이 참조 추론 유도 구문이다. auto&& 구문 중심으로 아래에서는 설명한다.
타입 추론 시점에, 피연산자로 lvalue를 제공하면 auto를 T&로, 피연산자로 xvalue를 제공하면 auto를 T로 추론하도록 하는 구문이다. 참조 추론 유도 구문은 전체 타입을 온전히 추론해야 한다. 일부 타입이 이미 추론된 상태거나, 일부 타입이 추론 불가능하거나, auto const&& 구문처럼 추론 정보 중 하나로 사용되어야 할 정보를 제공해서 부분 추론 요구하면, 참조 추론 연산 과정을 중단하고, 주어진 구문 자체를 그대로 해석한다. 즉 auto const&& 구문은 피연산자로 xvalue만을 요구하는 구문으로 해석한다.
'프로그래밍 언어 > C++' 카테고리의 다른 글
built-in operator를 통해 살펴보는 value category (1) | 2024.02.07 |
---|---|
value category의 서로 다른 유형간 변환 (0) | 2024.02.05 |
value category 이해하는 첫 단추 (0) | 2024.02.01 |
range-based for 구문 이해하기 (1) | 2024.01.28 |
프로그램 종료 과정 이해하기 (1) | 2024.01.26 |