C++Builder  |  Delphi  |  FireMonkey  |  C/C++  |  Free Pascal  |  Firebird
볼랜드포럼 BorlandForum
 경고! 게시물 작성자의 사전 허락없는 메일주소 추출행위 절대 금지
분야별 포럼
C++빌더
델파이
파이어몽키
C/C++
프리파스칼
파이어버드
볼랜드포럼 홈
헤드라인 뉴스
IT 뉴스
공지사항
자유게시판
해피 브레이크
공동 프로젝트
구인/구직
회원 장터
건의사항
운영진 게시판
회원 메뉴
북마크
볼랜드포럼 광고 모집

자유게시판
세상 살아가는 이야기들을 나누는 사랑방입니다.
[29098] Re:Re:Re: Value Category에 대한 공부가 되어있지 않은 듯...
빌더(TWx) [builder] 2174 읽음    2022-03-27 01:34
RAD 11.1 님이 쓰신 글 :
: 앗 엄청난 고수이신 C++빌더님이 답변해주시다니 영광입니다
:
: 링크해주신 사이트는 열심히 읽어봤지만 제 실력으론 도져히 이해가 안되네요
: 제가 알고있기론 "abcd" 같은 스트링 리터럴은 상수라서 당연히 rvalue 라야 할거 같은데
: 그 사이트에선 스트링 리터럴이 lvalue라고 하던데 이유를 모르겠습니다.
:
: a = ++i; 는 lvaue고, a = i++; 은 rvalue라고 하는 것도 이해가 안되고요
: 당영히 컴파일 되야 할거 같은데 pair에서 레퍼런스 붙이면 왜 컴파일 에러가 나는지도 도체 모르겠습니다
:
: C++빌더님 지식 좀 베풀어 주십시요. 부탁드립니다 ㅠㅠ
:
:
:
:
: 빌더(TWx) 님이 쓰신 글 :
: : RAD 11.1 님이 쓰신 글 :
: : : RAD 스튜디오 11.1이 새로 나왔다고 하길래 기존 버전 싹 밀어 버리고 설치해봤습니다.
: : : 근데 황당한? 에러가 발생하네요?
: : :
: : : #include <iostream>
: : : #include <tchar.h>
: : : #include <map>
: : : #include <string>
: : :
: : : using namespace std;
: : :
: : : int _tmain(int argc, _TCHAR* argv[])
: : : {
: : :     map<string, int> map;
: : :
: : :     map["num 1"] = 1;
: : :     map["num 2"] = 2;
: : :     map["num 3"] = 3;
: : :
: : :     for(pair<string, int>& p: map)
: : :         cout << p.first << endl;
: : :
: : :     cout << "end" << endl;
: : : }
: : :
: : : 쓸데없이 카피가 되지않도록 레퍼런스로 선언하면 에러가 나버리네요
: : :
: : : 근데 웃긴게 다음처럼
: : :
: : :     for(pair<string, int>p: map)
: : :         cout << p.first << endl;
: : :
: : : 레퍼런스 빼고하면 에러가 안나긴 합니다만 쓸데없이 카피가 되겠죠
: : : 뭐 이런 황당한 에러가 있을까요???
: : :
: : :
: :
: :
: : 답변:
: :
: :
: : 하나도 황당한 게 아니고 정상적인 겁니다.
: :
: : 해외 사이트 글들을 줏어다가... 번역기 돌려서 한글로 번역해서
: : 마치 자신이 집필한 글 처럼 짜집기 해서 황당하게 소설을 써 놓은 경우가 허다하니...
: :
: : 국내 한글 사이트 검색하면서 시간낭비 하지 말고...
: : 아래 공신력 있는 사이트에서 영어 원문 그대로를 이해될 때 까지 정독해서 읽어 보세요
: :
: : https://en.cppreference.com/w/cpp/language/value_category
: :
: : Value Category에 대한 공부가 되어있지 않은 듯...
: :



답변:


Value Category에서 expression(표현식)의 정의는 리터럴, 변수 자체, 그리고... + , *, -> 등의 오퍼레이터와 오퍼랜드가 결합된
모든 것을 포함하는 개념입니다.

좌측에 있으면 lvalue라고 설명하는 국내 한글 사이트들의 설명은 해외 사이트 글을 어설프게 번역해서 엉터리로 소설 써 놓은 거고...


int iv = 3; 이라는 표현식이 있다고 해 봅시다.

여기서 iv는 4 바이트 크기의 storage를 차지하는(스택이든 힙이든) 오브젝트죠.
lvalue 라는 것은 오브젝트를 특정할 수있는 identity(이름: 이 경우 iv)를 가지고 있고,
오브젝트의 주소를 반환하는 어드레스 오퍼레이터인 '& operator'의 오퍼랜드로 취할 수 있는 경우를 말합니다.

int iv = 3; 이라는 표현식에서...

4 바이트 크기의 storage를 차지하는 오브젝트는... iv라는 identity를 갖고 있고,

int* piv = &iv; 와 같이...

& operator로 오브젝트의 주소를 특정할 수 있기 때문에 lvalue 입니다.
그와 달리 '3' 이라는 표현식은 '&3'으로 주소를 특정할 수 없기 때문에 rvalue 입니다. (정확하게는 prvalue: pure rvalue)




질문에 언급되어 있는 내용들을 하나씩 풀어 봅시다.


1.

a = ++i; 는 lvaue고, a = i++; 은 rvalue 인 이유.


int i = 3;

int a = ++i; // i 자체는 lvalue, ++i도 lvalue
int b = i++; // i 자체는 lvalue, i++은 rvalue

표현식 ++i 는... i 값을 먼저 증가해서, 증가된 i 값 자체를 반환하는 pre - increment 연산이므로
'++i'라는 표현식은 lvalue 이지만...

표현식 i++ 는 얘기가 달라 집니다.

post - increment 연산자는 값이 증가되기 이전의 값을 반환하고, 후에 값을 증가 시키기 때문에
i++ 표현식에 대해서... 컴파일러가 생성하는 코드를 다음과 같이 슈도 코드로 표현해 보면...

{
  int temporary = i;
  i = i + 1;
  return temporary;
}

증가되기 이전의 값을 반환하기 위해 내부적으로 temporary를 생성해서 그 값을 b로 반환하는 코드를 생성하게 됍니다.

이와 같은 temporary는 오브젝트의 주소를 특정할 수 없기 때문에 lvalue가 아닌, prvalue 입니다.


++++i; 는 컴파일 되는 반면에... i++++; 는 왜 컴파일이 안될까... 를 이해할 수 있다면 C++ 초급 수준은 넘어선 것.




2.

"abc" 라는 스트링 리터럴이 rvalue가 아니고 lvalue 인 이유.

스트링 리터럴이 상수라 rvalue 라야 할것 같다는 것은 잘못된 생각 입니다.


const int ci = 3; 이라는 표현식이 있다고 해 봅시다.

여기서 표현식이 const 상수로 되어 있다고해서 ci가 rvalue 일까요? 아닙니다.

printf("%p\n", &ci); 와 같이 오브젝트의 주소를 특정할 수 있기 때문에...
ci는 4 바이트 크기와 ci라는 identity(이름)를 갖고있는 lvalue 이고

const 가 qualify로 지정되어 있어서 non-modifiable lvalue 가 됍니다.


스트링 리터럴도 마찬가지 입니다.

printf("%p\n", &"abc"); // 주소를 특정할 수 있다.

const char c = "abc"[1]; // 배열처럼 인덱싱도 가능.



사실상 컴파일러는 "abc" 라는 스트링 리터럴을

const char arr[4] = { 'a', 'b', 'c', 0 };

와 같이 null terminated 값 0 을 포함하는 배열로 취급함. (size = 4)


int arr[4]; 라는 배열이 있을 때... 컴파일러는 배열의 첫번째 요소를 가르키는 int* 로 취급하기도 하는데..

