C ++ 11에서 T && (더블 앰퍼샌드)는 무엇을 의미합니까?


799

나는 C ++ 11의 새로운 기능 중 일부를 살펴 보았으며 내가 주목 한 것은 이중 앰퍼샌드와 같은 변수를 선언하는 것 T&& var입니다.

처음에,이 짐승은 무엇입니까? Google에서 이와 같은 구두점을 검색 할 수 있기를 바랍니다.

정확히 무엇을 의미합니까?

언뜻보기에는 이중 참조 인 것처럼 보이지만 (C 스타일 이중 포인터와 같은 T** var) 유스 케이스를 생각하는 데 어려움을 겪고 있습니다.


55
앞으로 더 많이 올 것이라고 확신하기 때문에 이것을 C ++-faq에 추가했습니다.
GManNickG


41
당신은 구글을 ​​사용하여 이것을 검색 할 수 있습니다, 당신은 따옴표로 문구를 포장해야합니다 : google.com/#q="T%26%26 "이제 첫 질문으로 질문이 있습니다. :)
sbi

비슷한 질문에 대한 매우 좋고 이해하기 쉬운 답변이 여기 있습니다. stackoverflow.com/questions/7153991/…
Daniel

2
Google의 최상위 검색에서 "c ++ two ampersands parameter"에 대한 세 가지 stackoverflow 질문이 있으며 귀하의 첫 번째 질문입니다. 따라서 "two ampersands parameter"를 철자 할 수 있으면 문장 부호를 사용할 필요조차 없습니다.
sergiol

답변:


668

rvalue 참조 (표준 제안서)를 선언합니다 .

다음은 rvalue reference에 대한 소개 입니다.

다음은 Microsoft의 표준 라이브러리 개발자 중 한 명이 제공하는 rvalue 참조에 대한 환상적인 심도 입니다 .

주의 : MSDN의 링크 된 기사 ( "Rvalue 참조 : VC10의 C ++ 0x 기능, 2 부")는 Rvalue 참조에 대한 매우 명확한 소개이지만, C ++ 11 초안에서 한때 사실이었던 Rvalue 참조에 대한 설명을 제공합니다. 표준이지만 마지막 것은 사실이 아닙니다! 특히, 그것은 여러 가지 점에서 rvalue 참조가 lvalue에 바인딩 될 수 있다고 말하는데, 이는 한 번은 사실이지만 변경되었습니다 (예 : int x; int && rrx = x; 더 이상 GCC에서 컴파일되지 않음) – drewbarbs

C ++ 03 참조 (현재 C ++ 11에서 lvalue 참조라고 함)의 가장 큰 차이점은 const가 아니어도 임시처럼 rvalue에 바인딩 할 수 있다는 것입니다. 따라서이 구문은 이제 합법적입니다.

T&& r = T();

rvalue 참조는 주로 다음을 제공합니다.

의미를 이동합니다 . 일반적인 const-lvalue 참조 대신 rvalue 참조를 사용하는 이동 생성자 및 이동 지정 연산자를 정의 할 수 있습니다. 이동은 소스를 변경하지 않아도된다는 점을 제외하고는 복사와 같은 기능을합니다. 실제로는 일반적으로 소스를 수정하여 더 이상 이동 된 자원을 소유하지 않도록합니다. 이는 특히 표준 라이브러리 구현에서 불필요한 사본을 제거하는 데 좋습니다.

예를 들어, 복사 생성자는 다음과 같습니다.

foo(foo const& other)
{
    this->length = other.length;
    this->ptr = new int[other.length];
    copy(other.ptr, other.ptr + other.length, this->ptr);
}

이 생성자가 임시로 전달 된 경우 임시가 소멸 될 것이므로 사본이 필요하지 않습니다. 임시로 이미 할당 된 리소스를 사용하지 않는 이유는 무엇입니까? C ++ 03에서는 임시로 전달되었다고 판단 할 수 없으므로 복사를 막을 방법이 없습니다. C ++ 11에서는 이동 생성자를 오버로드 할 수 있습니다.

foo(foo&& other)
{
   this->length = other.length;
   this->ptr = other.ptr;
   other.length = 0;
   other.ptr = nullptr;
}

