Nilai C ++ 11 dan memindahkan kebingungan semantik (pernyataan pengembalian)

435

Saya mencoba memahami referensi nilai dan memindahkan semantik C ++ 11.

Apa perbedaan antara contoh-contoh ini, dan mana di antara mereka yang tidak memiliki salinan vektor?

Contoh pertama

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> &&rval_ref = return_vector();

Contoh kedua

std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

Contoh ketiga

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();
Tarantula
sumber
51
Harap jangan kembalikan variabel lokal dengan referensi. Referensi nilai masih merupakan referensi.
fredoverflow
63
Itu jelas disengaja untuk memahami perbedaan semantik antara contoh lol
Tarantula
@ FredOverflow Pertanyaan lama, tetapi saya butuh satu detik untuk memahami komentar Anda. Saya pikir pertanyaan dengan # 2 adalah apakah std::move()membuat "salinan."
3Dave
5
@ DavidLive std::move(expression)tidak membuat apa-apa, itu hanya melemparkan ekspresi ke nilai x. Tidak ada objek yang disalin atau dipindahkan dalam proses evaluasi std::move(expression).
fredoverflow

Jawaban:

563

Contoh pertama

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> &&rval_ref = return_vector();

Contoh pertama mengembalikan sementara yang ditangkap oleh rval_ref. Sementara itu akan memperpanjang umurnya di luar rval_refdefinisi dan Anda dapat menggunakannya seolah-olah Anda telah menangkapnya berdasarkan nilai. Ini sangat mirip dengan yang berikut:

const std::vector<int>& rval_ref = return_vector();

kecuali bahwa dalam penulisan ulang saya, Anda jelas tidak dapat menggunakan rval_refcara non-const.

Contoh kedua

std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

Dalam contoh kedua Anda telah membuat kesalahan run time. rval_refsekarang memegang referensi ke yang rusak tmpdi dalam fungsi. Dengan sedikit keberuntungan, kode ini akan langsung macet.

Contoh ketiga

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

Contoh ketiga Anda kira-kira setara dengan yang pertama. The std::moveon tmptidak perlu dan benar-benar dapat menjadi pessimization kinerja karena akan menghambat optimasi nilai kembali.

Cara terbaik untuk mengkode apa yang Anda lakukan adalah:

Praktek terbaik

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

Yaitu seperti yang Anda lakukan di C ++ 03. tmpsecara implisit diperlakukan sebagai nilai dalam pernyataan pengembalian. Entah itu akan dikembalikan melalui optimasi nilai-kembali (tidak ada salinan, tidak ada gerakan), atau jika kompilator memutuskan tidak dapat melakukan RVO, maka ia akan menggunakan vektor konstruktor bergerak untuk melakukan pengembalian . Hanya jika RVO tidak dilakukan, dan jika tipe yang dikembalikan tidak memiliki konstruktor pemindahan, maka konstruktor salinan akan digunakan untuk pengembalian.

Howard Hinnant
sumber
65
Compiler akan RVO ketika Anda mengembalikan objek lokal dengan nilai, dan tipe lokal dan kembalinya fungsi adalah sama, dan keduanya tidak memenuhi syarat cv (jangan kembalikan jenis const). Tinggal jauh dari kembali dengan pernyataan kondisi (:?) Karena dapat menghambat RVO. Jangan membungkus lokal dalam beberapa fungsi lain yang mengembalikan referensi ke lokal. Adil return my_local;. Beberapa pernyataan pengembalian ok dan tidak akan menghambat RVO.
Howard Hinnant
27
Ada sebuah peringatan: ketika kembali suatu anggota dari objek lokal, langkah harus eksplisit.
boycy
5
@NoSenseEtAl: Tidak ada yang sementara dibuat di baris kembali. movetidak membuat sementara. Ini melemparkan nilai ke nilai x, tidak membuat salinan, membuat apa-apa, tidak merusak apa pun. Contoh itu adalah situasi yang sama persis seperti jika Anda kembali dengan referensi-nilai dan menghapusnya movedari baris kembali: Either way Anda punya referensi menggantung ke variabel lokal di dalam fungsi dan yang telah dihancurkan.
Howard Hinnant
15
"Beberapa pernyataan pengembalian ok dan tidak akan menghambat RVO": Hanya jika mereka mengembalikan variabel yang sama .
Deduplicator
5
@Dupuplikator: Anda benar. Saya tidak berbicara seakurat yang saya maksudkan. Saya maksudkan bahwa beberapa pernyataan pengembalian tidak melarang kompiler dari RVO (meskipun itu membuatnya tidak mungkin untuk diterapkan), dan oleh karena itu ekspresi pengembalian masih dianggap sebagai nilai.
Howard Hinnant
42

Tak satu pun dari mereka akan menyalin, tetapi yang kedua akan merujuk ke vektor yang hancur. Referensi yang dinamai rvalue hampir tidak pernah ada dalam kode reguler. Anda menulisnya persis seperti bagaimana Anda menulis salinan di C ++ 03.

std::vector<int> return_vector()
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

