Program Language/C & C++

[C++] lvalue 참조

야곰야곰+책벌레 2021. 12. 29. 14:59
728x90
반응형

형식 이름에서 표기법 X&는 'X에 대한 참조'를 의미한다. lvalue에 대한 참조에 사용되므로 종종 lvalue 참조라고 한다.\

void f()
{
	int var = 1;
	int& r {var}; // r and var now refer to the same int
	int x = r; // x becomes 1
	r = 2; // var becomes 2
}

참조가 무언가에 대한 이름인지 확인하려면(즉, 객체에 바인딩됨)  참조를 초기화해야 한다.

int var = 1;
int& r1 {var}; // OK: r1 initialized
int& r2; // error : initializer missing
extern int& r3; // OK: r3 initialized elsewhere

참조 초기화는 할당과 다르다. 어떤 연산자도 참조에 대해 작동하지 않는다.

void g()
{
	int var = 0;
	int& rr {var};
	++rr; // var is incremented to 1
	int∗ pp = &rr; // pp points to var
}

여기서 ++rr은 참조 rr을 증가시키지 않는다. 오히려 ++는 rr이 참조하는 int, 즉 var에 적용된다. 따라서 초기화 후에 참조 값을 변경할 수 없다. 항상 나타내기 위해 초기화된 객체를 참조해야 한다. 참조 rr로 표시된 객체에 대한 포인터를 얻으려면 &rr을 쓸 수 있다. 다라서 참조에 대한 포인터를 가질 수 없다. 또한 참조 배열을 정의할 수 없다. 그런 의미에서 참조는 객체가 아니다.

 

참조의 명백한 구현은 사용될 때마다 역참조 되는 (상수) 포인터다. 참조가 포인터와 같은 방식으로 조작할 수 있는 객체가 아니라는 것을 기억하는 한 참조에 대해 그렇게 생각하는 것은 큰 해가 되지 않는다.

경우에 따라 컴파일러는 런타임에 해당 참조를 나타내는 객체가 없도록 참조를 최적화할 수 있다.

 

initializer가 lvalue(취할 수 있는 주소의 객체)인 경우 참조 초기화는 간단하다. "plain" T&의 initializer는 T 유형의 lvalue여야 한다. const T&의 initializer는 lvalue이거나 T 유형일 필요가 없다.

 

이러한 경우,

  1. 먼저 필요한 경우 암시적 유형 변환을 T로 적용한다.
  2. 그런 다음 결과 값은 유형 T의 임시 변수에 배치된다.
  3. 마지막으로 이 임시 변수는 초기화 값으로 사용된다.
double& dr = 1; // error : lvalue needed
const double& cdr {1}; // OK

이 마지막 초기화의 해석은 다음과 같다.

double temp = double{1}; // first create a temporar y with the right value
const double& cdr {temp}; // then use the temporar y as the initializer for cd

참조 initializer를 유지하기 위해 생성된 temporary는 참조 범위가 끝날 때까지 지속된다.

 

변수에 대한 참조와 상수에 대한 참조는 구별된다. 변수에 대한 temporary를 도입하면 오류가 발하기 쉬웠기 때문이다. 변수에 대한 할당은 (곧 사라질) temporary에 대한 할당이 될 것이다. 상수에 대한 참조에는 그러한 문제가 없으며 상수에 대한 참조는 종종 함수 인수로 중요하다. 참조를 사용하여 함수가 전달된 객체의 값을 변경할 수 있도록 함수 인수를 지정할 수 있다.

void increment(int& aa)
{
	++aa;
}
void f()
{
	int x = 1;
	increment(x); // x=2
}

인수 전달의 의미는 초기화의 의미로 정의되어 있으므로 호출 시 increment의 인수 aa는 x의 다른 이름이 된다. 프로그램을 읽을 수 있도록 유지하려면 인수를 수정하는 함수를 피하는 것이 가장 좋다. 대신 함수에서 값을 반환할 수 있다.

int next(int p) { return p+1; }
void g()
{
	int x = 1;
	increment(x); // x=2
	x = next(x); // x=3
}

increment(x) 표기법은 x=next(x)와 같이 x의 값이 수정되고 있다는 단서를 보는 사람에게 제공하지 않는다. 결과적으로 'plain' 참조 인수는 함수 이름이 참조 인수가 수정되었다는 강력한 힌트를 제공하는 경우에만 사용해야 한다.

참조는 반환 유형으로도 사용할 수도 있다. 이것은 할당의 왼쪽과 오른쪽 모두에서 사용할 수 있는 함수를 정의하는 데 주로 사용된다.

 

지도는 좋은 예다.

template<class K, class V>
class Map { // a simple map class
public:
	V& operator[](const K& v); // return the value corresponding to the key v
	pair<K,V>∗ begin() { return &elem[0]; }
	pair<K,V>∗ end() { return &elem[0]+elem.size(); }
private:
	vector<pair<K,V>> elem; // {key,value} pairs
};

표준 라이브러리 맵은 일반적으로 레드-블랙 트리로 구현되지만, 구현 세부 사항을 산만하게 하지 않기 위해 주요 일치에 대한 선형 검색을 기반으로 구현을 보여준다.

template<class K, class V>
V& Map<K,V>::operator[](const K& k)
{
	for (auto& x : elem)
		if (k == x.first)
		return x.second;
        
	elem.push_back({k,V{}}); // add pair at end (§4.4.2)
	return elem.back().second; // return the (default) value of the new element
}

복사 비용이 많이 드는 유형일 수 있으므로 키 인수 k를 참조로 전달한다. 마찬가지로 복사 비용이 많이 드는 유형일 수 있으므로 참조로 값을 반환한다. 그것을 수정하고 싶지 않고 literal 또는 임시 객체를 인수로 사용하고 싶을 수도 있기 때문에 k에 대해 const 참조를 사용한다. map의 사용자가 발견된 값을 수정하고 싶어 할 수 있기 때문에 non const 참조로 결과를 반환한다.

int main() // count the number of occurrences of each word on input
{
	Map<string,int> buf;
    
	for (string s; cin>>s;) ++buf[s];
    
	for (const auto& x : buf)
		cout << x.first << ": " << x.second << '\n';
}

입력 루프는 표준 입력 스트림 cin에서 문자열 s로 한 단어를 읽은 다음 연결된 카운터를 업데이트한다. 마지막으로 각각의 발생 횟수와 함께 입력에서 다른 단어의 결과 테이블이 인쇄된다.

aa bb bb aa aa bb aa aa

라고 입력하면

aa: 5
bb: 3

range-for 루프가 작동하는 이유는 Map이 begin()과 end()를 정의했기 때문이다.

728x90
반응형

'Program Language > C & C++' 카테고리의 다른 글

[C++] 참조에 대한 참조  (0) 2021.12.29
[C++] rvalue 참조  (0) 2021.12.29
[C++] 참조(references)  (0) 2021.12.29
[C++] pointer & 소유권  (0) 2021.12.29
[C++] Pointer & const  (0) 2021.12.29