여기서 큰 차이점을 알 수 있습니다. 이동 생성자는 실제로 인수를 수정합니다. 이렇게하면 임시를 구성중인 오브젝트로 효과적으로 "이동"하여 불필요한 사본을 제거 할 수 있습니다.

이동 생성자는 std::move함수를 사용하여 rvalue 참조로 명시 적으로 변환되는 임시 및 비 const lvalue 참조에 사용 됩니다 (변환 만 수행함). 다음 코드는 f1및에 대한 이동 생성자를 호출합니다 f2.

foo f1((foo())); // Move a temporary into f1; temporary becomes "empty"
foo f2 = std::move(f1); // Move f1 into f2; f1 is now "empty"

완벽한 전달 . rvalue 참조를 통해 템플릿 함수에 대한 인수를 올바르게 전달할 수 있습니다. 이 팩토리 기능을 예로 들어 보겠습니다.

template <typename T, typename A1>
std::unique_ptr<T> factory(A1& a1)
{
    return std::unique_ptr<T>(new T(a1));
}

우리가 호출 factory<foo>(5)하면, 인수는로 추론됩니다 int&. 이것은 foo생성자가을 가져 도 리터럴 5에 바인딩되지 않습니다 int. 글쎄, 대신 대신 사용할 수 A1 const&있지만, fooconst가 아닌 참조로 생성자 인수 를 사용 하면 어떻게 됩니까? 진정으로 일반적인 팩토리 기능을 만들려면 팩토리를 켜고 켜야 A1&합니다 A1 const&. 팩토리가 1 개의 매개 변수 유형을 사용하는 경우에는 문제가 없지만 추가 매개 변수 유형마다 필요한 과부하 세트에 2를 곱하면 매우 빠르게 유지 관리 할 수 ​​없습니다.

rvalue 참조는 표준 라이브러리가 std::forwardlvalue / rvalue 참조를 올바르게 전달할 수 있는 함수 를 정의 할 수있게하여이 문제를 해결합니다 . std::forward작동 방식 에 대한 자세한 내용 은 이 훌륭한 답변을 참조하십시오 .

이를 통해 다음과 같이 팩토리 기능을 정의 할 수 있습니다.

template <typename T, typename A1>
std::unique_ptr<T> factory(A1&& a1)
{
    return std::unique_ptr<T>(new T(std::forward<A1>(a1)));
}

이제 인수의 rvalue / lvalue-ness가의 T생성자에 전달 될 때 유지됩니다 . 즉, 팩토리가 rvalue로 호출되면 T의 생성자는 rvalue로 호출됩니다. lvalue로 factory를 호출하면 T의 생성자가 lvalue로 호출됩니다. 개선 된 팩토리 기능은 하나의 특수 규칙으로 인해 작동합니다.

함수 파라미터 입력 양식의 경우 템플릿 파라미터 및 함수 인자는 타입의 좌변 인 , 유형 템플릿 인자 추론에 사용된다.T&&TAA&

따라서 팩토리를 다음과 같이 사용할 수 있습니다.

auto p1 = factory<foo>(foo()); // calls foo(foo&&)
auto p2 = factory<foo>(*p1);   // calls foo(foo const&)

중요한 rvalue 참조 속성 :

  • 과부하 해결의 경우, lvalue는 lvalue 참조에 대한 바인딩을 선호하고 rvalue는 rvalue 참조에 대한 바인딩을 선호합니다 . 따라서 임시 직원이 복사 생성자 / 할당 연산자보다 이동 생성자 / 이동 할당 연산자를 호출하는 것을 선호하는 이유는 무엇입니까?
  • rvalue 참조는 암시 적으로 rvalue 및 암시 적 변환의 결과 인 임시에 바인딩됩니다 . 즉 float f = 0f; int&& i = f;float는 암시 적으로 int로 변환 가능하기 때문에 잘 구성됩니다. 참조는 변환의 결과 인 임시에 대한 것입니다.
  • 명명 된 rvalue 참조는 lvalue입니다. 명명되지 않은 rvalue 참조는 rvalue입니다. std::move전화가 왜 필요한지 이해하는 것이 중요합니다 .foo&& r = foo(); foo f = std::move(r);