이걸 컴파일러에 의해서 배열 타입이 포인터 형으로 decay 된다고 표현 하기도 하죠.

arr[1] 은 *(arr + 1) 과 등가 임.

const char c = "abc"[1]; 는 ... const char c = *("abc" + 1) 과 등가 임.

따라서... 스트링 리터럴은 rvalue 가 아니고, lvalue 입니다.



3.

for (pair<string, int>& p : map) 에서 컴파일 에러가 발생하는 이유.


map은 Key와 Value가 한 쌍인... pair로 element가 구성되기 때문에


int _tmain(int argc, _TCHAR * argv[])
{
  map<string, int> map;

  map["num 1"] = 1;
  map["num 2"] = 2;
  map["num 3"] = 3;

  for (pair<string, int>& p : map)
    cout << p.first << endl;

  cout << "end" << endl;
}

위와 같은 코드가 정상적으로 컴파일 될 것 같아 보이지만 아닙니다.


map은 탐색 속도를 빠르게 하기 위해 binary tree나 red black tree 알고리즘으로 구현되어 있기 마련이고
요즘은 worst-case 시에도 탐색 속도를 향상 시키기 위해 red black tree로 구현하는 게 추세 입니다.

linear search는 element가 10,000 개 있다면... 최악의 경우 10,000 개를 다 확인해야 하지만...
element가 sort 되어 있으면 바이너리 서치로 빠른 탐색이 가능하기 때문에 map<>이 red black tree로 구현되어 있는 거죠.

이와 달리 unordered_map<>은 tree가 아닌, hash 알고리즘으로 인덱싱을 하기 때문에 sort 되어 있을 필요가 없고
collision이 일어나지 않게 hash function이 잘 구현되어 있고 bucket이 충분한 공간을 갖고있으면...
아무리 데이타가 많아도 탐색 시간은 항상 상수 값으로 일정하게 더 빠른 탐색이 가능 합니다.


map<>을 구현할 때... Key는 tree node 에서 Value에 대한 인덱싱 역할 밖에 하지 않기 때문에

typedef pair<const Key, Value> value_type;

와 같이 element인 pair<>의 Key 타입을 const 로 read only로 지정해서 라이브러리를 구현 합니다.

Key를 함부로 변경하게 허용하면... tree가 sort된 상태를 유지하도록 하기 위해 불필요하게 rebalancing 이 일어나야 하기 때문이죠.

그런 연유로 map<>의 element인 pair<>의 Key 타입을 const 로 지정하는 게 클래스 디자인 패턴에 맞는 겁니다.
const 로 지정하지 않고 구현했다면... 그건 클래스를 엉터리로 구현해 놓은 거죠.



자... 이제 컴파일 에러가 발생하는 이유를 짚어 봅시다.


for (pair<string, int>& p : map)

위 구문에서...

map<>의 element인 pair는 pair<const Key, Value> 타입을 갖고있는데...
range-for 로 지정되어 있는 구문에선 p가 pair<string, int> 로 지정되어 있기 때문에

type mismatch가 발생해서 컴파일러는 implicit type conversion을 시도하게 되고..
그 결과 temporary 오브젝트를 생성해서 반환하게 됍니다.

그리고 앞서 언급 햇듯이 temporary는 prvalue 이므로...

lvalue reference 인 p 에... lvalue 가 아닌, rvalue 를 바인드 하는 결과가 되어 컴파일 에러가 발생하는 겁니다.



만약 컴파일러에서 temporary로 바인드 되는 것을 허용했다면...

레퍼런스를 이용해서 temporary 변경이 가능하게 되는 거고, 그렇게 되면 바인드된 반환 값과, 원래의 값이 달라지게 되어
reference는 대상 오브젝트의 alias 라는 언어의 정의가 한순간에 깨지게 되겠죠.


그런데... 이런 경우를 생각해 봅시다.