Kecuali sekarang, vektor dipindahkan. The pengguna kelas tidak berurusan dengan itu nilai p referensi dalam sebagian besar kasus.

Anak anjing
sumber
Apakah Anda benar-benar yakin bahwa contoh ketiga akan melakukan copy vektor?
Tarantula
@Tarantula: Ini akan merusak vektor Anda. Apakah itu menyalin atau tidak sebelum melanggar tidak penting.
Puppy
4
Saya tidak melihat alasan untuk busting yang Anda usulkan. Tidak apa-apa untuk mengikat variabel rvalue referensi lokal ke rvalue. Dalam hal itu, masa objek sementara diperpanjang ke masa hidup dari variabel referensi nilai.
fredoverflow
1
Hanya titik klarifikasi, karena saya sedang belajar ini. Dalam contoh baru ini, vektor tmptidak dipindahkan ke rval_ref, tetapi ditulis langsung rval_refmenggunakan RVO (yaitu copy elision). Ada perbedaan antara std::movedan menyalin elision. A std::movemungkin masih melibatkan beberapa data untuk disalin; dalam kasus vektor, vektor baru sebenarnya dibangun di copy constructor dan data dialokasikan, tetapi sebagian besar array data hanya disalin dengan menyalin pointer (pada dasarnya). Salinan salinan menghindari 100% dari semua salinan.
Mark Lakata
@ MarkLakata Ini NRVO, bukan RVO. NRVO adalah opsional, bahkan dalam C ++ 17. Jika tidak diterapkan, nilai balik dan rval_refvariabel dibangun menggunakan move constructor std::vector. Tidak ada copy constructor yang terlibat baik dengan / tanpa std::move. tmpdiperlakukan sebagai nilai p di returnpernyataan dalam kasus ini.
Daniel Langr
16

Jawaban sederhananya adalah Anda harus menulis kode untuk referensi nilai seperti halnya Anda menggunakan kode referensi reguler, dan Anda harus memperlakukan mereka dengan 99% mental yang sama. Ini termasuk semua aturan lama tentang pengembalian referensi (yaitu tidak pernah mengembalikan referensi ke variabel lokal).

Kecuali Anda menulis kelas templat templat yang perlu mengambil keuntungan dari std :: forward dan dapat menulis fungsi generik yang mengambil referensi baik nilai atau nilai, ini lebih atau kurang benar.

Salah satu keuntungan besar untuk memindahkan konstruktor dan memindahkan tugas adalah bahwa jika Anda mendefinisikannya, kompilator dapat menggunakannya dalam kasus-kasus adalah RVO (optimasi nilai kembali) dan NRVO (bernama optimasi nilai pengembalian) gagal dipanggil. Ini cukup besar untuk mengembalikan benda mahal seperti wadah & string dengan nilai secara efisien dari metode.

Sekarang hal-hal yang menarik dengan referensi nilai, adalah Anda juga dapat menggunakannya sebagai argumen untuk fungsi normal. Ini memungkinkan Anda untuk menulis kontainer yang memiliki kelebihan muatan untuk referensi const (const foo & lainnya) dan nilai referensi (foo && lainnya). Sekalipun argumen terlalu berat untuk dilewatkan dengan panggilan konstruktor belaka, masih bisa dilakukan:

std::vector vec;
for(int x=0; x<10; ++x)
{
    // automatically uses rvalue reference constructor if available
    // because MyCheapType is an unamed temporary variable
    vec.push_back(MyCheapType(0.f));
}


std::vector vec;
for(int x=0; x<10; ++x)
{
    MyExpensiveType temp(1.0, 3.0);
    temp.initSomeOtherFields(malloc(5000));

    // old way, passed via const reference, expensive copy
    vec.push_back(temp);

    // new way, passed via rvalue reference, cheap move
    // just don't use temp again,  not difficult in a loop like this though . . .
    vec.push_back(std::move(temp));
}

Wadah STL telah diperbarui untuk memindahkan kelebihan muatan untuk hampir semua hal (kunci hash dan nilai, penyisipan vektor, dll), dan di sinilah Anda paling sering melihatnya.

Anda juga dapat menggunakannya untuk fungsi normal, dan jika Anda hanya memberikan argumen referensi nilai, Anda dapat memaksa pemanggil untuk membuat objek dan membiarkan fungsi melakukan gerakan. Ini lebih merupakan contoh daripada penggunaan yang benar-benar baik, tetapi di perpustakaan rendering saya, saya telah menetapkan string ke semua sumber daya yang dimuat, sehingga lebih mudah untuk melihat apa yang diwakili setiap objek dalam debugger. Tampilannya kira-kira seperti ini:

TextureHandle CreateTexture(int width, int height, ETextureFormat fmt, string&& friendlyName)
{
    std::unique_ptr<TextureObject> tex = D3DCreateTexture(width, height, fmt);
    tex->friendlyName = std::move(friendlyName);
    return tex;
}

