T && (double ampersand) có nghĩa là gì trong C ++ 11?


799

Tôi đã xem xét một số tính năng mới của C ++ 11 và một trong số đó tôi nhận thấy là hai dấu và trong việc khai báo các biến, như thế nào T&& var.

Để bắt đầu, con thú này được gọi là gì? Tôi ước Google sẽ cho phép chúng tôi tìm kiếm dấu câu như thế này.

Chính xác nó có nghĩa là gì?

Thoạt nhìn, nó dường như là một tham chiếu kép (như con trỏ kép kiểu C T** var), nhưng tôi gặp khó khăn khi nghĩ về trường hợp sử dụng cho điều đó.


55
Tôi đã thêm nó vào c ++ - faq, vì tôi chắc chắn rằng nó sẽ xuất hiện nhiều hơn trong tương lai.
GManNickG

3
câu hỏi liên quan về ngữ nghĩa di chuyển
fredoverflow

41
Bạn có thể tìm kiếm điều này bằng cách sử dụng google, bạn chỉ cần bọc cụm từ của mình trong dấu ngoặc kép: google.com/#q="T%26%26 "bây giờ có câu hỏi của bạn là lần truy cập đầu tiên. :)
sbi

Có một câu trả lời rất hay, dễ hiểu cho một câu hỏi tương tự ở đây stackoverflow.com/questions/7153991/ Kẻ
Daniel

2
Tôi đã nhận được ba câu hỏi stackoverflow ở đầu tìm kiếm trong Google cho "c ++ hai tham số ampersands" và câu hỏi của bạn là câu hỏi đầu tiên. Vì vậy, bạn thậm chí không cần sử dụng dấu chấm câu cho điều này nếu bạn có thể đánh vần "hai tham số ampersands".
sergiol

Câu trả lời:


668

Nó tuyên bố một tham chiếu giá trị (tài liệu đề xuất tiêu chuẩn).

Đây là một giới thiệu về tài liệu tham khảo giá trị .

Đây là một cái nhìn sâu sắc tuyệt vời về các tài liệu tham khảo giá trị của một trong những nhà phát triển thư viện tiêu chuẩn của Microsoft .

THẬN TRỌNG: bài viết được liên kết trên MSDN ("Tài liệu tham khảo Rvalue: Các tính năng của C ++ 0x trong VC10, Phần 2") là phần giới thiệu rất rõ ràng về các tham chiếu Rvalue, nhưng đưa ra các tuyên bố về các tham chiếu Rvalue đã từng đúng trong dự thảo C ++ 11 tiêu chuẩn, nhưng không đúng với cái cuối cùng! Cụ thể, nó nói tại các điểm khác nhau mà các tham chiếu rvalue có thể liên kết với các giá trị, điều này đã từng đúng, nhưng đã được thay đổi. (Ví dụ: int x; int && rrx = x; không còn biên dịch trong GCC) - drewbarbs Jul 13 '14 lúc 16:12

Sự khác biệt lớn nhất giữa một tham chiếu C ++ 03 (hiện được gọi là tham chiếu lvalue trong C ++ 11) là nó có thể liên kết với một giá trị như tạm thời mà không cần phải là const. Vì vậy, cú pháp này là hợp pháp:

T&& r = T();

tài liệu tham khảo giá trị chủ yếu cung cấp cho sau đây:

Di chuyển ngữ nghĩa . Một hàm tạo di chuyển và toán tử gán di chuyển bây giờ có thể được xác định có tham chiếu giá trị thay vì tham chiếu const-lvalue thông thường. Một chức năng di chuyển như một bản sao, ngoại trừ nó không bắt buộc phải giữ nguồn không thay đổi; trong thực tế, nó thường sửa đổi nguồn sao cho nó không còn sở hữu các tài nguyên đã di chuyển. Điều này là tuyệt vời để loại bỏ các bản sao không liên quan, đặc biệt là trong việc triển khai thư viện tiêu chuẩn.

Ví dụ: một hàm tạo sao chép có thể trông như thế này:

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

Nếu nhà xây dựng này được thông qua tạm thời, bản sao sẽ không cần thiết bởi vì chúng tôi biết tạm thời sẽ bị hủy; Tại sao không sử dụng các tài nguyên tạm thời đã được phân bổ? Trong C ++ 03, không có cách nào để ngăn chặn bản sao vì chúng tôi không thể xác định chúng tôi đã được thông qua tạm thời. Trong C ++ 11, chúng ta có thể quá tải một hàm tạo di chuyển:

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

