Kapan saya harus menggunakan pengurangan tipe pengembalian otomatis C ++ 14?

144

Dengan GCC 4.8.0 dirilis, kami memiliki kompiler yang mendukung pengurangan tipe pengembalian otomatis, bagian dari C ++ 14. Dengan -std=c++1y, saya bisa melakukan ini:

auto foo() { //deduced to be int
    return 5;
}

Pertanyaan saya adalah: Kapan saya harus menggunakan fitur ini? Kapan itu perlu dan kapan itu membuat kode lebih bersih?

skenario 1

Skenario pertama yang bisa saya pikirkan adalah kapanpun memungkinkan. Setiap fungsi yang dapat ditulis dengan cara ini seharusnya. Masalah dengan ini adalah bahwa itu mungkin tidak selalu membuat kode lebih mudah dibaca.

Skenario 2

Skenario berikutnya adalah menghindari tipe pengembalian yang lebih kompleks. Sebagai contoh yang sangat ringan:

template<typename T, typename U>
auto add(T t, U u) { //almost deduced as decltype(t + u): decltype(auto) would
    return t + u;
}

Saya tidak percaya itu akan benar-benar menjadi masalah, meskipun saya kira memiliki tipe pengembalian secara eksplisit tergantung pada parameter bisa lebih jelas dalam beberapa kasus.

Skenario 3

Selanjutnya, untuk mencegah redundansi:

auto foo() {
    std::vector<std::map<std::pair<int, double>, int>> ret;
    //fill ret in with stuff
    return ret;
}

Dalam C ++ 11, kadang-kadang kita hanya bisa return {5, 6, 7};menggantikan vektor, tetapi itu tidak selalu berhasil dan kita perlu menentukan tipe di header fungsi dan fungsi tubuh. Ini murni berlebihan, dan pengurangan tipe pengembalian otomatis menyelamatkan kita dari redundansi itu.

Skenario 4

Akhirnya, dapat digunakan sebagai pengganti fungsi yang sangat sederhana:

auto position() {
    return pos_;
}

auto area() {
    return length_ * width_;
}

Namun, kadang-kadang, kita mungkin melihat fungsinya, ingin tahu tipe yang tepat, dan jika tidak disediakan di sana, kita harus pergi ke titik lain dalam kode, seperti di mana pos_dinyatakan.

Kesimpulan

Dengan skenario yang ditetapkan, yang mana di antara mereka yang benar-benar terbukti sebagai situasi di mana fitur ini berguna dalam membuat kode lebih bersih? Bagaimana dengan skenario yang saya lupa sebutkan di sini? Peringatan apa yang harus saya ambil sebelum menggunakan fitur ini agar tidak menggigit saya nanti? Apakah ada sesuatu yang baru yang dibawa fitur ini ke meja yang tidak mungkin tanpanya?

Perhatikan bahwa banyak pertanyaan dimaksudkan sebagai bantuan dalam menemukan perspektif untuk menjawab ini.

chris
sumber
18
Pertanyaan indah! Saat Anda bertanya skenario mana yang membuat kode "lebih baik", saya juga bertanya-tanya skenario mana yang akan memperburuknya .
Drew Dormann
2
@DrewDormann, Itu yang saya ingin tahu juga. Saya suka memanfaatkan fitur-fitur baru, tetapi mengetahui kapan menggunakannya dan kapan tidak begitu penting. Ada periode waktu ketika fitur baru muncul yang kami ambil untuk mencari tahu ini, jadi mari kita lakukan sekarang sehingga kita siap untuk ketika datang secara resmi :)
chris
2
@NicolBolas, Mungkin, tetapi kenyataan bahwa itu dalam rilis kompiler yang sebenarnya sekarang sudah cukup bagi orang untuk mulai menggunakannya dalam kode pribadi (itu pasti harus dijauhkan dari proyek pada saat ini). Saya salah satu dari orang-orang yang suka menggunakan fitur-fitur terbaru yang mungkin dalam kode saya sendiri, dan sementara saya tidak tahu seberapa baik proposal dengan komite, saya menemukan fakta bahwa itu adalah yang pertama dimasukkan dalam opsi baru ini kata sesuatu. Mungkin lebih baik dibiarkan nanti, atau (saya tidak tahu seberapa bagus kerjanya) hidup kembali ketika kita tahu pasti itu akan datang.
chris
1
@NicolBolas, Jika itu membantu, ini telah diadopsi sekarang: p
chris
1
Jawaban saat ini tampaknya tidak menyebutkan bahwa mengganti ->decltype(t+u)dengan pengurangan otomatis membunuh SFINAE.
Marc Glisse

