Apa yang Auto && beritahu kami?

168

Jika Anda membaca kode suka

auto&& var = foo();

di mana foosetiap fungsi kembali berdasarkan nilai tipe T. Kemudian varadalah nilai referensi tipe nilai T. Tapi apa maksudnya ini var? Apakah ini berarti, kita diizinkan mencuri sumber daya var? Apakah ada situasi yang masuk akal ketika Anda harus auto&&memberi tahu pembaca tentang kode Anda seperti yang Anda lakukan ketika Anda mengembalikannya unique_ptr<>untuk memberi tahu bahwa Anda memiliki kepemilikan eksklusif? Dan bagaimana dengan misalnya T&&kapan Ttipe kelas?

Saya hanya ingin mengerti, jika ada kasus penggunaan lain auto&&selain yang ada di pemrograman template; seperti yang dibahas dalam contoh di artikel ini Referensi Universal oleh Scott Meyers.

MWid
sumber
1
Saya bertanya-tanya hal yang sama. Saya mengerti cara pengurangan jenis bekerja, tetapi apa kode saya katakan ketika saya menggunakan auto&&? Saya telah berpikir untuk melihat mengapa sebuah range-based untuk loop diperluas untuk digunakan auto&&sebagai contoh, tetapi belum berhasil. Mungkin siapa pun yang menjawab dapat menjelaskannya.
Joseph Mansfield
1
Apakah ini sah? Maksud saya instance T dihancurkan dengan segera setelah fookembali, menyimpan referensi nilai sepertinya kedengarannya seperti UB untuk ne.
1
@aleguna Ini sah menurut hukum. Saya tidak ingin mengembalikan referensi atau pointer ke variabel lokal, tetapi sebuah nilai. Fungsi fookekuatan misalnya terlihat seperti: int foo(){return 1;}.
MWid
9
Referensi @aleguna ke temporari melakukan ekstensi seumur hidup, seperti pada C ++ 98.
ecatmur
5
Ekstensi @aleguna seumur hidup hanya berfungsi dengan temporari lokal, bukan fungsi yang mengembalikan referensi. Lihat stackoverflow.com/a/2784304/567292
ecatmur

Jawaban:

232

Dengan menggunakan auto&& var = <initializer>Anda mengatakan: Saya akan menerima inisialisasi apa pun terlepas dari apakah itu adalah ekspresi nilai rendah atau nilai dan saya akan menjaga keteguhannya . Ini biasanya digunakan untuk penerusan (biasanya dengan T&&). Alasan ini berhasil adalah karena "referensi universal", auto&&atau T&&, akan mengikat apa pun .

Anda mungkin berkata, mengapa tidak hanya menggunakan const auto&karena itu juga akan mengikat apa pun? Masalah dengan menggunakan constreferensi adalah itu const! Anda tidak akan dapat kemudian mengikatnya ke referensi non-const atau memohon fungsi anggota yang tidak ditandai const.

Sebagai contoh, bayangkan Anda ingin mendapatkan std::vector, ambil iterator ke elemen pertama dan modifikasi nilai yang ditunjukkan oleh iterator dengan cara:

auto&& vec = some_expression_that_may_be_rvalue_or_lvalue;
auto i = std::begin(vec);
(*i)++;

Kode ini akan dikompilasi dengan baik terlepas dari ekspresi initializer. Alternatif untuk auto&&gagal dengan cara berikut:

auto         => will copy the vector, but we wanted a reference
auto&        => will only bind to modifiable lvalues
const auto&  => will bind to anything but make it const, giving us const_iterator
const auto&& => will bind only to rvalues

Jadi untuk ini, auto&&bekerja dengan sempurna! Contoh menggunakan auto&&seperti ini adalah dalam forlingkaran berbasis rentang . Lihat pertanyaan saya yang lain untuk lebih jelasnya.

Jika kemudian Anda gunakan std::forwardpada auto&&referensi Anda untuk mempertahankan fakta bahwa itu awalnya baik nilai atau nilai, kode Anda mengatakan: Sekarang saya sudah mendapatkan objek Anda dari ekspresi nilai atau nilai, saya ingin mempertahankan mana pun yang menilai itu awalnya sudah jadi saya bisa menggunakannya paling efisien - ini mungkin membatalkannya. Seperti dalam:

auto&& var = some_expression_that_may_be_rvalue_or_lvalue;
// var was initialized with either an lvalue or rvalue, but var itself
// is an lvalue because named rvalues are lvalues
use_it_elsewhere(std::forward<decltype(var)>(var));

Hal ini memungkinkan use_it_elsewhereuntuk menghilangkan nyali untuk kinerja (menghindari salinan) ketika penginisialisasi asli adalah nilai yang dapat dimodifikasi.

Apa artinya ini apakah kita bisa atau kapan kita bisa mencuri sumber daya var? Yah karena auto&&kehendak mengikat apa pun, kita tidak mungkin mencoba untuk merobek varnyali diri kita sendiri - itu mungkin nilai yang lebih baik atau bahkan const. Namun kita bisa std::forwardke fungsi lain yang benar-benar dapat merusak bagian dalamnya. Segera setelah kami melakukan ini, kami harus mempertimbangkan vardalam keadaan tidak valid.

Sekarang mari kita terapkan ini pada kasus auto&& var = foo();, seperti yang diberikan dalam pertanyaan Anda, di mana foo mengembalikan Tnilai berdasarkan. Dalam hal ini kita tahu pasti bahwa jenis varakan disimpulkan sebagai T&&. Karena kita tahu pasti bahwa itu adalah nilai, kita tidak perlu std::forwardizin untuk mencuri sumber dayanya. Dalam kasus khusus ini, mengetahui bahwa fookembali dengan nilai , pembaca harus membacanya sebagai: Saya mengambil referensi nilai untuk sementara kembali dari foo, jadi saya bisa dengan senang hati pindah dari itu.


Sebagai tambahan, saya pikir ada baiknya menyebutkan kapan ekspresi seperti some_expression_that_may_be_rvalue_or_lvaluemungkin muncul, selain situasi "baik kode Anda mungkin berubah". Jadi, inilah contoh yang dibuat:

std::vector<int> global_vec{1, 2, 3, 4};

template <typename T>
T get_vector()
{
  return global_vec;
}

template <typename T>
void foo()
{
  auto&& vec = get_vector<T>();
  auto i = std::begin(vec);
  (*i)++;
  std::cout << vec[0] << std::endl;
}

Di sini, get_vector<T>()adalah ekspresi indah yang bisa berupa nilai atau nilai tergantung pada jenis generik T. Kami pada dasarnya mengubah jenis pengembalian get_vectormelalui parameter templat foo.

Ketika kami menelepon foo<std::vector<int>>, get_vectorakan kembali global_vecdengan nilai, yang memberikan ekspresi nilai. Atau, saat kita menelepon foo<std::vector<int>&>, get_vectorakan kembali global_vecdengan referensi, menghasilkan ekspresi nilai yang lebih tinggi.

Jika kita melakukannya:

foo<std::vector<int>>();
std::cout << global_vec[0] << std::endl;
foo<std::vector<int>&>();
std::cout << global_vec[0] << std::endl;

Kami mendapatkan output berikut, seperti yang diharapkan:

2
1
2
2

Jika Anda adalah untuk mengubah auto&&dalam kode untuk setiap auto, auto&, const auto&, atau const auto&&maka kita tidak akan mendapatkan hasil yang kita inginkan.


Cara alternatif untuk mengubah logika program berdasarkan pada apakah auto&&referensi Anda diinisialisasi dengan ekspresi lvalue atau rvalue adalah dengan menggunakan ciri-ciri tipe:

if (std::is_lvalue_reference<decltype(var)>::value) {
  // var was initialised with an lvalue expression
} else if (std::is_rvalue_reference<decltype(var)>::value) {
  // var was initialised with an rvalue expression
}
Joseph Mansfield
sumber
2
Tidak bisakah kita mengatakan T vec = get_vector<T>();fungsi dalam foo? Atau apakah saya menyederhanakannya ke tingkat yang tidak masuk akal :)
Asterisk
@Asterisk Tidak ada bcoz T vec hanya dapat ditugaskan untuk nilai dalam kasus std :: vector <int &> dan jika T adalah std :: vector <int> maka kita akan menggunakan panggilan dengan nilai yang tidak efisien
Kapil
1
otomatis & memberi saya hasil yang sama. Saya menggunakan MSVC 2015. Dan GCC menghasilkan kesalahan.
Sergey Podobry
Di sini saya menggunakan MSVC 2015, otomatis & memberikan hasil yang sama dengan otomatis &&.
Kehe CAI
Mengapa int i; otomatis && j = i; diizinkan tapi int saya; int && j = i; tidak ?
SeventhSon84
14

