{{ label!='' ? 'Label : ' : (q!='' ? '검색 : ' : '전체 게시글') }} {{ label }} {{ q }} {{ ('('+(pubs|date:'yyyy-MM')+')') }}

[C++] The C++ Programming Language: 7. Pointers, Arrays, and References (3) - Rvalue References, References to References

 7.7.2 Rvalue References

  한 종류 이상의 참조들에 대한 기본적인 아이디어는 서로 다른 객체의 사용법들을 지원하기 위함입니다.

  • non-const lvalue 참조는 객체를 참조하며, 객체는 사용자가 작성할 수 있는 것입니다.
  • const lvalue 참조는 상수를 참조하며, 상수는 참조의 사용자 관점에서 불변한 것입니다.
  • rvalue 참조는 임시 객체를 참조하며, 이는 참조의 사용자가 다시는 사용하지 않을 것이라고 생각하고, (일반적으로)변경할 수 있는 객체를 참조하는 것입니다.

  우리는 참조가 정말 임시로 참조하는지를 알고 싶습니다. 만약 그렇다면, 때때로 비용이 비싼 복사 연산 대신 저렴한 이동 연산으로 전환할 수 있을 것이기 때문이죠. 잠재적으로 엄청난 양의 정보를 가리키는(pointing) 작은 기술자(descriptor)에 의해 제공되는 객체는, 만약 그것이 더이상 사용되지 않을 것을 안다면 단순하고 저렴하게 이동될 수 있을 것입니다. 전형적인 예로 반환 값을 들 수 있습니다. 반환되는 지역 변수는 결코 다시 사용되지 않을 것임을 컴파일러가 알기 때문이죠.

  rvalue 참조는 rvalue에 바인딩 될 수 있으나, lvalue에는 될 수 없습니다. 따라서, rvalue 참조는 lvalue 참조의 정반대에 있는 것입니다:

1
2
3
4
5
6
7
8
9
10
11
12
string var {"Cambridge"};
string f();

string& r1 {var}; // lvalue reference, bind r1 to var (an lvalue)
string& r2 {f()}; // lvalue reference, error : f() is an rvalue
string& r3 {"Princeton"}; // lvalue reference, error : cannot bind to temporary

string&& rr1 {f()}; // rvalue reference, fine: bind rr1 to rvalue (a temporary)
string&& rr2 {var}; // rvalue reference, error : var is an lvalue
string&& rr3 {"Oxford"}; // rr3 refers to a temporary holding "Oxford"

const string cr1& {"Harvard"}; // OK: make temporary and bind to cr1
cs

  && 선언 연산자는 "rvalue 참조"를 의미합니다. 우리는 const rvalue 참조를 사용하지 않습니다. rvalue 참조를 활용으로부터 얻어지는 대부분의 이점은 참조하는 객체에 무언가를 쓰는 것에 있습니다. const lvaule 참조와 rvalue 참조는 rvalue로 바인딩될 수 있습니다. 하지만, 그 목적은 분명 다르죠:

  • rvalue 참조는 복사가 필요한 것들의 최적화를 위한 "destructive read"를 구현합니다.
  • const lvalue 참조는 인자의 변경을 예방하기 위해 사용합니다.

  rvalue 참조에 의해 참조된 객체는 lvalue 참조 혹은 원래의 변수 이름으로 참조된 객체와 비슷하게 접근됩니다:

1
2
3
4
5
6
string f(string&& s)
{
    if (s.size())
        s[0] = toupper(s[0]);
    return s;
}
cs

  때때로, 프로그래머는 한 객체가 더이상 사용되지 않을 것임을 알지만, 컴파일러는 모를 때가 있습니다:

1
2
3
4
5
6
7
template<class T>
swap(T& a, T& b) // "old-style swap"
{
    T tmp {a}; // now we have two copies of a
    a = b; // now we have two copies of b
    b = tmp; // now we have two copies of tmp (aka a)
}
cs

  만약 T의 타입이 복사에 큰 비용이 드는 타입이라고 생각해보죠. string이나 vector와 같은 것들입니다. 이 경우 swap()은 무거운 연산이 됩니다. 이는 고민이 필요한 부분이죠: 우리는 어떤 복사도 원하지 않았습니다. 그저 a와 b의 값을 옮기고 싶었을 뿐이죠. 우리는 이를 컴파일러에게 이야기할 수 있습니다:

1
2
3
4
5
6
7
template<class T>
void swap(T& a, T& b) // "perfect swap" (almost)
{
    T tmp {static_cast<T&&>(a)}; // the initialization may write to a
    a = static_cast<T&&>(b); // the assignment may write to b
    b = static_cast<T&&>(tmp); // the assignment may write to tmp
}
cs

  static_cast<T&&>(x) 값의 결과는 x의 T&& 타입의 rvalue입니다. rvalue들을 위해 최적화된 연산은 x를 위한 최적화에 사용할 수 있게 됩니다. 특히, 만약 T 타입이 이동 생성자 혹은 이동 할당자를 가지고 있다면, 이들이 사용될 것입니다. vector를 보죠:

1
2
3
4
5
6
7
8
9
template<class T> class vector {
    // ...
    vector(const vector& r); // copy constructor (copy r’s representation)
    vector(vector&& r); // move constructor ("steal" representation from r)
};

vector<string> s;
vector<string> s2 {s}; // s is an lvalue, so use copy constr uctor
vector<string> s3 {s+"tail"); // s+"tail" is an rvalue so pick move constructor
cs

  swap()에서 사용되는 static_cast는 꽤나 난잡하고 오타를 내기 쉽습니다. 따라서 라이브러리는 move() 함수를 제공하죠: move(x)는 static_cast<X&&>(x)를 의미하며, X는 x의 타입입니다. 이를 이용해서 swap()을 좀 더 깔끔하게 정리할 수 있죠:

1
2
3
4
5
6
7
template<class T>
void swap(T& a, T& b) // "perfect swap" (almost)
{
    T tmp {move(a)}; // move from a
    a = move(b); // move from b
    b = move(tmp); // move from tmp
}
cs

  기존의 swap()과는 대조적으로, 이 최신 버전은 어떠한 복사본도 만들지 않습니다. 가능한 한 move 연산자를 활용할 것이죠.

  move(x)는 x를 이동시키지 않으므로(단순히 x에 대한 rvalue 참조를 생성할 뿐이죠), move()는 rval()로 불리는 것이 더 나았겠지만, 어쨌거나 수년 동안 잘 사용되고 있습니다.

  swap()을 "거의 완벽"하다고 이야기한 이유는, 오로지 lvalue만을 교환하기 때문입니다. 다음을 참고해보죠:

1
2
3
4
5
void f(vector<int>& v)
{
    swap(v,vector<int>{1,2,3}); // replace v’s elements with 1,2,3
    // ...
}
cs

  특정 기본 값이 담긴 컨테이너로 내용물을 교체하는 것은 종종 일어나는 일입니다. 하지만 이 swap()은 이를 수행할 수 없죠. 이를 해결하기 위해서는 오버로딩을 두 번 해야 합니다:

1
2
template<class T> void swap(T&& a, T& b);
template<class T> void swap(T& a, T&& b);
cs

  이렇게 하면 해결할 수 있을 것입니다. 표준 라이브러리는 shrintk_to_fit()과 clear()를 vector, string 등에 구현함으로써 다른 해결책을 이야기합니다:

1
2
3
4
5
6
7
8
void f(string& s, vector<int>& v)
{
    s.shrink_to_fit(); // make s.capacity()==s.size()
    swap(s, string{s}); // make s.capacity()==s.size()
    v.clear(); // make v empty
    swap(v, vector<int>{}); // make v empty
    v = {}; // make v empty
}
cs

(s와 v를 조작하는 각 코드가 결과적으로 동치이며, swap() 대신 이와 같은 방식을 활용할 수 있다는 의미입니다-옮긴 이)

  rvalue 참조는 perfect frowarding에도 이용될 수 있습니다. (차후 기술)

  모든 표준 라이브러리 컨테이너는 이동 생성자와 이동 할당자를 제공합니다. 또한 insert(), push_back()과 같은 삽입 연산들은 rvalue 참조를 이용하는 버전들도 구현되어 있습니다.


7.7.3 References to References

  특정 타입의 참조의 참조를 얻는다고 생각해보죠. 참조형의 참조라는 특별한 것을 얻는 것 대신, 해당 타입의 참조를 얻을 것입니다. 하지만 어떤 종류의 참조를 얻죠? lvalue 참조일까요, 혹은 rvalue 참조일까요? 다음을 고려해보세요:

1
2
3
4
5
6
using rr_i = int&&;
using lr_i = int&;
using rr_rr_i = rr_i&&; // "int && &&" is an int&&
using lr_rr_i = rr_i&; // "int && &" is an int&
using rr_lr_i = lr_i&&; // "int & &&" is an int&
using lr_lr_i = lr_i&; // "int & &" is an int&
cs

  쉽게 말하면 lvalue 참조는 언제나 승리합니다. 이는 의미가 통하죠: lvalue 참조는 lvalue를 참조한다는 사실을 바꿀 수는 없습니다. 이는 reference collapse라고 부르기도 합니다.

  따라서 다음은 허용되지 않습니다:

1
int && & r = i;
cs

  참조의 참조는 alias나 템플릿 타입 인자의 결과를 일으킬 뿐입니다.

댓글

이 블로그의 인기 게시물

[코딩의탑] 4층: 툰 쉐이딩

[코딩의탑] 3층: 바다 렌더링

[코딩의탑] 5층: 포탈(Portal), 더 나아가기