본문 바로가기

3.구현/C or C++

[c++] 오른값(r-value) 참조 이해하기

들어가기

C++11부터 포함된 저의 머리를 아프게 만든 오른값(r-value) 참조에 대해 살펴볼려고 한다. 실무에서 c++을 쓸 기회가 없어서 신경쓰고 있지 않다가 최근에 여유가 생겨서 한번 정리해 보았습니다. 저처럼 오른값 참조에 이해가 잘 안되는 시는 분에게 도움되었으면 하네요.

작성자: ospace114@empal.com, http://ospace.tistory.com/

왼값(l-value)과 오른값(r-value)???

거칠게 말하면 값을 할당하는 등호 기준으로 등호 왼쪽이 왼값이고 등호 오른쪽이 오른값이다.

int n = 10;

위의 예에서 보면 n은 왼값이고 10은 오른값이다. 그래서 일반적으로 오른값이 왼값에 할당하게 된다. 그렇기 때문에 오른값을 왼값 위치에서 사용할 수 없다. 다른 말로는 오른값에는 할당할 수 없다. 그러나 단순하게 등호만 이야기 했지만 여러 연산자가 있어서 그때마다 판별해야한다.

왼값은 오른값이 할당되기에 접근할 수 있는 주소를 가지고 있다. 그렇기에 왼값은 변수, const 변수, 객체 등 같은 형태로 된다.

오른값은 변수, const 변수 뿐만 아니라 리터릴 값(숫자, 문자, 문자열 등), 연산식, 함수 등이 온다. 즉 거의 모든게 오른값으로 올 수 있다.

단순하게 정리한 부분이라 세세하게 들어가면 너무 다양한 형태가 있어서 언급하기가 쉽지 않다.

이렇게 일반적인 경우는 쉽게 이해가 되지만 함수를 호출하는 경우는 조금 헤갈릴 수 있다. 함수가 리터럴 값, 또는 상수를 리턴하는 경우 또는 주소를 리턴하는 경우가 달라진다. 리턴하는 값에 따라서 달라질 수 있다.

struct Box {
    int value = 10;
};

int& valueOf(Box& box) {
    return box.value;
}

int valueOf() {
    return 0;
}

void main() {
    Box box;
    valueOf(box) = 100; // (1)
    valueOf() = 100; // (2)
}

(1)인 경우는 멤버에 대한 참조를 리턴하므로 왼값이 되어서 값을 바로 할당할 수 있지만 (2)인 경우는 리터럴 값을 리턴하므로 오른값이 되어서 에러가 발생한다. 어떻게 보면 어떤 형태는 왼값과 오른값도 가능하지만 어떤 형태는 오른값만 가능하다. 오른값이 할당을 통해서 왼값으로 변환된다.

위의 예제는 극단적인 형태이고 대부분 다르게 작성하기에 이로 인해 문제가 발생하지 않는다. 그리고 위처럼 작성하는 경우 코드 가독성이나 구조적인 측면에서는 좋지 않아 보인다.

오른쪽값이 왼쪽값으로 할당하지 못하는 다른 말로 변환이 안되는 경우가 있다.

  • 왼값이 const가 있는 경우 초기화 이후 변환 불가
  • 왼값이 참조인 경우 오른값이 리터럴 값은 변환 불가
    • 단, const 참조인 경우는 가능

C++17 표준에서는 glvalue, prvalue, xvalue, lvalue, rvalue와 같은 값 표현이 정의되어 있다. 어쩌라고 ㅡ.ㅡ;;;

오른값 참조

일반적인 참조는 {형식}& 형태로 되어 있다.

int v1 = 10;
int& v2 = v1;

오른값 참조는 {형식}&& 형태로 되어 있다. 이는 오른값이기에 왼값 위치에서 사용할 수 없고, 함수 인자로 사용할 수 있다.

int inc(int& val) { // (1)
    val += 1;
    return val;
}

int inc(int&& val) { // (2)
    val += 1;
    return val;
}