Lưu ý sự khác biệt lớn ở đây: hàm tạo di chuyển thực sự sửa đổi đối số của nó. Điều này sẽ "di chuyển" tạm thời vào đối tượng đang được xây dựng, do đó loại bỏ các bản sao không cần thiết.

Hàm xây dựng di chuyển sẽ được sử dụng cho tạm thời và cho các tham chiếu giá trị không phải là const được chuyển đổi rõ ràng thành tham chiếu giá trị bằng cách sử dụng std::movehàm (nó chỉ thực hiện chuyển đổi). Đoạn mã sau gọi cả hàm tạo di chuyển cho f1f2:

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

Chuyển tiếp hoàn hảo . tham chiếu rvalue cho phép chúng ta chuyển tiếp các đối số đúng cho các hàm templated. Lấy ví dụ chức năng nhà máy này:

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

Nếu chúng ta gọi factory<foo>(5), đối số sẽ được suy ra int&, sẽ không liên kết với nghĩa đen 5, ngay cả khi hàm tạo foocủa nó có một int. Chà, thay vào đó chúng ta có thể sử dụng A1 const&, nhưng nếu foolấy đối số của hàm tạo bằng tham chiếu không phải là const thì sao? Để thực hiện một chức năng nhà máy thực sự chung chung, chúng ta sẽ phải quá tải nhà máy trên A1&và trên A1 const&. Điều đó có thể ổn nếu nhà máy lấy 1 loại tham số, nhưng mỗi loại tham số bổ sung sẽ nhân hệ số quá tải cần thiết được đặt bằng 2. Điều đó rất nhanh không thể nhận ra.

tham chiếu rvalue khắc phục vấn đề này bằng cách cho phép thư viện chuẩn xác định một std::forwardhàm có thể chuyển tiếp các tham chiếu lvalue / rvalue đúng cách. Để biết thêm thông tin về cách làm std::forwardviệc, xem câu trả lời tuyệt vời này .

Điều này cho phép chúng tôi xác định chức năng của nhà máy như thế này:

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

Bây giờ rvalue / lvalue-ness của đối số được giữ nguyên khi được truyền cho hàm tạo Tcủa đối số. Điều đó có nghĩa là nếu nhà máy được gọi với một giá trị, thì hàm tạo Tcủa nó được gọi với một giá trị. Nếu nhà máy được gọi với một giá trị, thì hàm tạo Tcủa nó được gọi với một giá trị. Chức năng nhà máy được cải thiện hoạt động nhờ một quy tắc đặc biệt:

Khi các loại tham số chức năng có dạng T&&nơi Tlà một tham số mẫu, và các tham số của hàm là một vế trái của loại A, loại A&được sử dụng để khấu trừ mẫu đối số.

Vì vậy, chúng ta có thể sử dụng nhà máy như vậy:

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

Các thuộc tính tham chiếu giá trị quan trọng :

  • Đối với độ phân giải quá tải, giá trị thích liên kết với tham chiếu lvalue và giá trị thích ràng buộc với tham chiếu giá trị . Do đó, tại sao các bộ tạm thời thích gọi một hàm tạo di chuyển / toán tử gán di chuyển trên một toán tử sao chép / toán tử gán.
  • tham chiếu giá trị sẽ ngầm liên kết với các giá trị và thời gian là kết quả của một chuyển đổi ngầm định . tức float f = 0f; int&& i = f;là được hình thành tốt bởi vì float được chuyển đổi hoàn toàn thành int; tham chiếu sẽ là tạm thời là kết quả của việc chuyển đổi.
  • Tài liệu tham khảo rvalue được đặt tên là giá trị. Tài liệu tham khảo rvalue không tên là giá trị. Điều này rất quan trọng để hiểu tại sao std::movecuộc gọi là cần thiết trong:foo&& r = foo(); foo f = std::move(r);

65
+1 cho Named rvalue references are lvalues. Unnamed rvalue references are rvalues.; mà không biết điều này, tôi đã đấu tranh để hiểu lý do tại sao mọi người làm một T &&t; std::move(t);thời gian dài trong việc di chuyển, và tương tự.
huyền thoại2k

@MaximYegorushkin: Trong ví dụ đó, r liên kết với một giá trị thuần túy (tạm thời) và do đó tạm thời nên mở rộng phạm vi trọn đời của nó, không?
Peter Huene

