Jawaban pertama saya adalah pengantar yang sangat disederhanakan untuk memindahkan semantik, dan banyak detail yang sengaja dibuat untuk membuatnya tetap sederhana. Namun, ada banyak lagi untuk memindahkan semantik, dan saya pikir sudah waktunya untuk jawaban kedua untuk mengisi kekosongan. Jawaban pertama sudah cukup lama, dan rasanya tidak benar hanya menggantinya dengan teks yang sama sekali berbeda. Saya pikir ini masih berfungsi dengan baik sebagai pengantar pertama. Tetapi jika Anda ingin menggali lebih dalam, baca terus :)
Stephan T. Lavavej meluangkan waktu untuk memberikan umpan balik yang berharga. Terima kasih banyak, Stephan!
pengantar
Pindah semantik memungkinkan suatu objek, dalam kondisi tertentu, untuk mengambil kepemilikan sumber daya eksternal beberapa objek lain. Ini penting dalam dua cara:
Mengubah salinan mahal menjadi gerakan murah. Lihat jawaban pertama saya sebagai contoh. Perhatikan bahwa jika suatu objek tidak mengelola setidaknya satu sumber daya eksternal (baik secara langsung, atau tidak langsung melalui objek anggotanya), pindahkan semantik tidak akan menawarkan keuntungan apa pun dibandingkan salinan semantik. Dalam hal itu, menyalin objek dan memindahkan objek berarti hal yang sama persis:
class cannot_benefit_from_move_semantics
{
int a; // moving an int means copying an int
float b; // moving a float means copying a float
double c; // moving a double means copying a double
char d[64]; // moving a char array means copying a char array
// ...
};
Menerapkan tipe aman "hanya bergerak"; yaitu, jenis-jenis yang penyalinannya tidak masuk akal, tetapi pemindahan tidak. Contohnya termasuk kunci, pegangan file, dan petunjuk pintar dengan semantik kepemilikan yang unik. Catatan: Jawaban ini membahas std::auto_ptr
, templat perpustakaan standar C ++ 98 yang sudah tidak digunakan lagi, yang digantikan oleh std::unique_ptr
dalam C ++ 11. Pemrogram C ++ menengah mungkin paling tidak agak familiar std::auto_ptr
, dan karena "pindahkan semantik" yang ditampilkannya, sepertinya merupakan titik awal yang baik untuk membahas pindahan semantik di C ++ 11. YMMV.
Apa itu langkah?
Pustaka standar C ++ 98 menawarkan penunjuk cerdas dengan semantik kepemilikan unik yang disebut std::auto_ptr<T>
. Jika Anda belum terbiasa auto_ptr
, tujuannya adalah untuk menjamin bahwa objek yang dialokasikan secara dinamis selalu dirilis, bahkan dalam menghadapi pengecualian:
{
std::auto_ptr<Shape> a(new Triangle);
// ...
// arbitrary code, could throw exceptions
// ...
} // <--- when a goes out of scope, the triangle is deleted automatically
Hal yang tidak biasa auto_ptr
adalah perilaku "menyalin":
auto_ptr<Shape> a(new Triangle);
+---------------+
| triangle data |
+---------------+
^
|
|
|
+-----|---+
| +-|-+ |
a | p | | | |
| +---+ |
+---------+
auto_ptr<Shape> b(a);
+---------------+
| triangle data |
+---------------+
^
|
+----------------------+
|
+---------+ +-----|---+
| +---+ | | +-|-+ |
a | p | | | b | p | | | |
| +---+ | | +---+ |
+---------+ +---------+
Perhatikan bagaimana inisialisasi b
dengan a
tidak tidak menyalin segitiga, melainkan transfer kepemilikan segitiga dari a
ke b
. Kami juga mengatakan " a
ini dipindahkan ke b
" atau "segitiga tersebut dipindahkan dari a
ke b
". Ini mungkin terdengar membingungkan karena segitiga itu sendiri selalu berada di tempat yang sama dalam memori.
Untuk memindahkan objek berarti memindahkan kepemilikan atas beberapa sumber daya yang dikelolanya ke objek lain.
Pembuat salinan dari auto_ptr
mungkin terlihat seperti ini (agak disederhanakan):
auto_ptr(auto_ptr& source) // note the missing const
{
p = source.p;
source.p = 0; // now the source no longer owns the object
}
Bergerak berbahaya dan tidak berbahaya
Yang berbahaya tentang auto_ptr
apa yang secara sintaksis tampak seperti salinan sebenarnya adalah sebuah langkah. Mencoba memanggil fungsi anggota pada pindah-dari auto_ptr
akan memanggil perilaku yang tidak terdefinisi, jadi Anda harus sangat berhati-hati untuk tidak menggunakan auto_ptr
setelah itu telah dipindahkan dari:
auto_ptr<Shape> a(new Triangle); // create triangle
auto_ptr<Shape> b(a); // move a into b
double area = a->area(); // undefined behavior
Namun auto_ptr
tidak selalu berbahaya. Fungsi pabrik adalah kasus penggunaan yang sangat baik untuk auto_ptr
:
auto_ptr<Shape> make_triangle()
{
return auto_ptr<Shape>(new Triangle);
}
auto_ptr<Shape> c(make_triangle()); // move temporary into c
double area = make_triangle()->area(); // perfectly safe
Perhatikan bagaimana kedua contoh mengikuti pola sintaksis yang sama:
auto_ptr<Shape> variable(expression);
double area = expression->area();
Namun, salah satu dari mereka memanggil perilaku yang tidak terdefinisi, sedangkan yang lain tidak. Jadi apa perbedaan antara ekspresi a
dan make_triangle()
? Bukankah keduanya memiliki tipe yang sama? Memang benar, tetapi mereka memiliki kategori nilai yang berbeda .
Kategori nilai
Jelas, harus ada beberapa perbedaan besar antara ekspresi a
yang menunjukkan auto_ptr
variabel, dan ekspresi make_triangle()
yang menunjukkan panggilan fungsi yang mengembalikan auto_ptr
nilai, sehingga menciptakan auto_ptr
objek sementara yang baru setiap kali dipanggil. a
adalah contoh dari sebuah lvalue , sedangkan make_triangle()
adalah contoh dari nilai p .
Pindah dari nilai-nilai seperti a
itu berbahaya, karena kita nanti bisa mencoba memanggil fungsi anggota melalui a
, memanggil perilaku yang tidak terdefinisi. Di sisi lain, pindah dari nilai-nilai seperti make_triangle()
sangat aman, karena setelah copy constructor melakukan tugasnya, kita tidak bisa menggunakan yang sementara lagi. Tidak ada ungkapan yang menunjukkan kata sementara; jika kita hanya menulis make_triangle()
lagi, kita mendapatkan perbedaan sementara. Bahkan, pindah-dari sementara sudah hilang di baris berikutnya:
auto_ptr<Shape> c(make_triangle());
^ the moved-from temporary dies right here
Perhatikan bahwa surat-surat l
dan r
memiliki asal bersejarah di sisi kiri dan kanan tugas. Ini tidak lagi benar dalam C ++, karena ada nilai yang tidak dapat muncul di sisi kiri suatu tugas (seperti array atau tipe yang ditentukan pengguna tanpa operator penugasan), dan ada nilai yang bisa (semua nilai dari tipe kelas dengan operator penugasan).
Nilai dari tipe kelas adalah ekspresi yang evaluasinya menciptakan objek sementara. Dalam keadaan normal, tidak ada ekspresi lain di dalam lingkup yang sama menunjukkan objek sementara yang sama.
Referensi nilai
Kita sekarang mengerti bahwa pindah dari nilai-nilai berpotensi berbahaya, tetapi bergerak dari nilai-nilai tidak berbahaya. Jika C ++ memiliki dukungan bahasa untuk membedakan argumen lvalue dari argumen rvalue, kami dapat melarang sepenuhnya pindah dari lvalues, atau setidaknya membuat bergerak dari lvalues secara eksplisit di situs panggilan, sehingga kami tidak lagi bergerak secara tidak sengaja.
Jawaban C ++ 11 untuk masalah ini adalah referensi nilai . Referensi nilai adalah jenis referensi baru yang hanya mengikat nilai, dan sintaksnya X&&
. Referensi lama yang baik X&
sekarang dikenal sebagai referensi nilai . (Perhatikan bahwa X&&
adalah tidak referensi ke referensi, tidak ada hal seperti itu di C ++.)
Jika kita memasukkan const
campuran, kita sudah memiliki empat jenis referensi. Jenis ekspresi seperti apa yang X
bisa mereka ikat?
lvalue const lvalue rvalue const rvalue
---------------------------------------------------------
X& yes
const X& yes yes yes yes
X&& yes
const X&& yes yes
Dalam praktiknya, Anda bisa melupakannya const X&&
. Terbatas untuk membaca dari nilai tidak sangat berguna.
Referensi nilai X&&
adalah jenis referensi baru yang hanya mengikat nilai saja.
Konversi tersirat
Referensi nilai melewati beberapa versi. Sejak versi 2.1, referensi nilai X&&
juga mengikat semua kategori nilai dari jenis yang berbeda Y
, asalkan ada konversi implisit dari Y
ke X
. Dalam hal itu, sementara jenis X
dibuat, dan referensi nilai terikat untuk sementara itu:
void some_function(std::string&& r);
some_function("hello world");
Dalam contoh di atas, "hello world"
adalah lvalue tipe const char[12]
. Karena ada konversi implisit dari const char[12]
melalui const char*
ke std::string
, sebuah sementara tipe std::string
dibuat, dan r
pasti bahwa sementara. Ini adalah salah satu kasus di mana perbedaan antara rvalues (ekspresi) dan temporaries (objek) agak buram.
Pindahkan konstruktor
Contoh berguna dari fungsi dengan X&&
parameter adalah move constructor X::X(X&& source)
. Tujuannya adalah untuk mentransfer kepemilikan sumber daya yang dikelola dari sumber ke objek saat ini.
Dalam C ++ 11, std::auto_ptr<T>
telah digantikan oleh std::unique_ptr<T>
yang mengambil keuntungan dari referensi nilai. Saya akan mengembangkan dan mendiskusikan versi sederhana dari unique_ptr
. Pertama, kami merangkum pointer mentah dan membebani operator ->
dan *
, jadi kelas kami terasa seperti pointer:
template<typename T>
class unique_ptr
{
T* ptr;
public:
T* operator->() const
{
return ptr;
}
T& operator*() const
{
return *ptr;
}
Konstruktor mengambil kepemilikan objek, dan destruktor menghapusnya:
explicit unique_ptr(T* p = nullptr)
{
ptr = p;
}
~unique_ptr()
{
delete ptr;
}
Sekarang sampai pada bagian yang menarik, move constructor:
unique_ptr(unique_ptr&& source) // note the rvalue reference
{
ptr = source.ptr;
source.ptr = nullptr;
}
Konstruktor gerakan ini melakukan persis apa yang dilakukan auto_ptr
konstruktor salin, tetapi hanya dapat diberikan dengan nilai:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // error
unique_ptr<Shape> c(make_triangle()); // okay
Baris kedua gagal dikompilasi, karena a
merupakan nilai lv, tetapi parameter unique_ptr&& source
hanya dapat terikat ke nilai. Inilah yang kami inginkan; gerakan berbahaya tidak boleh implisit. Baris ketiga mengkompilasi dengan baik, karena make_triangle()
merupakan nilai. Konstruktor pemindahan akan mengalihkan kepemilikan dari sementara ke c
. Sekali lagi, inilah yang kami inginkan.
Konstruktor pemindahan memindahkan kepemilikan sumber daya yang dikelola ke objek saat ini.
Pindahkan operator penugasan
Bagian yang hilang terakhir adalah operator penugasan pindah. Tugasnya adalah melepaskan sumber daya lama dan memperoleh sumber daya baru dari argumennya:
unique_ptr& operator=(unique_ptr&& source) // note the rvalue reference
{
if (this != &source) // beware of self-assignment
{
delete ptr; // release the old resource
ptr = source.ptr; // acquire the new resource
source.ptr = nullptr;
}
return *this;
}
};
Perhatikan bagaimana implementasi operator penugasan langkah ini menduplikasi logika destruktor dan konstruktor bergerak. Apakah Anda terbiasa dengan idiom copy-and-swap? Ini juga dapat diterapkan untuk memindahkan semantik sebagai idiom move-and-swap:
unique_ptr& operator=(unique_ptr source) // note the missing reference
{
std::swap(ptr, source.ptr);
return *this;
}
};
Sekarang itu source
adalah variabel tipe unique_ptr
, itu akan diinisialisasi oleh move constructor; artinya, argumen akan dipindahkan ke parameter. Argumen masih diperlukan untuk menjadi nilai, karena konstruktor bergerak itu sendiri memiliki parameter referensi nilai. Ketika aliran kontrol mencapai penjepit penutupan operator=
, source
keluar dari ruang lingkup, melepaskan sumber daya lama secara otomatis.
Operator penugasan memindahkan kepemilikan dari sumber daya yang dikelola ke objek saat ini, melepaskan sumber daya lama. Ungkapan move-and-swap mempermudah implementasi.
Pindah dari nilai-nilai
Terkadang, kami ingin beralih dari nilai-nilai. Yaitu, kadang-kadang kita ingin kompiler memperlakukan nilai l seolah-olah itu nilai, sehingga dapat memanggil move constructor, meskipun itu bisa berpotensi tidak aman. Untuk tujuan ini, C ++ 11 menawarkan templat fungsi pustaka standar yang disebut std::move
di dalam header <utility>
. Nama ini agak disayangkan, karena std::move
hanya melemparkan nilai ke nilai; itu tidak memindahkan apa pun dengan sendirinya. Itu hanya memungkinkan bergerak. Mungkin seharusnya dinamai std::cast_to_rvalue
atau std::enable_move
, tapi kita terjebak dengan namanya sekarang.
Inilah cara Anda secara eksplisit beralih dari nilai:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // still an error
unique_ptr<Shape> c(std::move(a)); // okay
Perhatikan bahwa setelah baris ketiga, a
tidak lagi memiliki segitiga. Tidak apa-apa, karena dengan menulis secara eksplisitstd::move(a)
, kami membuat niat kami jelas: "Konstruktor yang terhormat, lakukan apa pun yang Anda inginkan a
untuk menginisialisasi c
; Saya tidak peduli a
lagi. Jangan ragu untuk membantu Anda a
."
std::move(some_lvalue)
melemparkan nilai ke nilai, sehingga memungkinkan langkah selanjutnya.
Nilai X
Perhatikan bahwa meskipun std::move(a)
merupakan nilai, evaluasinya tidak membuat objek sementara. Teka-teki ini memaksa komite untuk memperkenalkan kategori nilai ketiga. Sesuatu yang dapat terikat pada referensi nilai, meskipun itu bukan nilai dalam arti tradisional, disebut nilai x (nilai eXpiring). Nilai-nilai tradisional diubah namanya menjadi nilai-nilai (Nilai murni).
Baik prvalues dan xvalues adalah rvalues. Xvalues dan lvalues keduanya adalah glvalues (Generalized lvalues). Hubungan lebih mudah dipahami dengan diagram:
expressions
/ \
/ \
/ \
glvalues rvalues
/ \ / \
/ \ / \
/ \ / \
lvalues xvalues prvalues
Perhatikan bahwa hanya nilai x yang benar-benar baru; sisanya hanya karena penggantian nama dan pengelompokan.
Nilai C ++ 98 dikenal sebagai nilai di C ++ 11. Gantikan secara mental semua kemunculan "nilai" pada paragraf sebelumnya dengan "nilai".
Keluar dari fungsi
Sejauh ini, kita telah melihat pergerakan ke variabel lokal, dan ke dalam parameter fungsi. Tetapi bergerak juga mungkin terjadi dalam arah yang berlawanan. Jika fungsi kembali berdasarkan nilai, beberapa objek di situs panggilan (mungkin variabel lokal atau sementara, tetapi bisa berupa objek apa pun) diinisialisasi dengan ekspresi setelah return
pernyataan sebagai argumen ke konstruktor bergerak:
unique_ptr<Shape> make_triangle()
{
return unique_ptr<Shape>(new Triangle);
} \-----------------------------/
|
| temporary is moved into c
|
v
unique_ptr<Shape> c(make_triangle());
Mungkin mengejutkan, objek otomatis (variabel lokal yang tidak dideklarasikan sebagai static
) juga dapat secara implisit dipindahkan dari fungsi:
unique_ptr<Shape> make_square()
{
unique_ptr<Shape> result(new Square);
return result; // note the missing std::move
}
Kenapa konstruktor langkah menerima nilai result
sebagai argumen? Ruang lingkup result
akan berakhir, dan itu akan hancur selama tumpukan unwinding. Tidak ada yang bisa mengeluh setelah itu yang result
entah bagaimana berubah; ketika aliran kontrol kembali ke pemanggil, result
tidak ada lagi! Untuk alasan itu, C ++ 11 memiliki aturan khusus yang memungkinkan pengembalian objek otomatis dari fungsi tanpa harus menulis std::move
. Bahkan, Anda tidak boleh menggunakan std::move
untuk memindahkan objek otomatis dari fungsi, karena ini menghambat "optimasi nilai pengembalian bernama" (NRVO).
Jangan pernah gunakan std::move
untuk memindahkan objek otomatis dari fungsi.
Perhatikan bahwa di kedua fungsi pabrik, tipe pengembalian adalah nilai, bukan referensi nilai. Referensi nilai masih referensi, dan seperti biasa, Anda tidak boleh mengembalikan referensi ke objek otomatis; penelepon akan berakhir dengan referensi menggantung jika Anda menipu kompiler agar menerima kode Anda, seperti ini:
unique_ptr<Shape>&& flawed_attempt() // DO NOT DO THIS!
{
unique_ptr<Shape> very_bad_idea(new Square);
return std::move(very_bad_idea); // WRONG!
}
Jangan pernah mengembalikan objek otomatis dengan referensi nilai. Bergerak secara eksklusif dilakukan oleh move constructor, bukan oleh std::move
, dan bukan hanya dengan mengikat nilai ke referensi nilai.
Pindah ke anggota
Cepat atau lambat, Anda akan menulis kode seperti ini:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(parameter) // error
{}
};
Pada dasarnya, kompiler akan mengeluh bahwa itu parameter
adalah nilai. Jika Anda melihat tipenya, Anda melihat referensi nilai, tetapi referensi nilai berarti "referensi yang terikat dengan nilai"; itu tidak berarti bahwa referensi itu sendiri adalah nilai! Memang, parameter
hanya variabel biasa dengan nama. Anda dapat menggunakan parameter
sesering mungkin di dalam tubuh konstruktor, dan selalu menunjukkan objek yang sama. Secara implisit pindah darinya akan berbahaya, karena itu bahasa melarangnya.
Referensi nilai yang dinamai adalah nilai, seperti halnya variabel lainnya.
Solusinya adalah mengaktifkan langkah secara manual:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(std::move(parameter)) // note the std::move
{}
};
Anda bisa membantah bahwa parameter
tidak digunakan lagi setelah inisialisasi member
. Mengapa tidak ada aturan khusus untuk memasukkan secara diam-diam std::move
seperti halnya dengan nilai pengembalian? Mungkin karena akan terlalu banyak beban pada implementor compiler. Misalnya, bagaimana jika badan konstruktor ada di unit terjemahan lain? Sebaliknya, aturan nilai balik hanya perlu memeriksa tabel simbol untuk menentukan apakah pengidentifikasi setelah return
kata kunci menunjukkan objek otomatis.
Anda juga dapat melewati nilai parameter
berdasarkan. Untuk tipe bergerak saja seperti unique_ptr
, sepertinya belum ada idiom yang mapan. Secara pribadi, saya lebih suka memberikan nilai, karena menyebabkan lebih sedikit kekacauan dalam antarmuka.
Fungsi anggota khusus
C ++ 98 secara implisit mendeklarasikan tiga fungsi anggota khusus sesuai permintaan, yaitu, ketika dibutuhkan di suatu tempat: copy constructor, operator penugasan copy, dan destructor.
X::X(const X&); // copy constructor
X& X::operator=(const X&); // copy assignment operator
X::~X(); // destructor
Referensi nilai melewati beberapa versi. Sejak versi 3.0, C ++ 11 menyatakan dua fungsi anggota khusus tambahan sesuai permintaan: konstruktor gerakan dan operator penugasan langkah. Perhatikan bahwa baik VC10 maupun VC11 belum sesuai dengan versi 3.0, jadi Anda harus mengimplementasikannya sendiri.
X::X(X&&); // move constructor
X& X::operator=(X&&); // move assignment operator
Dua fungsi anggota khusus yang baru ini hanya secara implisit dinyatakan jika tidak ada fungsi anggota khusus yang dinyatakan secara manual. Juga, jika Anda mendeklarasikan move constructor Anda sendiri atau memindahkan operator penugasan, baik copy constructor maupun operator penugasan salinan tidak akan dinyatakan secara implisit.
Apa arti aturan ini dalam praktik?
Jika Anda menulis kelas tanpa sumber daya yang tidak dikelola, Anda tidak perlu mendeklarasikan salah satu dari lima fungsi anggota khusus itu sendiri, dan Anda akan mendapatkan salinan semantik yang benar dan memindahkan semantik secara gratis. Jika tidak, Anda harus mengimplementasikan sendiri fungsi anggota khusus. Tentu saja, jika kelas Anda tidak mendapatkan manfaat dari semantik bergerak, tidak perlu menerapkan operasi langkah khusus.
Perhatikan bahwa operator penugasan salinan dan operator penugasan bergerak dapat digabungkan menjadi satu operator penugasan tunggal, yang mengambil argumen berdasarkan nilai:
X& X::operator=(X source) // unified assignment operator
{
swap(source); // see my first answer for an explanation
return *this;
}
Dengan cara ini, jumlah fungsi anggota khusus untuk mengimplementasikan turun dari lima menjadi empat. Ada tradeoff antara pengecualian-keselamatan dan efisiensi di sini, tapi saya bukan ahli dalam masalah ini.
Referensi penerusan ( sebelumnya dikenal sebagai referensi Universal )
Pertimbangkan templat fungsi berikut:
template<typename T>
void foo(T&&);
Anda mungkin berharap T&&
hanya mengikat nilai, karena pada pandangan pertama, itu terlihat seperti referensi nilai. Namun ternyata, T&&
juga mengikat nilai-nilai:
foo(make_triangle()); // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a); // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&
Jika argumennya adalah nilai jenis X
, T
disimpulkan X
, maka T&&
berarti X&&
. Inilah yang orang harapkan. Tetapi jika argumen adalah nilai jenis X
, karena aturan khusus, T
disimpulkan X&
, maka T&&
akan berarti sesuatu seperti X& &&
. Tapi karena C ++ masih tidak memiliki gagasan referensi ke referensi, tipe X& &&
ini diciutkan menjadi X&
. Ini mungkin terdengar membingungkan dan tidak berguna pada awalnya, tetapi runtuh referensi sangat penting untuk penerusan yang sempurna (yang tidak akan dibahas di sini).
T&& bukanlah referensi nilai, tetapi referensi penerusan. Ini juga mengikat nilai-nilai, dalam hal ini T
dan T&&
keduanya merupakan referensi nilai.
Jika Anda ingin membatasi templat fungsi ke nilai, Anda bisa menggabungkan SFINAE dengan tipe traits:
#include <type_traits>
template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);
Implementasi langkah
Sekarang setelah Anda memahami runtuhnya referensi, berikut ini cara std::move
penerapannya:
template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
Seperti yang Anda lihat, move
menerima segala jenis parameter berkat referensi penerusan T&&
, dan mengembalikan referensi nilai. The std::remove_reference<T>::type
meta-fungsi panggilan diperlukan karena jika tidak, untuk lvalues tipe X
, jenis kembali akan X& &&
, yang akan runtuh ke X&
. Karena t
selalu merupakan lvalue (ingat bahwa referensi rvalue bernama adalah lvalue), tetapi kami ingin mengikat t
ke rvalue referensi, kami harus secara eksplisit melemparkan t
ke tipe pengembalian yang benar. Panggilan fungsi yang mengembalikan referensi nilai sendiri merupakan nilai tambah. Sekarang Anda tahu dari mana nilai-nilai berasal;)
Panggilan fungsi yang mengembalikan referensi nilai, seperti std::move
, adalah nilai x.
Perhatikan bahwa mengembalikan dengan referensi nilai baik-baik saja dalam contoh ini, karena t
tidak menunjukkan objek otomatis, tetapi sebaliknya objek yang dilewatkan oleh pemanggil.