Ini adalah bentuk 'abstraksi bocor' tetapi memungkinkan saya untuk mengambil keuntungan dari fakta bahwa saya harus membuat string sudah sebagian besar waktu, dan menghindari membuat salinan lain dari itu. Ini bukan kode berkinerja tinggi tetapi merupakan contoh yang baik tentang kemungkinan orang memahami fitur ini. Kode ini sebenarnya mengharuskan variabel menjadi sementara untuk panggilan, atau std :: move dipanggil:

// move from temporary
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string("Checkerboard"));

atau

// explicit move (not going to use the variable 'str' after the create call)
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, std::move(str));

atau

// explicitly make a copy and pass the temporary of the copy down
// since we need to use str again for some reason
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string(str));

tetapi ini tidak akan dikompilasi!

string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, str);
Zoner
sumber
3

Bukan jawaban semata , tetapi sebuah pedoman. Sebagian besar waktu tidak ada artinya dalam mendeklarasikan T&&variabel lokal (seperti yang Anda lakukan dengan std::vector<int>&& rval_ref). Anda masih harus std::move()menggunakannya untuk menggunakan foo(T&&)metode tipe. Ada juga masalah yang sudah disebutkan bahwa ketika Anda mencoba mengembalikannyarval_ref dari fungsi Anda akan mendapatkan referensi standar-untuk-hancur-sementara-kegagalan.

Sebagian besar waktu saya akan pergi dengan pola berikut:

// Declarations
A a(B&&, C&&);
B b();
C c();

auto ret = a(b(), c());

Anda tidak memegang referensi untuk mengembalikan objek sementara, sehingga Anda menghindari kesalahan programmer (yang tidak berpengalaman) yang ingin menggunakan objek yang dipindahkan.

auto bRet = b();
auto cRet = c();
auto aRet = a(std::move(b), std::move(c));

// Either these just fail (assert/exception), or you won't get 
// your expected results due to their clean state.
bRet.foo();
cRet.bar();

Jelas ada (walaupun agak jarang) kasus di mana fungsi benar-benar mengembalikan T&&yang merupakan referensi ke objek non-temporer yang dapat Anda pindahkan ke objek Anda.

Mengenai RVO: mekanisme ini umumnya bekerja dan kompiler dapat dengan baik menghindari penyalinan, tetapi dalam kasus di mana jalur pengembalian tidak jelas (pengecualian, ifpersyaratan menentukan objek bernama Anda akan kembali, dan mungkin beberapa orang lain) rref adalah penyelamat Anda (bahkan jika berpotensi lebih mahal).

Merah XIII
sumber
2

Tak satu pun dari mereka akan melakukan penyalinan tambahan. Bahkan jika RVO tidak digunakan, standar baru mengatakan bahwa memindahkan konstruksi lebih disukai untuk menyalin ketika melakukan pengembalian saya percaya.

Saya percaya bahwa contoh kedua Anda menyebabkan perilaku tidak terdefinisi karena Anda mengembalikan referensi ke variabel lokal.

Edward Strange
sumber
1

Seperti yang telah disebutkan dalam komentar untuk jawaban pertama, return std::move(...);konstruk dapat membuat perbedaan dalam kasus selain mengembalikan variabel lokal. Berikut adalah contoh runnable yang mendokumentasikan apa yang terjadi ketika Anda mengembalikan objek anggota dengan dan tanpa std::move():

#include <iostream>
#include <utility>

struct A {
  A() = default;
  A(const A&) { std::cout << "A copied\n"; }
  A(A&&) { std::cout << "A moved\n"; }
};

class B {
  A a;
 public:
  operator A() const & { std::cout << "B C-value: "; return a; }
  operator A() & { std::cout << "B L-value: "; return a; }
  operator A() && { std::cout << "B R-value: "; return a; }
};

class C {
  A a;
 public:
  operator A() const & { std::cout << "C C-value: "; return std::move(a); }
  operator A() & { std::cout << "C L-value: "; return std::move(a); }
  operator A() && { std::cout << "C R-value: "; return std::move(a); }
};

int main() {
  // Non-constant L-values
  B b;
  C c;
  A{b};    // B L-value: A copied
  A{c};    // C L-value: A moved

  // R-values
  A{B{}};  // B R-value: A copied
  A{C{}};  // C R-value: A moved

  // Constant L-values
  const B bc;
  const C cc;
  A{bc};   // B C-value: A copied
  A{cc};   // C C-value: A copied

  return 0;
}

Agaknya, return std::move(some_member);hanya masuk akal jika Anda benar-benar ingin memindahkan anggota kelas tertentu, misalnya dalam kasus di mana class Cmewakili objek adaptor berumur pendek dengan satu-satunya tujuan membuat instance daristruct A .

Perhatikan bagaimana struct Aselalu mendapat disalin dari class B, bahkan ketika class Bobjek adalah R-nilai. Ini karena kompiler tidak memiliki cara untuk mengatakan bahwa class Binstance struct Atidak akan digunakan lagi. Dalam class C, compiler tidak memiliki informasi ini dari std::move(), yang mengapa struct Aakan dipindahkan , kecuali contoh class Cadalah konstan.

Andrej Podzimek
sumber