Apa arti T&& (ampersand ganda) dalam C ++ 11?

801

Saya telah melihat beberapa fitur baru C ++ 11 dan yang saya perhatikan adalah double ampersand dalam mendeklarasikan variabel, seperti T&& var .

Sebagai permulaan, apa nama binatang ini? Saya berharap Google mengizinkan kami untuk mencari tanda baca seperti ini.

Apa tepatnya yang dilakukan artinya?

Pada pandangan pertama, ini tampaknya menjadi referensi ganda (seperti pointer ganda C-style T** var), tapi saya kesulitan memikirkan use case untuk itu.

paxdiablo
sumber
55
Saya telah menambahkan ini ke c ++ - faq, karena saya yakin ini akan muncul lebih banyak di masa depan.
GManNickG
3
pertanyaan terkait tentang pindah semantik
fredoverflow
41
Anda dapat mencari ini menggunakan google, Anda hanya perlu membungkus frase Anda dengan tanda kutip: google.com/#q="T%26%26 "sekarang miliki pertanyaan Anda sebagai hit pertama. :)
sbi
Ada respons yang sangat baik, mudah dipahami untuk pertanyaan serupa di sini stackoverflow.com/questions/7153991/…
Daniel
2
Saya mendapat tiga pertanyaan stackoverflow di pencarian teratas di Google untuk "c ++ two ampersands parameter" dan pertanyaan Anda adalah yang pertama. Jadi Anda bahkan tidak perlu menggunakan tanda baca untuk ini jika Anda bisa mengeja "dua parameter ampersands".
sergiol

Jawaban:

668

Ini menyatakan referensi nilai (proposal standar dokumen).

Inilah pengantar untuk menilai kembali referensi .

Berikut ini adalah tinjauan mendalam yang luar biasa pada referensi nilai oleh salah satu pengembang perpustakaan standar Microsoft .

PERHATIAN: artikel tertaut di MSDN ("Rvalue Referensi: C ++ 0x Fitur di VC10, Bagian 2") adalah pengantar yang sangat jelas untuk referensi Rvalue, tetapi membuat pernyataan tentang Rvalue referensi yang dulunya benar dalam konsep C ++ 11 standar, tetapi tidak benar untuk yang terakhir! Secara khusus, dikatakan di berbagai titik bahwa nilai referensi dapat mengikat nilai-nilai, yang pernah benar, tetapi diubah (mis. Int x; int && rrx = x; tidak lagi dikompilasi dalam GCC) - drewbarbs 13 Juli 14 di 16:12

Perbedaan terbesar antara referensi C ++ 03 (sekarang disebut referensi lvalue dalam C ++ 11) adalah bahwa ia dapat mengikat ke nilai seperti sementara tanpa harus menjadi const. Dengan demikian, sintaks ini sekarang sah:

T&& r = T();

referensi nilai terutama untuk hal-hal berikut:

Pindahkan semantik . Operator pemindah dan pemindah tugas sekarang dapat didefinisikan yang mengambil referensi nilai daripada referensi konstanta-nilai yang biasa. Pindah berfungsi seperti salinan, kecuali tidak wajib menjaga sumbernya tidak berubah; bahkan, biasanya memodifikasi sumber sehingga tidak lagi memiliki sumber daya yang dipindahkan. Ini bagus untuk menghilangkan salinan asing, terutama dalam implementasi perpustakaan standar.

Misalnya, penyalin salinan mungkin terlihat seperti ini:

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

Jika konstruktor ini disahkan sementara, salinannya tidak perlu karena kami tahu sementara hanya akan dihancurkan; mengapa tidak menggunakan sumber daya yang sementara telah dialokasikan? Di C ++ 03, tidak ada cara untuk mencegah penyalinan karena kami tidak dapat menentukan bahwa kami lulus sementara. Di C ++ 11, kita bisa membebani konstruktor pemindahan:

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