Jawaban:

62

C ++ 11 menimbulkan pertanyaan serupa: kapan harus menggunakan pengurangan tipe kembali dalam lambdas, dan kapan harus menggunakan autovariabel.

Jawaban tradisional untuk pertanyaan dalam C dan C ++ 03 adalah "melintasi batas pernyataan yang kita buat menjadi tipe eksplisit, dalam ekspresi mereka biasanya implisit tetapi kita bisa membuatnya eksplisit dengan gips". C ++ 11 dan C ++ 1y memperkenalkan alat pengurang tipe sehingga Anda dapat mengabaikan tipe di tempat baru.

Maaf, tetapi Anda tidak akan menyelesaikan ini di muka dengan membuat aturan umum. Anda perlu melihat kode tertentu, dan memutuskan sendiri apakah itu membantu keterbacaan untuk menentukan jenis di semua tempat: apakah lebih baik bagi kode Anda untuk mengatakan, "jenis benda ini adalah X", atau lebih baik untuk kode Anda untuk mengatakan, "jenis hal ini tidak relevan untuk memahami bagian kode ini: kompiler perlu tahu dan kami mungkin bisa mengatasinya tetapi kami tidak perlu mengatakannya di sini"?

Karena "keterbacaan" tidak didefinisikan secara objektif [*], dan lebih jauh lagi bervariasi menurut pembaca, Anda memiliki tanggung jawab sebagai penulis / editor sepotong kode yang tidak dapat sepenuhnya dipenuhi oleh panduan gaya. Bahkan sejauh panduan gaya tidak menentukan norma, orang yang berbeda akan lebih suka norma yang berbeda dan cenderung menemukan sesuatu yang tidak dikenal sebagai "kurang dapat dibaca". Jadi keterbacaan aturan gaya yang diusulkan tertentu seringkali hanya dapat dinilai dalam konteks aturan gaya lain yang ada.

Semua skenario Anda (bahkan yang pertama) akan digunakan untuk gaya pengkodean seseorang. Secara pribadi saya menemukan yang kedua menjadi use case yang paling menarik, tetapi meskipun begitu saya mengantisipasi bahwa itu akan tergantung pada alat dokumentasi Anda. Sangat tidak membantu untuk melihat bahwa tipe pengembalian dari templat fungsi adalah auto, sedangkan melihatnya didokumentasikan sebagai decltype(t+u)membuat antarmuka yang diterbitkan yang dapat (mudah-mudahan) Anda andalkan.

[*] Kadang-kadang seseorang mencoba melakukan beberapa pengukuran objektif. Sebagian kecil bahwa siapa pun yang pernah muncul dengan hasil yang signifikan secara statistik dan berlaku umum, mereka sepenuhnya diabaikan oleh programmer yang bekerja, demi naluri penulis tentang apa yang "dapat dibaca".