int main() {
    int v1 = 0;
    int v2 = inc(v1); // (1) 호출
    int v3 = inc(1);  // (2) 호출

    return 0;
}

inc()에 왼값을 넘기는 경우 첫번째 함수(1)을 호출하고 오른값을 넘기면 두번째 함수(2)을 호출한다. 주의할 부분은 왼값 참조는 값 수정시 왼값도 수정되지만, 오른값은 수정되지 않는다.

valueof() 예제를 사용하면

    Box box;
    int v4 = valueOf(box); // (1)
    int v5 = valueOf();    // (2)

(1)은 왼값이 리턴되기에 첫번째 inc()를 호출하고 (2)은 오른값이 리턴되기에 두번째 inc()을 호출한다.

오른값 참조는 왜 필요하지?

왼값 참조는 C++을 사용하셨던 분들은 이미 익숙해서 잘 사용하고 있을 거라 생각합니다. 근데 오른값 참조를 굳이 왜 만들었을까? 오른값 참조는 대부분 값이 복제되는 경우가 많다. 물론 포인터를 사용하면 이런 복잡한 문제를 고려할 필요가 없을 수 있지만, 포인터 관리가 쉽지 않다. 최대한 참조를 활용하고 싶은 생각이 들 것이다(?).

함수 호출시 인자로 오른값을 넘기면 해당 값이 복제되서 넘어가게 된다. 리터럴 값이면 그나마 감수할 수 있지만, 구조체, 클래스 등인 경우 데이터가 커지면서 고민이 필요하게 된다. 이때 오른값을 참조로 받아서 처리하고 필요하면 이를 다시 참조로 리턴해서 사용하면 어떨까 하는 생각이 들 수 있다. 그러면 메모리 복제 작업이 줄어든다.

예를 보자.

struct BigData {
    long value = 100;
}

BigData& longProcess(BigData&& data) {
    data.value = data.value * data.value;
    return data;
}

int main() {
    BigData& data = longProcess(BigData());

    return 0;
}

매우 큰 데이터(가정)인 BigData은 한번만 생성되고 longProcess()로 오른쪽 참조로 넘겨진다. 그리고 처리하고 다시 참조로 리턴한다. BigData의 불필요한 메모리 복제 없이 처리하는 모습을 볼 수 있다. 위 예에서는 왼값에 대한 부분은 없지만, 반드시 해당 함수를 추가해줘야 한다.

BigData& longProcess(BigData& data) {
    data.value = data.value * data.value;
    return data;
}

왜 코드 중복이 보이지? 별도 함수로 다시 빼내야 되나? 아니면 보편 참조 형태로 바꿔야하나? …

결론

지금까지 오른값 참조를 살펴보았다. 만약 포인터를 사용하고 있다면 크게 신경쓸 필요가 없을 수 있다. 그러나 참조를 잘 활용하고 있다면 가뭄에 단비처럼 오른값 참조도 고려할만 하다. 그렇지만 모든 오른값에 대해 오른값 참조를 사용할지 고려해야하는 함정이 있다. 이는 auto나 템플릿으로도 확장된다. 생각할 부분이 2배가 아닌 몇 배가 늘어난다. 점점 복잡하고 어려워지는 느낌은 느낌적인 느낌인가?

실무에서는 얼마나 잘 활용되는지는 모르겠네요. 아무튼 부족한 저의 지식으로 정리한 내용이 여러분에게 도움이 되었으면 합니다. 모두 즐프하세요. ospace.

참조

[1] Value categories, https://en.cppreference.com/w/cpp/language/value_category

반응형

'3.구현 > C or C++' 카테고리의 다른 글

[c++] C++14 형식연역  (1) 2023.11.21
[c++] C++11 보편참조 사용하기  (0) 2023.11.20
protothreads 정리  (0) 2023.10.25
[c++] 함수자 구현 고찰  (0) 2023.10.18
openssl에서 nonblock socket으로 ssl 연결  (2) 2013.05.07