Perhatikan perbedaan besar di sini: move constructor sebenarnya memodifikasi argumennya. Ini secara efektif akan "memindahkan" sementara ke objek yang sedang dibangun, sehingga menghilangkan salinan yang tidak perlu.

Konstruktor pemindahan akan digunakan untuk temporari dan untuk referensi non-nilai konstanta yang secara eksplisit dikonversi ke referensi nilai menggunakan std::movefungsi (itu hanya melakukan konversi). Kode berikut keduanya memohon konstruktor pemindahan untuk f1dan 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"

Penerusan sempurna . referensi nilai memungkinkan kita untuk meneruskan argumen dengan benar untuk fungsi templated. Ambil contoh fungsi pabrik ini:

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

Jika kita memanggil factory<foo>(5), argumen akan dideduksi menjadi int&, yang tidak akan mengikat ke 5 literal, bahkan jika fookonstruktor membutuhkan int. Yah, kita malah bisa menggunakan A1 const&, tetapi bagaimana jika foomengambil argumen konstruktor dengan referensi non-const? Untuk membuat fungsi pabrik yang benar-benar generik, kita harus membebani pabrik di A1&dan diA1 const& . Itu mungkin baik-baik saja jika pabrik mengambil 1 tipe parameter, tetapi setiap tipe parameter tambahan akan melipatgandakan kelebihan yang diperlukan ditetapkan oleh 2. Itu sangat cepat tidak dapat dipertahankan.

referensi nilai memperbaiki masalah ini dengan memungkinkan perpustakaan standar untuk mendefinisikan std::forwardfungsi yang dapat meneruskan referensi nilai / nilai dengan benar. Untuk informasi lebih lanjut tentang cara std::forwardkerjanya, lihat jawaban yang sangat bagus ini .

Ini memungkinkan kami untuk mendefinisikan fungsi pabrik seperti ini:

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

Sekarang argumen / nilai-nilai argumen dipertahankan ketika diteruskan ke Tkonstruktor. Itu berarti bahwa jika pabrik dipanggil dengan nilai, Tkonstruktor disebut dengan nilai. Jika pabrik dipanggil dengan lvalue, Tkonstruktor disebut dengan lvalue. Fungsi pabrik yang ditingkatkan berfungsi karena satu aturan khusus:

Ketika tipe parameter fungsi adalah formulir di T&&mana Tmerupakan parameter templat, dan argumen fungsi adalah nilai jenis A, maka jenis A&ini digunakan untuk pengurangan argumen templat.

Jadi, kita bisa menggunakan pabrik seperti ini:

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

Properti referensi nilai penting :

  • Untuk resolusi kelebihan beban, nilai lebih suka mengikat ke nilai referensi dan nilai lebih suka mengikat ke nilai referensi . Oleh karena itu mengapa temporaries lebih suka memanggil move constructor / move assignment operator daripada copy constructor / assignment operator
  • referensi nilai akan secara implisit mengikat nilai dan untuk sementara yang merupakan hasil dari konversi implisit . yaitufloat f = 0f; int&& i = f; terbentuk dengan baik karena float secara implisit dapat dikonversi menjadi int; rujukannya adalah sementara yang merupakan hasil konversi.
  • Referensi nilai yang dinamai adalah nilai. Referensi nilai yang tidak disebutkan namanya adalah nilai. Ini penting untuk memahami mengapa std::movepanggilan itu perlu di:foo&& r = foo(); foo f = std::move(r);