Steve Jessop
sumber
1
Poin bagus dengan koneksi ke lambdas (meskipun ini memungkinkan untuk fungsi tubuh yang lebih kompleks). Yang utama adalah bahwa ini adalah fitur yang lebih baru, dan saya masih mencoba untuk menyeimbangkan pro dan kontra dari setiap use case. Untuk melakukan ini, akan sangat membantu untuk melihat alasan di balik mengapa itu akan digunakan untuk apa sehingga saya sendiri dapat menemukan mengapa saya lebih suka apa yang saya lakukan. Saya mungkin terlalu memikirkannya, tapi itulah saya.
chris
1
@ Chris: seperti yang saya katakan, saya pikir semuanya bermuara pada hal yang sama. Apakah lebih baik bagi kode Anda untuk mengatakan, "jenis benda ini adalah X", atau apakah lebih baik bagi kode Anda untuk mengatakan, "jenis benda ini tidak relevan, kompiler perlu tahu dan kita mungkin bisa mengatasinya tetapi kita tidak perlu mengatakannya ". Ketika kita menulis 1.0 + 27Ukita menegaskan yang terakhir, ketika kita menulis (double)1.0 + (double)27Ukita menegaskan yang pertama. Kesederhanaan fungsi, tingkat duplikasi, menghindari decltypesemua mungkin berkontribusi untuk itu tetapi tidak ada yang akan menjadi penentu yang andal.
Steve Jessop
1
Apakah lebih baik bagi kode Anda untuk mengatakan, "jenis benda ini adalah X", atau apakah lebih baik bagi kode Anda untuk mengatakan, "jenis benda ini tidak relevan, kompiler perlu tahu dan kita mungkin bisa mengatasinya tetapi kita tidak perlu mengatakannya ". - Kalimat itu persis seperti yang saya cari. Saya akan mempertimbangkan hal itu ketika saya menemukan pilihan untuk menggunakan fitur ini, dan autosecara umum.
chris
1
Saya ingin menambahkan bahwa IDE mungkin mengurangi "masalah keterbacaan". Ambil Visual Studio, misalnya: Jika Anda mengarahkan kursor ke autokata kunci, itu akan menampilkan jenis pengembalian yang sebenarnya.
andreee
1
@andreee: itu benar dalam batas. Jika suatu jenis memiliki banyak alias maka mengetahui jenis yang sebenarnya tidak selalu berguna seperti yang Anda harapkan. Misalnya jenis iterator mungkin int*, tetapi yang sebenarnya penting, jika ada, adalah alasannya int*karena itulah yang std::vector<int>::iterator_typeada pada opsi build Anda saat ini!
Steve Jessop
30

Secara umum, tipe pengembalian fungsi sangat membantu untuk mendokumentasikan suatu fungsi. Pengguna akan tahu apa yang diharapkan. Namun, ada satu kasus di mana saya pikir bisa lebih baik untuk menjatuhkan tipe pengembalian untuk menghindari redundansi. Berikut ini sebuah contoh:

template<typename F, typename Tuple, int... I>
  auto
  apply_(F&& f, Tuple&& args, int_seq<I...>) ->
  decltype(std::forward<F>(f)(std::get<I>(std::forward<Tuple>(args))...))
  {
    return std::forward<F>(f)(std::get<I>(std::forward<Tuple>(args))...);
  }

template<typename F, typename Tuple,
         typename Indices = make_int_seq<std::tuple_size<Tuple>::value>>
  auto
  apply(F&& f, Tuple&& args) ->
  decltype(apply_(std::forward<F>(f), std::forward<Tuple>(args), Indices()))
  {
    return apply_(std::forward<F>(f), std::forward<Tuple>(args), Indices());
  }

Contoh ini diambil dari makalah komite resmi N3493 . Tujuan fungsi applyadalah untuk meneruskan elemen-elemen dari std::tuplesuatu fungsi dan mengembalikan hasilnya. Itu int_seqdan make_int_seqhanya bagian dari implementasi, dan mungkin hanya akan membingungkan setiap pengguna yang mencoba memahami apa fungsinya.

Seperti yang Anda lihat, tipe pengembalian tidak lebih dari a decltype ekspresi yang dikembalikan. Selain itu, apply_tidak dimaksudkan untuk dilihat oleh pengguna, saya tidak yakin tentang kegunaan mendokumentasikan tipe kembalinya ketika itu kurang lebih sama dengan yang applyada. Saya berpikir bahwa, dalam kasus khusus ini, menjatuhkan tipe pengembalian membuat fungsi lebih mudah dibaca. Perhatikan bahwa tipe pengembalian ini sebenarnya telah dijatuhkan dan diganti oleh decltype(auto)dalam proposal untuk ditambahkan applyke standar, N3915 (juga perhatikan bahwa jawaban awal saya sebelum makalah ini):

template <typename F, typename Tuple, size_t... I>
decltype(auto) apply_impl(F&& f, Tuple&& t, index_sequence<I...>) {
    return forward<F>(f)(get<I>(forward<Tuple>(t))...);
}

template <typename F, typename Tuple>
decltype(auto) apply(F&& f, Tuple&& t) {
    using Indices = make_index_sequence<tuple_size<decay_t<Tuple>>::value>;
    return apply_impl(forward<F>(f), forward<Tuple>(t), Indices{});
}

Namun, sebagian besar waktu, lebih baik menyimpan tipe pengembalian itu. Dalam kasus khusus yang saya jelaskan di atas, tipe pengembalian agak tidak dapat dibaca dan seorang pengguna potensial tidak akan mendapatkan apa pun dari mengetahuinya. Dokumentasi yang baik dengan contoh-contoh akan jauh lebih bermanfaat.


