이 블로그 검색

2012년 8월 10일 금요일

Lvalue 와 Rvalue 정리

c++11 의 기능중에 Rvalue references 을 보다 보니, Lvalue 와 Rvalue 의 판단 기준을 다시 한번 정리해야 할것 같다. 내가 지금까지 이것들을 구분하는 기준은 대입연산자 왼쪽, 오른쪽으로 기준으로 원시적 판단을 했었는데, C++ 의 새로운 기능을 보다보니...머릿속이 헷갈리기 시작한다.
먼저, 일반적이고 간단한 예를 들어보면 :

int i = 3;

이경우엔, Lvalue = i, Rvalue = 3 이다.. 지금까지 판단 기준이 통하는군...
그런데 다음 경우는 어떠한가?

int i = 1;
int j = 2;
i = j; // Lvalue = i, Rvalue = j ???

위 경우에는 Rvalue 는 없다. i,j는 모두 Lvalue 이며, 컴파일러에 의해 lvalue-to-rvalue conversion 이 발생하면서 j 가 마치 Rvalue 처럼 작동할 뿐이다. 그래서, 명확한 정의를 찾던 중 http://www.codeproject.com/Articles/313469/The-Notion-of-Lvalues-and-Rvalues 에서 좋은 내용을 찾았다. 알기 쉽게 잘 설명되어 있어서 정리해본다.

introduction

필자는 Lvalues and Rvalues 에 대해서 심각하게 생각해본적이 없으며, 그런것이 문제되는 경우는 컴파일시 에러가 나는 경우가 대부분이었으며, 이 또한 쉽게 에러를 수정할수 있었다.
즉 다음 코드에서처럼 말이다.

int  NextVal_1(int* p) { return *(p+1); }
int* NextVal_2(int* p) { return (p+1); }

int main()
{
    int a[] = {1,2,3,4,5};
    NextVal_1(a) = 9;   // 에러. left operand must be l-value
    *NextVal_2(a) = 9;    // Fine. Now a[] = {1,9,3,4,5}
}

위 코드를 통해서 내가 말하는것을 잘 이해했길 바란다. 그런데 내가 C++0X 의 RValue reference 부분을 읽기 시작했을때, 내 비젼과 확신이 조금씩 흔들리기 시작했다. 내가 Lvalue 로 당연히 여기던 것들이 Rvalue 로 보이기 시작했다. 이 글을 통해서 L & R value들에 관련된 다양한 개념들을 간략하게 정리하려 한다. 이것과 관련된 여러가지 정보들을 다시 구글링할 필요가 없도록 하나의 정보로 모으기 위해 노력하였음을 알아주길 바란다. 모든 크레딧은 원 저자들에게 돌린다.



Definitions

객체는 하나의 저장 영역으로 간주되어질수 있다. 그리고 이 저장 영역은 관찰 가능하거나 변경 가능하거나, 혹은 그 두가지가 접근 지정자에 의해 결정될수 있다. 즉,

int i;                    // i 에 연관된 저장영역은 Observable, Modifiable 모두 해당된다.
const int j = 8;   // j 에 연관된 저장영역은 Observable 만 가능하며, 변경 불가이다.

더 진행하기 전에 다음 구절을 기억하기 바란다. "Lvalueness 혹은 Rvalueness 개념은 전적으로 표현법이며 객체와 무관하다". 이걸 좀더 간단하게 말하자면 :

double d;

이제 d 는 단순히 double 타입의 객체이다 [ 그리고 d에 대해서 l/r 값을 따지는것은 이시점에서는 무의미하다]. 이제 다음처럼 표현된다면,

d = 3.1414 * 2;

모든 l/r 값 개념이 시작된다. 여기서 우리는 수치 연산으로 임시 값을 구한후 d 에 대해 대입식을 쓰고 있고, 이 임시값은 세미콜론 이후에는 사라질 것이다. 여기서 구분가능한 메모리 위치를 가르키는 'd'는 Lvalue 이다. 그리고 (3.1414*2) 로 계산되는 임시값은 Rvalue 이다.

자 이시점에 L/RValue를 한번 정의해보자.

Lvalue : Lvalue 는 객체를 참조하는 표현식이다 [메모리 위치를 가지고 있다] [The C Programming Language - Kernighan and Ritchie].

Rvalue : C++ 표준은 r-value 정의할때, 제외 개념으로 처리하고있느데, 다음과 같다. "모든 표현식은 Lvalue 거나 Rvalue이다" 고로, Rvalue 는 Lvalue 가 아닌 모든것이다. 정확하게 말하자면, 구분가능한 메모리 영역을 가지는 객체를 나타낼 필요가 없는 표현식이다(임시로 존재하는것일수 있다).

Points on Lvalues and Rvalues

1. Numeric literals, 3 과 3.14159, 이것들은 Rvalues 이다. character literals, 예를 들면 'a' 도 마찬가지이다.