@PeterHuene Tôi lấy lại, tham chiếu giá trị r sẽ kéo dài thời gian tồn tại tạm thời.
Maxim Egorushkin

32
THẬN TRỌNG : bài viết được liên kết trên MSDN ("Tài liệu tham khảo Rvalue: Các tính năng của C ++ 0x trong VC10, Phần 2") phần giới thiệu rất rõ ràng về các tham chiếu Rvalue, nhưng đưa ra các tuyên bố về các tham chiếu Rvalue đã từng đúng trong dự thảo C ++ 11 tiêu chuẩn, nhưng không đúng với cái cuối cùng! Cụ thể, nó nói tại các điểm khác nhau mà các tham chiếu giá trị có thể liên kết với các giá trị, điều này đã từng đúng, nhưng đã được thay đổi (ví dụ: int x; int &&rrx = x; không còn biên dịch trong GCC)
drewbarbs

@PeterHuene Trong ví dụ trên, không typename identity<T>::type& atương đương với T&?
ibp73

81

Nó biểu thị một tham chiếu giá trị. Tham chiếu Rvalue sẽ chỉ liên kết với các đối tượng tạm thời, trừ khi được tạo rõ ràng bằng cách khác. Chúng được sử dụng để làm cho các đối tượng hiệu quả hơn nhiều trong các trường hợp nhất định và để cung cấp một phương tiện được gọi là chuyển tiếp hoàn hảo, giúp đơn giản hóa rất nhiều mã mẫu.

Trong C ++ 03, bạn không thể phân biệt giữa một bản sao của một giá trị không thể thay đổi và một giá trị.

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&);

Trong C ++ 0x, đây không phải là trường hợp.

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

Hãy xem xét việc thực hiện đằng sau các nhà xây dựng này. Trong trường hợp đầu tiên, chuỗi phải thực hiện một bản sao để giữ lại ngữ nghĩa giá trị, bao gồm phân bổ heap mới. Tuy nhiên, trong trường hợp thứ hai, chúng tôi biết trước rằng đối tượng được truyền cho nhà xây dựng của chúng tôi ngay lập tức là do bị phá hủy và nó không phải bị ảnh hưởng. Chúng tôi thực sự có thể trao đổi các con trỏ nội bộ và không thực hiện bất kỳ sao chép nào trong kịch bản này, điều này thực sự hiệu quả hơn. Di chuyển ngữ nghĩa có lợi cho bất kỳ lớp nào có bản sao đắt tiền hoặc bị cấm sao chép tài nguyên tham chiếu nội bộ. Hãy xem xét trường hợp std::unique_ptr- bây giờ lớp chúng ta có thể phân biệt giữa tạm thời và không tạm thời, chúng ta có thể làm cho ngữ nghĩa di chuyển hoạt động chính xác để unique_ptrkhông thể sao chép nhưng có thể được di chuyển, có nghĩa làstd::unique_ptrcó thể được lưu trữ hợp pháp trong các thùng chứa Tiêu chuẩn, được sắp xếp, v.v., trong khi C ++ 03 std::auto_ptrthì không thể.

Bây giờ chúng tôi xem xét việc sử dụng khác của tham chiếu giá trị - chuyển tiếp hoàn hảo. Xem xét câu hỏi ràng buộc một tài liệu tham khảo đến một tài liệu tham khảo.

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

Không thể nhớ lại những gì C ++ 03 nói về điều này, nhưng trong C ++ 0x, loại kết quả khi xử lý các tham chiếu giá trị là rất quan trọng. Tham chiếu giá trị cho loại T, trong đó T là loại tham chiếu, trở thành tham chiếu của loại 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&&

Hãy xem xét hàm mẫu đơn giản nhất - tối thiểu và tối đa. Trong C ++ 03, bạn phải quá tải cho cả bốn kết hợp const và non-const thủ công. Trong C ++ 0x, nó chỉ là một quá tải. Kết hợp với các mẫu matrixdic, điều này cho phép chuyển tiếp hoàn hảo.

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);
}

Tôi đã loại bỏ việc khấu trừ kiểu trả về, vì tôi không thể nhớ lại cách nó được thực hiện một cách thủ công, nhưng min đó có thể chấp nhận bất kỳ sự kết hợp nào của giá trị, giá trị, giá trị const.