65
Named rvalue references are lvalues. Unnamed rvalue references are rvalues.; +1 이것을 알지 못하고 나는 왜 사람들이 T &&t; std::move(t);이동 ctor 등에서 오랜 시간을 하는지 이해하려고 애썼다 .
legends2k

@MaximYegorushkin :이 예에서 r은 순수한 rvalue (임시)에 바인딩되므로 임시 수명의 연장 범위가 아니어야합니까?
Peter Huene

@PeterHuene 나는 그것을 다시 가져 간다. r 값 참조는 일시적으로 수명을 연장시킨다.
Maxim Egorushkin

32
주의 : MSDN의 링크 된 기사 ( "Rvalue References : VC10의 C ++ 0x 기능, 2 부") Rvalue 참조에 대한 매우 명확한 소개 이지만 C ++ 11 초안에서 한때 사실 이었던 Rvalue 참조에 대한 설명을 제공 합니다. 표준이지만 마지막 것은 사실아닙니다 ! 특히, 그것은 여러 가지 점에서 rvalue 참조가 lvalue에 바인딩 될 수 있다고 말하는데, 이는 한 번은 사실이지만 변경 되었습니다 (예 : int x; int &&rrx = x; 더 이상 GCC에서 컴파일되지 않음 )
drewbarbs

@PeterHuene 위의 예에서 ? typename identity<T>::type& a와 같지 않습니다 T&.
ibp73

81

rvalue 참조를 나타냅니다. Rvalue 참조는 달리 명시 적으로 생성되지 않는 한 임시 객체에만 바인드됩니다. 특정 상황에서 객체를 훨씬 더 효율적으로 만들고 완벽한 전달이라고하는 기능을 제공하여 템플릿 코드를 크게 단순화하는 데 사용됩니다.

C ++ 03에서는 변경할 수없는 lvalue와 rvalue의 사본을 구별 할 수 없습니다.

std::string s;
std::string another(s);           // calls std::string(const std::string&);
std::string more(std::string(s)); // calls std::string(const std::string&);

C ++ 0x에서는 그렇지 않습니다.

std::string s;
std::string another(s);           // calls std::string(const std::string&);
std::string more(std::string(s)); // calls std::string(std::string&&);

이러한 생성자 뒤에 구현을 고려하십시오. 첫 번째 경우, 문자열은 새로운 힙 할당을 포함하는 값 의미론을 유지하기 위해 복사를 수행해야합니다. 그러나 두 번째 경우에는 생성자로 전달 된 객체가 즉시 폐기 될 예정이므로 그대로 유지하지 않아도됩니다. 이 시나리오에서는 내부 포인터를 효과적으로 교체하고 복사를 전혀 수행 할 수 없으며 이는 훨씬 더 효율적입니다. 이동 의미론은 내부 참조 자원의 비싸거나 금지 된 복사가있는 모든 클래스에 혜택을줍니다. 의 경우를 생각해 std::unique_ptr지금 우리의 클래스는 임시직이 아닌 임시직을 구별 할 수있는, 우리가 이동 의미가 제대로 그래서이 있음을 작동 할 수 있습니다 - unique_ptr어떤 의미, 복사 할 수 없습니다 있지만 이동할 수 있습니다std::unique_ptrC ++ 03은 표준 컨테이너에 합법적으로 저장하고 정렬 std::auto_ptr할 수 있습니다.

이제 rvalue 참조의 다른 사용-완벽한 전달을 고려합니다. 참조를 참조에 바인딩하는 문제를 고려하십시오.

std::string s;
std::string& ref = s;
(std::string&)& anotherref = ref; // usually expressed via template

C ++ 03이 이것에 대해 말한 것을 기억할 수는 없지만 C ++ 0x에서는 rvalue 참조를 처리 할 때 결과 유형이 중요합니다. 유형 T에 대한 rvalue 참조 (여기서 T는 참조 유형 임)는 유형 T의 참조가됩니다.

(std::string&)&& ref // ref is std::string&
(const std::string&)&& ref // ref is const std::string&
(std::string&&)&& ref // ref is std::string&&
(const std::string&&)&& ref // ref is const std::string&&