2. enumeration 상수 구분자는 Rvalue 이다. 예를 들면:

    enum Color { red, green, blue };
    Color enumColor;
    enumColor = green;    // Fine
    blue = green;         // Error. blue is an Rvalue

3. binary + 연산자의 결과는 항상 Rvalue 이다.

    m + 1 = n  // Error. 왜냐하면 (m+1) 는 Rvalue.

4. 단항 & (address-of) 연산자는 그것의 피연산자로 Lvalue 를 필요로 한다. 즉, &n는 n이  Lvalue 인 경우에만 유효한 표현식이다. 그러므로, &3 같은 표현식은 에러이다. 다시한번 말하자면, 3 은 객체를 참조하고 있지 않다.그러므로 그것은 주소를 이용해서 불러낼수 없다. 비록 단항 & 연산자가 피연산자로 Lvalue 를 필요로 하지만, 그 결과는 Rvalue 이다.

    int n, *p;
    p = &n;     // Fine
    &n = p;     // Error: &n is an Rvalue

5. unary & 과는 대조적으로, unary * 는 그 결과로 lvalue 를 만들어 준다. non-null(유효한) 포인터 p 는 항상 객체를 가르킨다. 그러므로 *p 는 lvalue 이다. 예를 들면:

    int a[N];
    int *p = a;
    *p = 3;         // Fine.

    // 그 결과가 Lvalue 이긴 하지만, 피 연산자는 Rvalue 가 될수도 있다.
    *(p + 1) = 4; // Fine. (p+1) 는 Rvalue

6. Pre-increment 연산자 표현식의 결과는 LValues

    int nCount = 0;   // nCount 는 영속 객체를 나타내며 그러므로 Lvalue 이다.
    ++nCount;          // 이 표현식은 Lvalue 이다.왜냐하면
                                // 이것은 변경이후 nCount 객체를 가르키기 때문이다.

    // 이것이 Lvalue인 것을 증명하기 위해, 다음 연산을 할수 있다
    ++nCount = 5;    // Fine. nCount 는 5 이다.

7. 리턴타입이 오직 참조인 경우에만 함수 호출은 Lvalue 이다.

    int& GetBig(int& a, int& b)    // 함수 호출을 Lvalue 로 만들기 위해 참조를 반환
    {
        return ( a > b ? a : b );
    }

    void main()
    {
        int i = 10, j = 50;
        GetBig( i, j ) *= 5;
        // 여기서, j = 250. GetBig() 은 j의 참조를 리턴한다.
        // 그리고 그것에 5가 곱해진것으로 저장된다.
}

8. 참조는 그냥 이름이다. 그래서 Rvalue 에 묶인 참조 그 자체는 Lvalue 이다.

    int GetBig(int& a, int& b)    // 함수 호출을 Rvalue 로 만들기 위해 int를 리턴
    {
        return ( a > b ? a : b );
    }

    void main()
    {
        int i = 10, j = 50;
        const int& big = GetBig( i, j );
        // 'big'를 GetBig()의 리턴값(Rvalue)에 대해 Lvalue로 바인딩한다.

        int& big2 = GetBig(i, j); // Error. big2 가 const가 아니므로
        // temporary 는 non-const reference 에 바인드 불가.
    }

9. Rvalues 는 temporaries 이고 메모리 영역을 가르킬 필요가 없다. 그러나 어떤 경우에는 메모리를 가르킬수 있다. 하지만 이런 임시값에 대해서 작업하는것은 권장되지 않는다

    char* fun() { return "Hellow"; }

    int main()
    {
        char* q = fun();
        q[0]='h';    // 예외발생, fun() 이 임시 메모리를 리턴하는데 거기 접근하려 한다.
    }

10. 후위 증가(Post-increment) 연산자 표현식의 결과는 RValue 이다.

    int nCount = 0;  // nCount 는 영속 객체를 나타내므로 Lvalue
    nCount++          // 이 표현식은 Rvalue이다. 객체의 값을 복사하고,
                               // 변경한후 임시 복사를 리턴하기 때문이다.

    // 이것이 Rvalue라는것을 증명하기 위해, 다음 연산을 할수 있다.
    nCount++ = 5; //Error

정리해보면 다음과 같이 말할수 있다 :
만약 우리가 표현식의 주소를 안전하게 얻을수 있다면, 그것은 lvalue 표현식이다. 그렇지 않다면 그것은 rvalue 표현식이다.

Note : Lvalues 와 Rvalues 모두 변경가능 혹은 변경 불가일수 있다.여기 예제가 있다 :

string strName("Hello");                               // modifiable lvalue
const string strConstName("Hello");                // const lvalue
string JunkFunction() { return "Hellow World"; /*catch this properly*/}//modifiable rvalue
const string Fun() { return "Hellow World"; }      // const rvalue