만약 반환된 temporary에 대한 값을 변경하지 않고 사용한다는 전제 조건이 주어진다면...
반환된 temporary와 원래의 값이 같게 유지될 수 있기때문에 언어의 정의를 깨뜨릴 일은 일어나지 않겠죠 ?


따라서... 이 조건에 맞도록 다음과 같이 p를 const reference 로 바꿔주면 정상적인 컴파일이 가능해 집니다.

for (const pair<string, int>& p : map)


temporary 값을 변경하지 않는 조건이 주어지면 컴파일러가 허용하도록 하겠다는 건데
rvalue move semantic rule 이라는 컨셉이 도입되기 이전에...

lvalue reference가 rvalue 로 바인드 될 수 있는 예외적 경우이기도 합니다.


그러나... const lvalue reference로 지정해서 컴파일은 정상적으로 할 수 있게 되지만...
type mismatch 로... 컴파일러에 의한 implicit type conversion 으로 인해서 불필요하게 temporary 가 생성되기 때문에

다음과 같이 컴파일러가 알아서 type deduction 을 추론하도록 해주면

for (auto& p : map)

type mismatch로 인해 불필요하게 temporary가 생성되는 것을 피할 수 있게 되지요.



말 나온 김에 한가지 더 다루어 봅시다.


"const pair<string, int>&p" 에서...

p가 lvalue reference 이니까, lvalue 로만 바인드 되어야 reference semantic rule 에 맞는 게 아닌가? 생각할 수 있겠죠?



https://en.cppreference.com/w/cpp/language/value_category

위 사이트에서 디테일하게 설명해주고 있는 Value Category를 그림으로 도식화 해보면 다음과 같습니다.

< C++17 에서의 Value Category > C++20에서도 똑같이 적용 됌.



델파이 파스칼 같은 단순한 언어는 lvalue와 rvalue 2개의 간단한 Value 체계로 그만 이지만(컴파일러 만들기도 쉽다)
C++언어는 코드 최적화를 위한 move semantic rule을 지원하기 위해 위와 같이 세분화된 Value Category를 갖고 있지요.

언어의 겉모습만 보고 델파이 파스칼과 C++은 동급의 언어라고 황당한 소리를 하는 사람 들도 있던데...
언어를 디테일하게 모르고 있는 사람들의 헛소리 일 뿐이죠.(델파이 파스칼은 랭귀지 스펙이 간단해서 초보자들이
쉽게 사용할 수 있는 언어에 불과 함)


위의 Value Category를 도식화 한 그림을 보면...

prvalue 에서 xvalue 로 전이되는 표현을 볼수 있죠?


이와 같이 prvalue(temporary)에서 xvalue로 conversion 되는 경우를...
temporary materialization 이라고 하는데...

그림에서 보는 바와 같이...
xvalue는 glvalue의 범주에 포함되기 때문에...

const T& = rvalue; // prvalue -> xvalue -> glvalue ; lvalue;

문법적 정의가 깨지지 않게 되는 겁니다.


temporary materialization conversion이 일어나지 않는 경우도 있는데...
이해하는 데에 혼동이 있을 거 같아 여기서는 생략 함.


Value Category는...

코드 최적화를 위한 rvalue move semantic rule을 이해하기 위해 사전적으로 필히 마스터 하고 있어야 할 중요한 개념 입니다.









+ -

관련 글 리스트
29094 RAD 스튜디오11.1 새로 나왔다해서 깔아봤는데 황당한 에러가? RAD 11.1 1786 2022/03/21
29095     Re: Value Category에 대한 공부가 되어있지 않은 듯... 빌더(TWx) 1853 2022/03/23
29097         Re:Re: Value Category에 대한 공부가 되어있지 않은 듯... RAD 11.1 1935 2022/03/23
29098             Re:Re:Re: Value Category에 대한 공부가 되어있지 않은 듯... 빌더(TWx) 2174 2022/03/27
Google
Copyright © 1999-2015, borlandforum.com. All right reserved.