Pertama, saya sarankan membaca jawaban saya ini sebagai bacaan sampingan untuk penjelasan langkah demi langkah tentang bagaimana deduksi argumen templat untuk referensi universal bekerja.

Apakah ini berarti, kita diizinkan mencuri sumber daya var?

Belum tentu. Bagaimana jika foo()tiba-tiba kembali referensi, atau Anda mengubah panggilan tetapi lupa memperbarui penggunaan var? Atau jika Anda menggunakan kode generik dan tipe pengembalian foo()mungkin berubah tergantung pada parameter Anda?

Pikirkan auto&&untuk menjadi persis sama dengan T&&di template<class T> void f(T&& v);, karena (hampir ) persis seperti itu. Apa yang Anda lakukan dengan referensi universal dalam fungsi, ketika Anda harus meneruskannya atau menggunakannya dengan cara apa pun? Anda menggunakan std::forward<T>(v)untuk mendapatkan kembali kategori nilai asli. Jika itu adalah nilai sebelum diteruskan ke fungsi Anda, itu tetap nilai setelah melewati std::forward. Jika itu adalah nilai, itu akan menjadi nilai lagi (ingat, referensi nilai yang disebut adalah nilai).

Jadi, bagaimana Anda menggunakannya vardengan benar dalam mode generik? Gunakan std::forward<decltype(var)>(var). Ini akan bekerja persis sama dengan std::forward<T>(v)di templat fungsi di atas. Jika vara T&&, Anda akan mendapatkan nilai kembali, dan jika ya T&, Anda akan mendapatkan nilai kembali.

Jadi, kembali ke topik: Apa yang dikatakan oleh auto&& v = f();dan std::forward<decltype(v)>(v)dalam basis kode? Mereka memberi tahu kami bahwa vakan diperoleh dan diteruskan dengan cara yang paling efisien. Ingat, meskipun, setelah meneruskan variabel seperti itu, ada kemungkinan bahwa variabel tersebut dipindahkan-dari, jadi akan lebih salah menggunakannya tanpa menyetel ulang.

Secara pribadi, saya menggunakan auto&&kode generik ketika saya membutuhkan variabel yang dapat dimodifikasi . Penerusan sempurna nilai ulang dimodifikasi, karena operasi pemindahan berpotensi mencuri nyali. Jika saya hanya ingin menjadi malas (yaitu, tidak mengeja nama jenis bahkan jika saya mengetahuinya) dan tidak perlu memodifikasi (misalnya, ketika hanya mencetak elemen rentang), saya akan tetap menggunakannya auto const&.


autoadalah sejauh berbeda yang auto v = {1,2,3};akan membuat vsebuah std::initializer_list, sementara f({1,2,3})akan gagal deduksi.

Xeo
sumber
Di bagian pertama dari jawaban Anda: Maksud saya, jika foo()mengembalikan tipe-nilai T, maka var(ungkapan ini) akan menjadi nilai dan jenisnya (dari ungkapan ini) akan menjadi referensi nilai untuk T(yaitu T&&).
MWid
@MWid: Masuk akal, menghapus bagian pertama.
Xeo
3

Pertimbangkan beberapa tipe Tyang memiliki konstruktor bergerak, dan asumsikan

T t( foo() );

menggunakan konstruktor bergerak itu.

Sekarang, mari gunakan referensi perantara untuk mengambil kembali dari foo:

auto const &ref = foo();

ini mengesampingkan penggunaan konstruktor bergerak, sehingga nilai kembali harus disalin alih-alih dipindahkan (bahkan jika kita gunakan di std::movesini, kita tidak dapat benar-benar bergerak melalui const ref)

T t(std::move(ref));   // invokes T::T(T const&)

Namun, jika kita gunakan

auto &&rvref = foo();
// ...
T t(std::move(rvref)); // invokes T::T(T &&)