Conversion between Lvalues and Rvalues

Rvalue를 필요로 하는곳에 Lvalue인 것이 사용될 수 있을까? 그렇다 가능하다.예를 들면,

int a, b;
a = 8;
b = 5;
a = b;

이 = 표현식은 Lvalue 인 b를 Rvalue 로 사용한다. 이 경우 컴파일러는 b에 저장된 값을 얻기 위해 lvalue-to-rvalue conversion 라고 불리는 것을 수행한다.

그럼 Lvalue 가 필요한곳에 Rvalue 가 사용될수 있을까? 아니 그것은 불가능하다.

3 = a    // Error. Lvalue 가 필요한곳에 3이라는 RValue가 사용됨

Acknowledgments

이 정보를 취합하고 구성하는것을 흔쾌히 도와준 Clement Emerson에게 감사한다.

External resources

1. http://msdn.microsoft.com/en-us/library/f90831hc.aspx
2. http://www.eetimes.com/discussion/programming-pointers/4023341/Lvalues-and-Rvalues

댓글 16개:

  1. 와웅 잘 이해가 안갔는데 감사합니당.

    답글삭제
  2. 잘봤습니다!! 감사해요

    답글삭제
  3. 자세한 예시와 설명 감사합니다~

    답글삭제
  4. 저에게도 도움이 많이되었습니다! 감사합니다 :)

    답글삭제
  5. 9번이 잘못된 것 같네요.
    함수 fun이 리턴하는 주소는 임시 주소가 아닙니다.
    C/C++에서 모든 리터럴 문자열은 고유의 메모리 주소를 가지며,
    저 경우 함수 fun이 리턴하는 "값"은 rvalue가 맞지만 그 내용이 의미하는
    "주소"는 유효한 주소입니다.

    q[0] = 'h';
    이 구문에서 문제가 발생하는 이유는
    q가 const char * 값이기 때문입니다.
    (사실 애초에 fun 함수 정의에서 컴파일 에러가 나야합니다. 문자열 리터럴을 char * 타입으로 리턴할 수 없습니다. const 타입은 암시적으로 비const 타입으로 캐스팅될 수 없기 때문이죠.)
    q[0] 은 *(q + 0)과 동일한 표현식인데,
    이는 *q와 동일하고 이녀석은 lvalue 이지만(*연산은 lvalue를 리턴합니다)
    불행히도 타입이 const char 타입입니다.
    때문에 다른 값을 대입하는 것은 컴파일 에러를 발생시킵니다.
    (사실 예외가 아니라 컴파일 오류가 난다고 했어야 맞습니다.)

    답글삭제
  6. 좋은 지적 감사드립니다. 말씀하신데로 문자열 리터럴은 프로그램 생명 주기 동안 유효한 메모리에 존재하므로 임시값이라고는 할수 없겠네요. 그리고 문자열 리터럴은 읽기 전용 메모리에 위치하므로 그 영역에 접근하려는 시도는 런타임시에 에러(segmentation fault, 정의되지 않은 행동)가 발생 될 겁니다. 읽기 전용 메모리를 변경하려는 시도는 아예 컴파일이 안되야 맞는데, 잠깐 컴파일 해보니 경고와 함께 컴파일은 되네요. 아마 이 글의 원 저자는 Rvalue는 임시값이지만 경우에 따라서는 유효한 메모리를 가질수도 있다. 하지만 Rvalue 가 접근 가능한 메모리를 가지고 있다 하더라도 더 이상의 작업은 하지말라... 이런 맥락으로 글을 작성한거 같네요. 제가 번역한 내용이 부실해서겠죠. 다음이 원 저자의 글이니 참고 하시기 바랍니다. ^^
    " 9. Rvalues are temporaries and doesn't necessarily point to an memory region but they may hold memory in some cases. It is not advisable to catch this address and do any further operations as it would be a booby trap to work on these temporaries. "

    답글삭제
  7. 좋은 글 감사합니다.

    사소한 것이긴 한데, 읽다보니 오자가 있는것이 보여서 알려드립니다.

    introduction 바로 밑의 코드에서

    NextVal_2(a) = 9; 이부분을
    *NextVal_2(a) = 9; 이렇게 고쳐야 되네요 ^^

    답글삭제
  8. 깔끔한 정리와 레퍼런스 정말 감사합니다!

    답글삭제
  9. 좋은 글 잘 읽었습니다. 감사합니다.

    답글삭제
  10. 감사합니다. 좋은 글입니다.

    답글삭제
  11. 좋은 자료감사드립니다~! ㅠ 도움이 많이 되었네요!!

    답글삭제
  12. 정말 좋은 글입니다! C++에서의 전위/후위 연산자 오버로딩에 대해서 깊게 이해할 수 있었습니다 :)

    답글삭제