프로그래밍 지식/Effective Modern C++

Effective Modern C++ 값의 종류

게임이 더 좋아 2022. 5. 14. 20:20
반응형
728x170

 

 

이번에 Effective Modern C++ 책을 오직 나의 만족을 위해서 공부해보기로 하였다.

근데 뭐 읽는다고 내가 개발 실력이 급격하게 늘 것 같지는 않고 그냥 후두러 까보자.

까다보면 잘해지겠지..

시작해보자

 


 

C++에서는 코드를 써놓은 것을 표현식(expression)말하는데 자주 쓰는 용어이니 기억하자.

아무튼 표현식은 2가지를 가진다.

 

1.Type (자료형)

2. value category(값의 종류)

 

?? 엥 종류가 타입 아니냐???

물론 비슷하지만 다르다.

 

책에서도 다르게 분류하는데

  • 타입: int, int*, string, const int&, int[], ... 등

우리가 평소에 알고 있는 타입 그대로다.

 

  • 값의 종류: lvalue, prvalue, xvalue, rvalue, glvalue

?? 우리가 처음보거나 익숙하지 않은 단어들이다.

** 단 lvalue reference나 rvalue reference는 타입을 말하는 것이다. 이것은 값의 종류가 아니다.

다시 말해서 lvalue가 들어갔다고 해서 그것이 값의 종류가 아니란 말이다.

 

 

값의 종류를 구분하는 것이 중요하다. 

이것에 따라 써야하는 방식이 달라지기 때문이다.

즉,  lvalue와 rvalue를 구분하는 것이 무엇보다도 중요하다.

어차피 모든 표현식은 lvalue 아니면 rvalue로 나누어진다.

 

하지만 왜 modern C++에서는 lvalue와 rvalue 구분이 중요한가?

또한 rvalue reference와 같은 타입이 추가시켜서 나를 힘들게 하는가?

 

쉽게 말하자면

lvalue는 '다시 쓰일 수도 있으니 망가뜨리면 안되는 것 => 원본 유지

rvalue는 '다시 쓰이지 않을 것이니까 망가뜨리거나 비워버려도 괜찮은 것' => 복사해서 쓸 건데?

 

lvalue나 rvalue에 따라 사용방식이 달라지는 것을 알 수 있다.

lvalue와 rvalue를 구분하는 이유는 rvalue 안에 있는 내용물은 다른 곳으로 옮겨 버려도 괜찮다는 전제가 있어서 그렇다. 

다시 말해서 어떤 표현식이 rvalue라는 것은 어차피 그것이 다시 쓰이지 않을 것이란 뜻이다.

그러므로 그걸 어딘가로 전달할 때 굳이 복사할 필요 없이 안에 있는 내용물을 옮겨 버려도 된다는 것이다.

 

예를 들어 함수를 만들 때

함수가 어떤 매개변수를 rvalue reference로 받도록 선언되어 있으면

여기에는 rvalue만을 넣어줄 수 있다.

 

함수가 이렇게 rvalue reference를 매개변수로 받는다는 것은

이것의 내용물을 쓰겠다는 뜻이다.

즉, 어떤 entity를 전달받은 함수의 입장에서 이것을 다시 안 쓰일 녀석 취급해도 되는지

아니면 함부로 망가뜨리면 안되는지를 판단하기 위해 rvalue reference와 lvalue reference를 구분하여 받는 것이다.

 

물론 lvalue라 해도 어떤 lvalue는 실제로 코드에서 다시 안 쓸 수도 있지만

그걸 lvalue reference로 받은 입장에선 이것이 나중에 다시 쓰일지 어떨지 모르니 함부로 파괴하면 안되는 것이다.

 

** rvalue reference의 의미는 다른 사람이 더 잘 설명해놓았다. 링크

 


 

하지만 여전히 남은 의문... 어떻게 구분하는데???

 

[lvalue]

lvalue는 원래 등호의 좌측에 올 수 있는 값이란 뜻이었다.

그것은 지금도 대략 맞지만 정확하진 않다. 

 

const 변수, 문자열 literal, 함수 이름 등은 등호의 좌측에 올 수 없지만 lvalue이기 때문이다.

lvalue가 무엇인지 여러 가지 방법으로 설명할 수 있다.

 