가장 간단한 템플릿 기능인 min과 max를 고려하십시오. C ++ 03에서는 const와 non-const의 네 가지 조합을 모두 수동으로 오버로드해야합니다. C ++ 0x에서는 단지 하나의 과부하입니다. 다양한 템플릿과 결합하여 완벽한 전달이 가능합니다.

template<typename A, typename B> auto min(A&& aref, B&& bref) {
    // for example, if you pass a const std::string& as first argument,
    // then A becomes const std::string& and by extension, aref becomes
    // const std::string&, completely maintaining it's type information.
    if (std::forward<A>(aref) < std::forward<B>(bref))
        return std::forward<A>(aref);
    else
        return std::forward<B>(bref);
}

반환 유형 공제를 중단했습니다. 왜냐하면 그것이 어떻게 수행되었는지를 기억할 수는 없지만 최소값은 lvalue, rvalue, const lvalue의 조합을 허용 할 수 있습니다.


왜 사용 std::forward<A>(aref) < std::forward<B>(bref)했습니까? 그리고 당신이 앞으로 할 때이 정의가 올 것이라고 생각 해달라고 int&하고 float&. 한 유형의 양식 서식 파일을 삭제하는 것이 좋습니다.
Yankes

25

T&& 유형 공제와 함께 사용될 때 (완전한 전달과 같은) 용어 는 구어체 적으로 전달 참조라고 합니다. "Universal reference"라는 용어 는이 기사에서 Scott Meyers 만들었지 만 나중에 변경되었습니다.

r- 값 또는 l- 값일 수 있기 때문입니다.

예를 들면 다음과 같습니다.

// template
template<class T> foo(T&& t) { ... }

// auto
auto&& t = ...;

// typedef
typedef ... T;
T&& t = ...;

// decltype
decltype(...)&& t = ...;

: 더 논의에 대한 대답에서 찾을 수 있습니다 보편적 참조 구문


14

rvalue 참조는 몇 가지 예외를 제외하고 일반 참조 X &와 매우 유사하게 작동하는 유형입니다. 가장 중요한 것은 기능 과부하 해결에있어 lvalue는 구식 lvalue 참조를 선호하는 반면 rvalue는 새로운 rvalue 참조를 선호한다는 것입니다.

void foo(X& x);  // lvalue reference overload
void foo(X&& x); // rvalue reference overload

X x;
X foobar();

foo(x);        // argument is lvalue: calls foo(X&)
foo(foobar()); // argument is rvalue: calls foo(X&&)

rvalue는 무엇입니까? lvalue가 아닌 모든 것. lvalue는 메모리 위치를 나타내는 표현식이며 & 연산자를 통해 해당 메모리 위치의 주소를 가져올 수 있습니다.

예제를 통해 rvalue가 무엇을 달성하는지 이해하는 것이 가장 쉽습니다.

 #include <cstring>
 class Sample {
  int *ptr; // large block of memory
  int size;
 public:
  Sample(int sz=0) : ptr{sz != 0 ? new int[sz] : nullptr}, size{sz} 
  {
     if (ptr != nullptr) memset(ptr, 0, sz);
  }
  // copy constructor that takes lvalue 
  Sample(const Sample& s) : ptr{s.size != 0 ? new int[s.size] :\
      nullptr}, size{s.size}
  {
     if (ptr != nullptr) memcpy(ptr, s.ptr, s.size);
     std::cout << "copy constructor called on lvalue\n";
  }

  // move constructor that take rvalue
  Sample(Sample&& s) 
  {  // steal s's resources
     ptr = s.ptr;
     size = s.size;        
     s.ptr = nullptr; // destructive write
     s.size = 0;
     cout << "Move constructor called on rvalue." << std::endl;
  }    
  // normal copy assignment operator taking lvalue
  Sample& operator=(const Sample& s)
  {
   if(this != &s) {
      delete [] ptr; // free current pointer
      size = s.size;

      if (size != 0) {
        ptr = new int[s.size];
        memcpy(ptr, s.ptr, s.size);
      } else 
         ptr = nullptr;
     }
     cout << "Copy Assignment called on lvalue." << std::endl;
     return *this;
  }    
 // overloaded move assignment operator taking rvalue
 Sample& operator=(Sample&& lhs)
 {
   if(this != &s) {
      delete [] ptr; //don't let ptr be orphaned 
      ptr = lhs.ptr;   //but now "steal" lhs, don't clone it.
      size = lhs.size; 
      lhs.ptr = nullptr; // lhs's new "stolen" state
      lhs.size = 0;
   }
   cout << "Move Assignment called on rvalue" << std::endl;
   return *this;
 }
//...snip
};     