Hal lain yang belum disebutkan: sementara declype(t+u)memungkinkan untuk menggunakan ekspresi SFINAE , decltype(auto)tidak (meskipun ada proposal untuk mengubah perilaku ini). Ambil contoh afoobar fungsi yang akan memanggil fungsi anggota tipe foojika ada atau panggil barfungsi anggota tipe jika ada, dan asumsikan bahwa kelas selalu memiliki ketepatan fooatau bartetapi tidak keduanya sekaligus:

struct X
{
    void foo() const { std::cout << "foo\n"; }
};

struct Y
{
    void bar() const { std::cout << "bar\n"; }
};

template<typename C> 
auto foobar(const C& c) -> decltype(c.foo())
{
    return c.foo();
}

template<typename C> 
auto foobar(const C& c) -> decltype(c.bar())
{
    return c.bar();
}

Memanggil foobarinstance Xakan menampilkan foosaat memanggil foobarinstance Yakan menampilkan bar. Jika Anda menggunakan pengurangan tipe pengembalian otomatis sebagai gantinya (dengan atau tanpa decltype(auto)), Anda tidak akan mendapatkan ekspresi SFINAE dan memanggil foobarinstance dari salah satuX atau Yakan memicu kesalahan waktu kompilasi.

Morwenn
sumber
8

Tidak pernah perlu. Seperti kapan Anda harus- Anda akan mendapatkan banyak jawaban berbeda tentang itu. Saya akan mengatakan tidak sama sekali sampai itu benar-benar bagian yang diterima dari standar dan didukung oleh mayoritas kompiler utama dengan cara yang sama.

Di luar itu, itu akan menjadi argumen agama. Saya pribadi akan mengatakan bahwa tidak pernah memasukkan jenis pengembalian aktual membuat kode lebih jelas, jauh lebih mudah untuk pemeliharaan (saya bisa melihat tanda tangan fungsi dan tahu apa yang dikembalikan vs benar-benar harus membaca kode), dan itu menghilangkan kemungkinan bahwa Anda pikir itu harus mengembalikan satu jenis dan kompiler berpikir yang lain menyebabkan masalah (seperti yang terjadi pada setiap bahasa scripting yang pernah saya gunakan). Saya pikir otomatis adalah kesalahan besar dan itu akan menyebabkan pesanan lebih banyak rasa sakit daripada bantuan. Orang lain akan mengatakan Anda harus menggunakannya setiap saat, karena sesuai dengan filosofi pemrograman mereka. Bagaimanapun, ini jauh dari ruang lingkup untuk situs ini.

Gabe Sechan
sumber
1
Saya setuju bahwa orang-orang dengan latar belakang yang berbeda akan memiliki pendapat yang berbeda tentang ini, tapi saya berharap ini sampai pada kesimpulan C ++ - ish. Dalam (hampir) setiap kasus, tidak dapat dimaafkan untuk menyalahgunakan fitur bahasa untuk mencoba membuatnya menjadi yang lain, seperti menggunakan #defines untuk mengubah C ++ menjadi VB. Fitur umumnya memiliki konsensus yang baik dalam pola pikir bahasa tentang apa yang layak digunakan dan apa yang tidak, sesuai dengan apa yang terbiasa oleh pemrogram bahasa itu. Fitur yang sama dapat hadir dalam berbagai bahasa, tetapi masing-masing memiliki pedoman sendiri tentang penggunaannya.
chris
2
Beberapa fitur memiliki konsensus yang baik. Banyak yang tidak. Saya tahu banyak programmer yang berpikir bahwa sebagian besar Peningkatan adalah sampah yang seharusnya tidak digunakan. Saya juga tahu beberapa yang berpikir itu hal terbesar yang terjadi pada C ++. Dalam kedua kasus saya pikir ada beberapa diskusi menarik yang bisa didapat tentang itu, tetapi itu benar-benar contoh yang tepat dari penutupan sebagai opsi tidak konstruktif di situs ini.
Gabe Sechan
2
Tidak, saya benar-benar tidak setuju dengan Anda, dan saya pikir itu autoadalah berkah murni. Ini menghilangkan banyak redundansi. Sungguh merepotkan untuk mengulang jenis yang kembali terkadang. Jika Anda ingin mengembalikan lambda, bisa jadi tidak mungkin tanpa menyimpan hasilnya ke dalam std::function, yang mungkin menimbulkan beberapa biaya tambahan.
Yongwei Wu
1
Setidaknya ada satu situasi di mana semuanya sangat diperlukan. Misalkan Anda perlu memanggil fungsi, lalu mencatat hasilnya sebelum mengembalikannya, dan fungsinya tidak semua memiliki tipe pengembalian yang sama. Jika Anda melakukannya melalui fungsi logging yang mengambil fungsi dan argumennya sebagai parameter, maka Anda harus mendeklarasikan tipe pengembalian fungsi logging autosehingga selalu cocok dengan tipe kembali dari fungsi yang diteruskan.
Justin Time - Pasang kembali Monica
1
[Secara teknis ada cara lain untuk melakukan ini, tetapi menggunakan templat ajaib untuk melakukan hal yang sama persis pada dasarnya, dan masih membutuhkan fungsi logging autountuk trailing tipe kembali.]
Justin Time - Reinstate Monica
7