Peter Huene
sumber
65
+1 untuk Named rvalue references are lvalues. Unnamed rvalue references are rvalues.; tanpa mengetahui hal ini saya telah berjuang untuk memahami mengapa orang melakukan hal T &&t; std::move(t);yang lama dalam memindahkan petugas, dan sejenisnya.
legends2k
@ MaximYegorushkin: Dalam contoh itu, r mengikat ke nilai murni (sementara) dan oleh karena itu sementara harus memperpanjang cakupan seumur hidup, bukan?
Peter Huene
@ PeterHuene Saya mengambil itu kembali, referensi r-value memperpanjang umur sementara.
Maxim Egorushkin
32
PERHATIAN : artikel tertaut di MSDN ("Rvalue Referensi: C ++ 0x Fitur di VC10, Bagian 2") adalah pengantar yang sangat jelas untuk referensi Rvalue, tetapi membuat pernyataan tentang Rvalue referensi yang dulunya benar dalam konsep C ++ 11 standar, tetapi tidak benar untuk yang terakhir! Secara khusus, dikatakan di berbagai titik bahwa nilai referensi dapat mengikat nilai-nilai, yang pernah benar, tetapi diubah (misalnya int x; int &&rrx = x; tidak lagi dikompilasi dalam GCC)
drewbarbs
@ PeterHuene Pada contoh di atas, tidak typename identity<T>::type& asetara dengan T&?
ibp73
81

Ini menunjukkan referensi nilai. Referensi nilai hanya akan mengikat ke objek sementara, kecuali secara eksplisit dihasilkan sebaliknya. Mereka digunakan untuk membuat objek jauh lebih efisien dalam keadaan tertentu, dan untuk menyediakan fasilitas yang dikenal sebagai penerusan yang sempurna, yang sangat menyederhanakan kode templat.

Di C ++ 03, Anda tidak bisa membedakan antara salinan nilai yang tidak dapat diubah dan nilai yang lain.

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

Dalam C ++ 0x, ini tidak terjadi.

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

Pertimbangkan implementasi di balik konstruktor ini. Dalam kasus pertama, string harus melakukan salinan untuk mempertahankan nilai semantik, yang melibatkan alokasi tumpukan baru. Namun, dalam kasus kedua, kita tahu sebelumnya bahwa objek yang diteruskan ke konstruktor kita segera karena kehancuran, dan itu tidak harus tetap tak tersentuh. Kami dapat secara efektif hanya menukar pointer internal dan tidak melakukan penyalinan sama sekali dalam skenario ini, yang jauh lebih efisien. Memindahkan semantik menguntungkan setiap kelas yang mahal atau melarang penyalinan sumber daya yang dirujuk secara internal. Pertimbangkan kasus std::unique_ptr- sekarang kelas kita dapat membedakan antara temporaries dan non-temporaries, kita dapat membuat gerakan semantik bekerja dengan benar sehinggaunique_ptr tidak dapat disalin tetapi dapat dipindahkan, yang berarti bahwastd::unique_ptrdapat disimpan secara legal dalam kontainer Standar, disortir, dll, sedangkan C ++ 03 std::auto_ptrtidak bisa.

Sekarang kami mempertimbangkan penggunaan lain dari referensi nilai-penerusan sempurna. Pertimbangkan pertanyaan tentang mengikat referensi ke referensi.

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

Tidak dapat mengingat apa yang dikatakan C ++ 03 tentang ini, tetapi dalam C ++ 0x, tipe yang dihasilkan ketika berhadapan dengan referensi nilai adalah sangat penting. Referensi nilai untuk tipe T, di mana T adalah tipe referensi, menjadi referensi tipe 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&&

Pertimbangkan fungsi template paling sederhana - minimal dan maks. Dalam C ++ 03 Anda harus membebani semua kombinasi const dan non-const secara manual. Dalam C ++ 0x itu hanya satu kelebihan. Dikombinasikan dengan templat variadic, ini memungkinkan penerusan yang sempurna.

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

Saya mengabaikan pengurangan jenis pengembalian, karena saya tidak dapat mengingat bagaimana hal itu dilakukan begitu saja, tetapi min itu dapat menerima kombinasi dari setiap nilai, nilai, dan nilai.