tại sao bạn sử dụng std::forward<A>(aref) < std::forward<B>(bref)? và tôi không nghĩ định nghĩa này sẽ đúng khi bạn cố gắng chuyển tiếp int&float&. Tốt hơn nên thả một mẫu mẫu.
Yankes

25

Thuật ngữ T&& khi được sử dụng với khấu trừ loại (chẳng hạn như để chuyển tiếp hoàn hảo) được gọi thông thường là một tài liệu tham khảo chuyển tiếp . Thuật ngữ "tài liệu tham khảo phổ quát" được Scott Meyers đặt ra trong bài viết này , nhưng sau đó đã được thay đổi.

Đó là bởi vì nó có thể là giá trị r hoặc giá trị l.

Ví dụ là:

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

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

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

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

Có thể tìm thấy nhiều thảo luận hơn trong câu trả lời cho: Cú pháp cho các tài liệu tham khảo phổ quát


14

Tham chiếu rvalue là loại hoạt động giống như tham chiếu X & thông thường, với một vài ngoại lệ. Điều quan trọng nhất là khi nói đến độ phân giải quá tải chức năng, các giá trị thích các tham chiếu giá trị kiểu cũ, trong khi các giá trị thích các tham chiếu giá trị mới:

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&&)

Vì vậy, một giá trị là gì? Bất cứ điều gì không phải là một giá trị. Một giá trị là một biểu thức đề cập đến một vị trí bộ nhớ và cho phép chúng ta lấy địa chỉ của vị trí bộ nhớ đó thông qua toán tử &.

Trước tiên, hầu như dễ hiểu hơn những gì các giá trị thực hiện với một ví dụ:

 #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
};     

Hàm xây dựng và toán tử gán đã bị quá tải với các phiên bản có tham chiếu giá trị. Tham chiếu Rvalue cho phép một hàm phân nhánh tại thời gian biên dịch (thông qua độ phân giải quá tải) với điều kiện "Tôi có được gọi trên một giá trị hoặc một giá trị không?". Điều này cho phép chúng tôi tạo ra các toán tử xây dựng và gán hiệu quả hơn ở trên để di chuyển tài nguyên thay vì sao chép chúng.

Trình biên dịch tự động phân nhánh tại thời gian biên dịch (tùy thuộc vào việc nó được gọi cho một giá trị hoặc một giá trị) chọn xem nên gọi hàm tạo di chuyển hoặc toán tử gán di chuyển.

Tóm tắt: tài liệu tham khảo rvalue cho phép di chuyển ngữ nghĩa (và chuyển tiếp hoàn hảo, được thảo luận trong liên kết bài viết dưới đây).

Một ví dụ thực tế dễ hiểu là mẫu lớp std :: unique_ptr . Vì unique_ptr duy trì quyền sở hữu độc quyền của con trỏ thô bên dưới của nó, nên unique_ptr không thể được sao chép. Điều đó sẽ vi phạm quyền sở hữu độc quyền của họ. Vì vậy, họ không có các nhà xây dựng sao chép. Nhưng họ có nhà xây dựng di chuyển:

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)thường được thực hiện bằng cách sử dụng std :: move

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

Một bài viết xuất sắc giải thích tất cả những điều này và hơn thế nữa (như cách các giá trị cho phép chuyển tiếp hoàn hảo và điều đó có nghĩa là gì) với rất nhiều ví dụ hay là C ++ Rvalue Tài liệu tham khảo Giải thích . Bài đăng này dựa rất nhiều vào bài viết của mình.

Giới thiệu ngắn hơn là Giới thiệu ngắn gọn về Tài liệu tham khảo Rvalue của Stroutrup, et. al


Không phải vì vậy mà các nhà xây dựng sao chép Sample(const Sample& s)cũng cần phải sao chép nội dung? Câu hỏi tương tự cho 'toán tử gán sao chép'.
K.Karamazen

Vâng, bạn đúng. Tôi không thể sao chép bộ nhớ. Cả hai hàm tạo sao chép và toán tử gán gán sao chép nên thực hiện memcpy (ptr, s.ptr, size) sau khi kiểm tra kích thước đó! = 0. Và hàm tạo mặc định sẽ thực hiện memset (ptr, 0, size) nếu size! = 0.
kurt krueckeberg

Được rồi, cảm ơn. Do đó , bình luận này và hai bình luận trước có thể được gỡ bỏ vì vấn đề cũng đã được khắc phục trong câu trả lời.
K.Karamazen
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.