Itu tidak ada hubungannya dengan kesederhanaan fungsi (sebagai duplikat yang seharusnya dihapus dari pertanyaan ini seharusnya).

Entah jenis pengembalian diperbaiki (tidak digunakan auto), atau bergantung secara kompleks pada parameter templat (digunakan autodalam kebanyakan kasus, dipasangkan dengan decltypeketika ada beberapa titik kembali).

Ben Voigt
sumber
3

Pertimbangkan lingkungan produksi nyata: banyak fungsi dan pengujian unit semua saling bergantung pada jenis pengembalian foo(). Sekarang anggaplah bahwa tipe pengembalian perlu diubah untuk alasan apa pun.

Jika jenis kembali ada di automana - mana, dan penelepon ke foo()dan fungsi terkait menggunakan autoketika mendapatkan nilai kembali, perubahan yang perlu dilakukan minimal. Jika tidak, ini bisa berarti jam kerja yang sangat membosankan dan rawan kesalahan.

Sebagai contoh dunia nyata, saya diminta untuk mengubah modul dari menggunakan pointer mentah di mana saja menjadi pointer pintar. Memperbaiki unit test lebih menyakitkan daripada kode sebenarnya.

Meskipun ada cara lain yang bisa ditangani, penggunaan autotipe pengembalian sepertinya cocok.

Jim Wood
sumber
1
Dalam hal ini, bukankah lebih baik menulis decltype(foo())?
Oren S
Secara pribadi, saya merasa typedefdeklarasi s dan alias (mis. usingDeklarasi tipe) lebih baik dalam kasus ini, khususnya ketika mereka di-scoped kelas.
MAChitgarha
3

Saya ingin memberikan contoh di mana tipe pengembalian otomatis sempurna:

Bayangkan Anda ingin membuat alias pendek untuk panggilan fungsi yang panjang berikutnya. Dengan otomatis Anda tidak perlu mengurus jenis pengembalian asli (mungkin akan berubah di masa mendatang) dan pengguna dapat mengklik fungsi asli untuk mendapatkan jenis pengembalian nyata:

inline auto CreateEntity() { return GetContext()->GetEntityManager()->CreateEntity(); }

PS: Tergantung pada pertanyaan ini .

kaisar
sumber
2

Untuk skenario 3 saya akan mengubah jenis tanda tangan fungsi kembali dengan variabel lokal untuk dikembalikan sekitar. Itu akan membuatnya lebih jelas untuk pemrogram klien dan kembali fungsi. Seperti ini:

Skenario 3 Untuk mencegah redundansi:

std::vector<std::map<std::pair<int, double>, int>> foo() {
    decltype(foo()) ret;
    return ret;
}

Ya, itu tidak memiliki kata kunci otomatis tetapi kepala sekolah adalah sama untuk mencegah redundansi dan memberikan programmer yang tidak memiliki akses ke sumber waktu yang lebih mudah.

Sajikan Laurijssen
sumber
2
IMO perbaikan yang lebih baik untuk ini adalah dengan memberikan nama spesifik domain untuk konsep apa pun yang dimaksudkan untuk direpresentasikan sebagai a vector<map<pair<int,double>,int>dan kemudian menggunakannya.
davidbak