Anak anjing
sumber
mengapa kamu digunakan std::forward<A>(aref) < std::forward<B>(bref)? dan saya tidak berpikir definisi ini akan benar ketika Anda mencoba maju int&dan float&. Lebih baik jatuhkan satu templat formulir tipe.
Yankes
25

Istilah untuk T&& saat digunakan dengan pengurangan tipe (seperti untuk penerusan yang sempurna) dikenal dengan bahasa sehari-hari sebagai referensi penerusan . Istilah "referensi universal" diciptakan oleh Scott Meyers dalam artikel ini , tetapi kemudian diubah.

Itu karena mungkin nilai-r atau nilai-l.

Contohnya adalah:

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

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

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

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

Diskusi lebih lanjut dapat ditemukan dalam jawaban untuk: Sintaks untuk referensi universal

mmocny
sumber
14

Referensi nilai adalah jenis yang berperilaku seperti referensi biasa X &, dengan beberapa pengecualian. Yang paling penting adalah bahwa ketika datang ke fungsi resolusi overload, nilai-nilai lebih suka referensi nilai-gaya lama, sedangkan nilai-nilai lebih suka referensi nilai-nilai baru:

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

Jadi apa itu nilai? Apa pun yang bukan nilai. Nilai lebih merupakan ekspresi yang merujuk ke lokasi memori dan memungkinkan kami untuk mengambil alamat lokasi memori itu melalui operator &.

Hampir lebih mudah untuk memahami terlebih dahulu apa yang dicapai oleh nilai dengan contoh:

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

Operator konstruktor dan tugas telah kelebihan beban dengan versi yang mengambil referensi nilai. Referensi nilai memungkinkan fungsi untuk bercabang pada waktu kompilasi (melalui resolusi overload) dengan syarat "Apakah saya dipanggil pada nilai lebih atau nilai?". Ini memungkinkan kami untuk membuat operator konstruktor dan penugasan yang lebih efisien di atas yang memindahkan sumber daya alih-alih menyalinnya.

Compiler secara otomatis bercabang pada waktu kompilasi (tergantung pada apakah ia dipanggil untuk nilai lebih atau nilai) memilih apakah operator konstruktor atau pemindah tugas harus dipanggil.

Kesimpulannya: referensi nilai memungkinkan semantik bergerak (dan penerusan sempurna, dibahas di tautan artikel di bawah).

Salah satu contoh praktis yang mudah dipahami adalah template kelas std :: unique_ptr . Karena unique_ptr mempertahankan kepemilikan eksklusif dari pointer mentah yang mendasarinya, unique_ptr tidak dapat disalin. Itu akan melanggar invarian kepemilikan eksklusif mereka. Jadi mereka tidak memiliki copy constructor. Tetapi mereka memiliki konstruktor bergerak:

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)biasanya dilakukan menggunakan std :: move

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

Artikel yang sangat bagus menjelaskan semua ini dan banyak lagi (seperti bagaimana nilai memungkinkan penerusan sempurna dan apa artinya itu) dengan banyak contoh bagus adalah Dijelaskan oleh Thomas Becker . Posting ini sangat bergantung pada artikelnya.

Pengantar yang lebih pendek adalah Pengantar Singkat untuk Menilai Nilai oleh Stroutrup, et. Al

kurt krueckeberg
sumber
Bukankah begitu Sample(const Sample& s)juga konstruktor copy perlu menyalin konten? Pertanyaan yang sama untuk 'operator penugasan salinan'.
K.Karamazen
Ya kamu benar. Saya gagal menyalin memori. Copy constructor dan copy assignment operator harus melakukan memcpy (ptr, s.ptr, size) setelah menguji ukuran itu! = 0. Dan konstruktor default harus melakukan memset (ptr, 0, size) jika ukuran! = 0.
kurt krueckeberg
Oke terima kasih. Dengan demikian komentar ini dan dua komentar sebelumnya dapat dihapus karena masalahnya juga telah diperbaiki dalam jawabannya.
K.Karamazen