konstruktor bergerak masih tersedia.


Dan untuk menjawab pertanyaan Anda yang lain:

... Apakah ada situasi yang masuk akal ketika Anda harus menggunakan auto && untuk memberi tahu pembaca kode Anda sesuatu ...

Hal pertama, seperti kata Xeo, pada dasarnya saya melewati X seefisien mungkin , apa pun tipe X. Jadi, melihat kode yang menggunakan auto&&internal harus berkomunikasi bahwa itu akan menggunakan semantik bergerak secara internal yang sesuai.

... seperti yang Anda lakukan ketika Anda mengembalikan Unique_ptr <> untuk memberi tahu bahwa Anda memiliki kepemilikan eksklusif ...

Ketika templat fungsi mengambil argumen tipe T&&, itu mengatakan itu dapat memindahkan objek yang Anda masukkan. Mengembalikan unique_ptrsecara eksplisit memberikan kepemilikan kepada pemanggil; menerima T&&dapat menghapus kepemilikan dari penelepon (jika ada agen yang bergerak, dll.).

Tak berguna
sumber
2
Saya tidak yakin contoh kedua Anda valid. Apakah Anda tidak perlu Anda meneruskan sempurna untuk memanggil konstruktor bergerak?
3
Ini salah. Dalam kedua kasus, konstruktor salinan dipanggil, karena refdan rvrefkeduanya adalah nilai. Jika Anda ingin memindahkan konstruktor, maka Anda harus menulis T t(std::move(rvref)).
MWid
Apakah maksud Anda ref const dalam contoh pertama Anda: auto const &?
PiotrNycz
@aleguna - Anda dan MWid benar, terima kasih. Saya sudah memperbaiki jawaban saya.
berguna
1
@ Tidak Berguna Anda benar. Tetapi ini tidak menjawab pertanyaan saya. Kapan Anda menggunakan auto&&dan apa yang Anda katakan kepada pembaca tentang kode Anda dengan menggunakan auto&&?
MWid
-3

The auto &&sintaks menggunakan dua fitur baru dari C ++ 11:

  1. Bagian ini automemungkinkan kompilator menyimpulkan tipe berdasarkan konteks (nilai balik dalam kasus ini). Ini tanpa kualifikasi referensi apa pun (memungkinkan Anda menentukan apakah Anda mau T, T &atau T &&untuk jenis yang disimpulkan T).

  2. Ini &&adalah semantik langkah baru. Tipe semantik yang mendukung gerakan mengimplementasikan konstruktor T(T && other)yang secara optimal memindahkan konten dalam tipe baru. Ini memungkinkan suatu objek untuk menukar representasi internal daripada melakukan salinan yang dalam.

Ini memungkinkan Anda untuk memiliki sesuatu seperti:

std::vector<std::string> foo();

Begitu:

auto var = foo();

akan melakukan salinan vektor yang dikembalikan (mahal), tetapi:

auto &&var = foo();

akan menukar representasi internal vektor (vektor dari foodan vektor kosong dari var), sehingga akan lebih cepat.

Ini digunakan dalam sintaks for-loop baru:

for (auto &item : foo())
    std::cout << item << std::endl;

Di mana for-loop memegang auto &&nilai pengembalian dari foodan itemmerupakan referensi untuk setiap nilai dalam foo.

terima kasih
sumber
Ini salah. auto&&tidak akan memindahkan apapun, itu hanya akan membuat referensi. Apakah itu referensi nilai atau nilai tergantung pada ekspresi yang digunakan untuk menginisialisasi itu.
Joseph Mansfield
Dalam kedua kasus akan disebut konstruktor bergerak, karena std::vectordan std::stringdapat bergerak. Ini tidak ada hubungannya dengan tipe var.
MWid
1
@MWid: Sebenarnya, panggilan ke copy / move constructor juga bisa dieliminasi bersama dengan RVO.
Matthieu M.
@ MatthieuM. Kamu benar. Tetapi saya berpikir bahwa dalam contoh di atas, copy cnstructor tidak akan pernah dipanggil, karena semuanya dapat digerakkan.
MWid
1
@ MWW: maksud saya adalah bahwa bahkan konstruktor bergerak dapat dihilangkan. Pemindahan kartu truf (lebih murah).
Matthieu M.