값 범주
값 범주 (Value categories)
C에서 모든 표현식(연산자와 그 인자, 함수 호출, 상수, 변수 이름 등)은 타입과 **값 범주(value category)**라는 두 가지 독립적인 속성으로 특징지어져요. 타입이 "무엇인지"를 알려준다면, 값 범주는 "메모리를 갖는 객체인지, 아니면 잠깐 쓰고 버리는 값인지"를 알려줘요.
모든 표현식은 세 가지 값 범주 중 하나에 속해요: lvalue, non-lvalue 객체(우값, rvalue), 함수 지정자(function designator).
출처: cppreference
본문
lvalue 표현식
lvalue 표현식은 void가 아닌 객체 타입을 가진 표현식으로서, 잠재적으로 객체를 가리키는 표현식이에요 (평가 시점에 lvalue가 실제 객체를 가리키지 않으면 동작은 정의되지 않아요). 다시 말해 lvalue 표현식은 **객체의 정체성(object identity)**을 값으로 산출해요. "left value"라는 이름은 역사적인 것으로, CPL이라는 언어에서 대입 연산자의 왼쪽 피연산자로 쓰이던 데서 유래했어요.
lvalue 표현식은 다음과 같은 lvalue 문맥에서 쓸 수 있어요.
- 주소 연산자(&)의 피연산자 (lvalue가 비트 필드를 가리키거나
register로 선언된 경우 제외) - 전위/후위 증가·감소 연산자의 피연산자
- 멤버 접근(점,
.) 연산자의 왼쪽 피연산자 - 대입·복합 대입 연산자의 왼쪽 피연산자
lvalue 표현식을 sizeof, _Alignof, 또는 위 연산자들 외의 문맥에서 쓰면, 완전 타입의 비배열 lvalue는 **lvalue 변환(lvalue conversion)**을 거쳐요. 이는 객체의 값을 그 위치에서 메모리에 로드하는 동작을 모델링한 것이에요. 마찬가지로 배열 lvalue는 sizeof, _Alignof, 주소 연산자, 문자열 리터럴에서의 배열 초기화 외의 문맥에서 쓰이면 **배열-포인터 변환(array-to-pointer conversion)**을 거쳐요.
const/volatile/restrict 한정자와 _Atomic 타입의 의미는 오직 lvalue에만 적용돼요 (lvalue 변환은 한정자를 벗겨내고 원자성을 제거해요).
다음 표현식들은 lvalue예요.
- 객체를 가리키도록 선언된 식별자 (함수나 열거 상수가 아닌 것). 함수의 이름 붙은 매개변수도 포함돼요
- 문자열 리터럴
- (C99) 복합 리터럴(compound literal)
- 괄호로 감싼 표현식 (괄호 없을 때 lvalue라면)
- 왼쪽 인자가 lvalue인 멤버 접근(점) 연산자의 결과
->연산자를 통한 멤버 접근의 결과- 객체 포인터에 적용된 역참조(단항
*) 연산자의 결과 - 첨자 연산자(
[])의 결과
수정 가능한 lvalue(Modifiable lvalue)
수정 가능한 lvalue는 완전하고 비배열 타입이며 const로 한정되지 않은 lvalue 표현식이에요. struct/union이라면 그 멤버 중 const로 한정된 멤버(재귀적으로)가 없어야 해요.
증가/감소 연산자의 인자로, 그리고 대입·복합 대입 연산자의 왼쪽 인자로 쓸 수 있는 것은 오직 수정 가능한 lvalue 표현식뿐이에요.
non-lvalue 객체 표현식
**우값(rvalue)**으로도 알려진 non-lvalue 객체 표현식은, 객체를 가리키지 않고 객체 정체성이나 저장 위치가 없는 값을 산출하는 객체 타입의 표현식이에요. non-lvalue 객체 표현식의 주소는 취할 수 없어요.
다음 표현식들은 non-lvalue 객체 표현식이에요.
- 정수, 문자, 부동소수점 상수
- lvalue를 반환하도록 지정되지 않은 모든 연산자, 그중에서도
- 모든 함수 호출 표현식
- 모든 캐스트 표현식 (비슷해 보이는 복합 리터럴은 lvalue라는 점을 기억하세요)
- non-lvalue 구조체/공용체에 적용된 멤버 접근(점) 연산자.
f().x,(x, s1).a,(s1 = s2).m - 모든 산술·관계·논리·비트 연산자의 결과
- 증가·감소 연산자의 결과 (전위 형태는 C++에서 lvalue라는 점을 기억하세요)
- 대입 연산자의 결과 (C++에서도 lvalue)
- 조건 연산자 (C++에서는 두 번째·세 번째 피연산자가 같은 타입의 lvalue면 lvalue)
- 쉼표 연산자 (C++에서는 두 번째 피연산자가 lvalue면 lvalue)
- 단항
*연산자의 결과에 적용돼 무력화됐더라도 주소 연산자
특수한 경우로, void 타입의 표현식은 표현이 없고 저장이 필요 없는 값을 산출하는 non-lvalue 객체 표현식으로 간주돼요.
배열 타입 멤버(중첩 포함)를 가진 struct/union의 우값은 실제로 **임시 수명(temporary lifetime)**을 가진 객체를 가리킨다는 점을 기억해 두세요. 이 객체는 배열 멤버를 첨자로 인덱싱하거나, 배열 멤버의 배열-포인터 변환으로 얻은 포인터를 역참조해서 형성된 lvalue 표현식으로 접근할 수 있어요. (C99부터)
함수 지정자 표현식
함수 지정자(함수 선언이 도입한 식별자)는 함수 타입의 표현식이에요. 주소 연산자, sizeof, _Alignof 외의 문맥(마지막 두 개는 함수에 적용하면 컴파일 오류가 나요)에서 쓰이면, 함수 지정자는 항상 non-lvalue 함수 포인터로 변환돼요. 함수 호출 연산자는 함수 지정자 자체가 아니라 함수 포인터에 대해 정의된다는 점을 기억하세요.
더 알아보기
- lvalue가 어떤 변환을 거치는지, 그리고 배열-포인터 변환·한정자 제거의 규칙은 변환(implicit conversion) 문서와 이어져요.
- 주소 연산자
&가 lvalue에만 쓸 수 있는 이유는 값 범주 개념을 이해하면 자연스러워져요. - C++의 값 범주(value category)는 lvalue/rvalue/xvalue/prvalue로 더 세분화되니, 비교해 보면 흥미로워요.