=> 나같은 경우는 방법을 섞어서 판단한다. (빨간색)

 

  • 방법 1: 대략적으로 추측
    • 이름이 있으면 lvalue라고 할 수 있다. (여기서 이름은 alias 같은 것을 말한다)
    • 주의: a[0], a+=1, ++a, a.x, ... 모두 lvalue일 가능성이 높다.
    • (a++는 조금 다르다. 이것은 a의 사본을 임시 객체로 리턴하고 a를 1 증가시키는 함수와 마찬가지다. - 참고 링크).
    • 주의: 이름이 없어도 lvalue인 것들이 있다 (예: "abc"같은 문자열 리터럴)

 

 

  • 방법 2: 쉽게 판단하는 방법
    • & 연산자로 주소를 취할 수 있으면 lvalue
    • 타입이 lvalue reference인 표현식은 lvalue  
    • static_cast<int&>(i)  // 이것도 lvalue
    • int& f() { return global_var; }
      int main(...) {
         f() = 100;  // 여기의 f()는 lvalue
    • 나머지는 rvalue

 

  • 방법 3: 또 다른 방식으로 생각하기 
    • 어떤 코드 라인에 어떤 표현식이 있는데, 그 표현식이 가리키는 대상(entity)을 그 다음 라인에서도 다시 액세스할 수 있으면, 그 표현식은 lvalue다. (주의: 문자열 literal, [a++]).
    • 단, rvalue reference를 리턴하는 함수 호출 및 rvalue reference로의 타입 캐스팅 제외 (예: std::move(...) 는 rvalue)

 

 

  • 예를 들어서 아래에서 f()와 100의 값의 종류는 각각 무엇일까?
    int& f() { return global_var; }
    int main(...) {
       f() = 100;  // global_var에 100을 넣어보자.
       // 이제 여기서 다시 f()라고 쓰면, 윗줄에서 f()가 나타냈던 그 entity를 정확히 다시 액세스한다. 그러므로 f()는 lvalue다.
       // 그러나 여기서 100이라고 쓰면, 그것은 윗윗줄의 100이라는 표현이 나타내는 entity를 그대로 다시 액세스하는 것이 아니다. 그러므로 100은 rvalue다.
  • 주의
    • 문자열 literal을 똑같이 다시 쓰면(예를 들어 코드의 어떤 줄에서 "abc"라 한 뒤 다음 줄에서 또 "abc"라 하면), 동일한 entity를 액세스하게 되는 것이 맞다. 문자열 literal은 배열이고, 프로세스가 살아있는 한 메모리 특정 영역에 보존된다 (참고링크). 그러므로 문자열이 lvalue인 것이 이상하지 않다.
    • a++는 a의 사본을 임시객체로 리턴하고 a를 1 증가시키는 함수와 마찬가지다. 그 임시 객체는 (다른 변수에 담지 않는 한) 다음 줄에서 다시 액세스 불가능하다. 그러므로 a++는 rvalue다. (참고링크)

 

[rvalue]

rvalue는 원래 등호의 우측에만 올 수 있는 값이란 뜻이었다.

const 변수, 문자열 literal 등은 등호의 우측에만 올 수 있지만 lvalue이기 때문이다.

다시 말하면 lvalue가 아니면 다 rvalue다.

 

참고로 rvalue는 prvalue와 xvalue로 나눌 수 있다

 

  • prvalue(Pure rvalue)
    • 상수 literal (단, 문자열 literal 제외) ex) int a  = 42  에서 42
    • 다시 접근할 수 없는 임시 객체

 

  • xvalue(eXpiring value, 즉 곧 없어질 값이란 뜻)
    • rvalue reference를 리턴하는 함수를 호출하는 표현식
    • rvalue reference로 형변환(type casting)하는 표현식 (std::move를 호출하는 표현식도 이에 해당)

 

 

std::move는 실제로 자료를 옮기지도 않고 아무 액션도 하지 않는다.

std::move는 단지 주어진 표현식을 rvalue reference 타입으로 타입 캐스팅한다.

이렇게 타입 캐스팅한 결과는 rvalue이기 때문에 rvalue reference를 받아들이는 함수가 바인딩된다.

** 그런 함수들은 주어진 entity가 다시 쓰이지 않을 거라 생각한다고 앞에서 말했다.

결국 std::move에 뭔가를 넣고 이걸 어딘가로 넘겨준다는 것은

그 entity는 내부의 자료들이 다시 쓰이지 않을 entity처럼 취급하겠다는 것이다.

**해당 작업 내용을 이어서 쓰지 않는다는 것이다.

 

 

 


 

참고 내용

 

더보기

[glvalue]

lvalue와 xvalue를 묶어 glvalue(Generalized lvalue)라고 한다.

[Scott Meyers의 rule of thumb]

  • 어떤 표현식의 주소를 얻을 수 있으면, 그 표현식은 lvalue다.
  • 어떤 표현식의 타입(자료형)이 lvalue reference (예를 들어 T& 또는 const T& 등) 이면, 그 표현식은 lvalue다.
  • 그렇지 않으면, 그 표현식은 rvalue다.
    • 임시 객체들(함수가 리턴한 임시 객체들, 또는 implicit한 type casting 등으로 생성된 임시 객체들)
    • 상수 리터럴들(10 또는 5.3 등)

 

[Bjarne Stroustroup의 구분] => C++ 창시자가 만든 방법

Bjarne Stroustroup은 has identity 여부와 can be moved from 여부에 따라 값의 종류들을 구분했다(링크).

can be moved from (rvalue)canNOT be moved from

has identity (glvalue) xvalue lvalue
NOT have identity prvalue (not useful)

 

 

Identity란? (참고 링크)

  • 어떤 표현식이 있을 때, 이 표현식과 다른 표현식이 같은 entity를 가리키는지를 (예컨대 그것들의 주소 등을 비교해서) 판단할 수 있으면, 이 표현식은 identity가 있다고 한다.

 

 


 

 

어떤 표현식이 lvalue인지 rvalue인지 잘 모르겠으면 아래 링크에 몇가지 예시가 있다.

cppreference.com의 'Value categories'

 

 


 

그래도 잘 모르겠으면, 어떤 표현식이 lvalue인지 rvalue인지를 코드를 작성하여 확인할 수 있다.

여기 나온 방법을 시도해보자

728x90
반응형
그리드형