생성자와 할당 연산자는 rvalue 참조를 사용하는 버전으로 오버로드되었습니다. Rvalue 참조를 사용하면 "lvalue 또는 rvalue에서 호출되고 있습니까?"라는 조건에서 함수가 컴파일 시간 (과부하 해결을 통해)으로 분기 될 수 있습니다. 이를 통해 리소스를 복사하는 대신보다 효율적인 생성자 및 할당 연산자를 생성 할 수있었습니다.

이동 생성자 또는 이동 할당 연산자를 호출해야하는지 여부를 선택하는 컴파일러는 컴파일 할 때 자동으로 분기합니다 (lvalue 또는 rvalue에 대해 호출되는지 여부에 따라 다름).

요약 : rvalue 참조는 이동 의미론 (그리고 아래 기사 링크에서 설명한 완벽한 전달)을 허용합니다.

이해하기 쉬운 한 가지 예는 클래스 템플릿 std :: unique_ptr 입니다. unique_ptr은 기본 raw 포인터의 독점 소유권을 유지하므로 unique_ptr은 복사 할 수 없습니다. 그것은 그들의 독점 소유권의 불변을 위반할 것입니다. 따라서 복사 생성자가 없습니다. 그러나 이동 생성자가 있습니다.

template<class T> class unique_ptr {
  //...snip
 unique_ptr(unique_ptr&& __u) noexcept; // move constructor
};

 std::unique_ptr<int[] pt1{new int[10]};  
 std::unique_ptr<int[]> ptr2{ptr1};// compile error: no copy ctor.  

 // So we must first cast ptr1 to an rvalue 
 std::unique_ptr<int[]> ptr2{std::move(ptr1)};  

std::unique_ptr<int[]> TakeOwnershipAndAlter(std::unique_ptr<int[]> param,\
 int size)      
{
  for (auto i = 0; i < size; ++i) {
     param[i] += 10;
  }
  return param; // implicitly calls unique_ptr(unique_ptr&&)
}

// Now use function     
unique_ptr<int[]> ptr{new int[10]};

// first cast ptr from lvalue to rvalue
unique_ptr<int[]> new_owner = TakeOwnershipAndAlter(\
           static_cast<unique_ptr<int[]>&&>(ptr), 10);

cout << "output:\n";

for(auto i = 0; i< 10; ++i) {
   cout << new_owner[i] << ", ";
}

output:
10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 

static_cast<unique_ptr<int[]>&&>(ptr)일반적으로 std :: move를 사용하여 수행됩니다.

// first cast ptr from lvalue to rvalue
unique_ptr<int[]> new_owner = TakeOwnershipAndAlter(std::move(ptr),0);

좋은 예가 많은 rvalues가 완벽한 전달을 허용하는 방법과 그 의미와 같은 모든 것을 설명하는 훌륭한 기사는 Thomas Becker의 C ++ Rvalue References Explained 입니다. 이 게시물은 그의 기사에 크게 의존했습니다.

짧은 소개는 Stroutrup의 Rvalue References대한 간략한 소개 입니다. 알


그렇지 않으면 복사 생성자 Sample(const Sample& s)가 내용을 복사해야합니까? '복사 할당 연산자'와 동일한 질문입니다.
K.Karamazen

그래, 너가 맞아. 메모리를 복사하지 못했습니다. 복사 생성자와 복사 할당 연산자는 해당 크기! = 0을 테스트 한 후 memcpy (ptr, s.ptr, size)를 모두 수행해야합니다. 그리고 크기가! = 0 인 경우 기본 생성자는 memset (ptr, 0, size)을 수행해야합니다.
kurt krueckeberg

괜찮 감사. 따라서이 의견과 앞의 두 의견은 문제가 답변에서 수정 되었으므로 제거 될 수 있습니다 .
K.Karamazen
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.