Mengapa menggunakan fungsi awal dan akhir non-anggota di C ++ 11?

197

Setiap kontainer standar memiliki begindan endmetode untuk mengembalikan iterator untuk wadah itu. Namun, C ++ 11 tampaknya telah memperkenalkan fungsi bebas yang dipanggil std::begindan std::endyang memanggil fungsi begindan endanggota. Jadi, alih-alih menulis

auto i = v.begin();
auto e = v.end();

kamu akan menulis

auto i = std::begin(v);
auto e = std::end(v);

Dalam ceramahnya, Writing Modern C ++ , Herb Sutter mengatakan bahwa Anda harus selalu menggunakan fungsi gratis sekarang ketika Anda ingin memulai atau mengakhiri iterator untuk sebuah wadah. Namun, ia tidak menjelaskan secara terperinci mengapa Anda menginginkannya. Melihat kode itu, Anda menghemat satu karakter. Jadi, sejauh kontainer standar berjalan, fungsi bebas tampaknya sama sekali tidak berguna. Herb Sutter menunjukkan bahwa ada manfaat untuk wadah non-standar, tetapi sekali lagi, dia tidak menjelaskan secara rinci.

Jadi, pertanyaannya adalah apa tepatnya versi fungsi bebas std::begindan std::endlakukan di luar memanggil versi fungsi anggota yang sesuai, dan mengapa Anda ingin menggunakannya?

Jonathan M Davis
sumber
29
Ini satu karakter yang lebih sedikit, simpan titik-titik itu untuk anak-anak Anda: xkcd.com/297
HostileFork mengatakan jangan percaya pada SE
Entah bagaimana aku benci menggunakannya karena aku harus mengulanginya std::sepanjang waktu.
Michael Chourdakis

Jawaban:

162

Bagaimana Anda memanggil .begin()dan .end()menggunakan C-array?

Fungsi bebas memungkinkan pemrograman yang lebih umum karena mereka dapat ditambahkan sesudahnya, pada struktur data yang tidak dapat Anda ubah.

Matthieu M.
sumber
7
@JonathanMDavis: Anda dapat memiliki endarray yang dideklarasikan secara statis ( int foo[5]) menggunakan trik pemrograman template. Setelah membusuk menjadi pointer, Anda tentu saja kurang beruntung.
Matthieu M.
33
template<typename T, size_t N> T* end(T (&a)[N]) { return a + N; }
Hugh
6
@JonathanMDavis: Seperti yang ditunjukkan oleh yang lainnya, tentu mungkin untuk mendapatkan begindan endmenggunakan array C selama Anda belum membukanya sendiri ke sebuah pointer - @Huw menguraikannya. Adapun mengapa Anda ingin: bayangkan bahwa Anda refactored kode yang menggunakan array untuk menggunakan vektor (atau sebaliknya, untuk alasan apa pun). Jika Anda telah menggunakan begindan end, dan mungkin beberapa kesalahan ketik yang pintar, kode implementasi tidak harus berubah sama sekali (kecuali mungkin beberapa dari typedefs).
Karl Knechtel
31
@ JonathanMDavis: Array bukan pointer. Dan untuk semua orang: Demi mengakhiri kebingungan yang selalu menonjol ini, berhentilah menyebut (beberapa) petunjuk sebagai "array yang membusuk". Tidak ada istilah seperti itu dalam bahasa, dan benar-benar tidak ada gunanya. Pointer adalah pointer, array adalah array. Array dapat dikonversi menjadi pointer ke elemen pertama mereka secara implisit, tetapi masih hanya pointer lama biasa, tanpa perbedaan dengan yang lain. Tentu saja Anda tidak bisa mendapatkan "ujung" dari sebuah pointer, case ditutup.
GManNickG
5
Nah, selain array ada sejumlah besar API yang mengekspos wadah seperti aspek. Jelas Anda tidak dapat memodifikasi API pihak ke-3 tetapi Anda dapat dengan mudah menulis fungsi awal / akhir berdiri bebas ini.
edA-qa mort-ora-y
35

Pertimbangkan kasus ketika Anda memiliki perpustakaan yang berisi kelas:

class SpecialArray;

ini memiliki 2 metode:

int SpecialArray::arraySize();
int SpecialArray::valueAt(int);

untuk mengulangi nilai-nilai itu Anda perlu mewarisi dari kelas ini dan menentukan begin()dan end()metode untuk kasus saat

auto i = v.begin();
auto e = v.end();

Tetapi jika Anda selalu menggunakan

auto i = begin(v);
auto e = end(v);

kamu bisa melakukan ini:

template <>
SpecialArrayIterator begin(SpecialArray & arr)
{
  return SpecialArrayIterator(&arr, 0);
}

template <>
SpecialArrayIterator end(SpecialArray & arr)
{
  return SpecialArrayIterator(&arr, arr.arraySize());
}

di mana SpecialArrayIteratorsesuatu seperti:

class SpecialArrayIterator
{
   SpecialArrayIterator(SpecialArray * p, int i)
    :index(i), parray(p)
   {
   }
   SpecialArrayIterator operator ++();
   SpecialArrayIterator operator --();
   SpecialArrayIterator operator ++(int);
   SpecialArrayIterator operator --(int);
   int operator *()
   {
     return parray->valueAt(index);
   }
   bool operator ==(SpecialArray &);
   // etc
private:
   SpecialArray *parray;
   int index;
   // etc
};

sekarang idan edapat secara hukum digunakan untuk iterasi dan mengakses nilai-nilai SpecialArray

GreenScape
sumber
8
Ini seharusnya tidak termasuk template<>garis. Anda mendeklarasikan kelebihan fungsi baru, bukan spesialisasi template.
David Stone
33

Menggunakan begindan endfungsi bebas menambahkan satu lapisan tipuan. Biasanya itu dilakukan untuk memungkinkan lebih banyak fleksibilitas.

Dalam hal ini saya bisa memikirkan beberapa kegunaan.

Penggunaan yang paling jelas adalah untuk C-array (bukan c pointer).

Lain adalah ketika mencoba menggunakan algoritma standar pada wadah yang tidak sesuai (yaitu wadah tersebut tidak memiliki .begin()metode). Dengan asumsi Anda tidak bisa hanya memperbaiki wadah, opsi terbaik berikutnya adalah membebani beginfungsinya. Ramuan menyarankan Anda selalu menggunakan beginfungsi untuk mempromosikan keseragaman dan konsistensi dalam kode Anda. Daripada harus mengingat wadah mana yang mendukung metode begindan mana yang perlu berfungsi begin.

Sebagai samping, C ++ berikutnya rev harus menyalin D's notasi pseudo-anggota . Jika a.foo(b,c,d)tidak didefinisikan sebagai gantinya mencoba foo(a,b,c,d). Hanya sedikit sintaksis gula untuk membantu kita manusia miskin yang lebih suka subjek daripada memesan kata kerja.

deft_code
sumber
5
The notasi pseudo-anggota terlihat seperti C # /. Net metode ekstensi . Mereka memang berguna untuk berbagai situasi meskipun - seperti semua fitur - dapat rentan terhadap 'penyalahgunaan'.
Gareth Wilson
5
Notasi pseudo-anggota adalah anugerah untuk pengkodean dengan Intellisense; memukul "a." menunjukkan kata kerja yang relevan, membebaskan kekuatan otak dari daftar menghafal, dan membantu menemukan fungsi API yang relevan dapat membantu mencegah fungsi duplikasi, tanpa harus menyisipkan fungsi non-anggota ke dalam kelas.
Matt Curtis
Ada proposal untuk memasukkannya ke dalam C ++, yang menggunakan istilah Unified Function Call Syntax (UFCS).
underscore_d
17

Untuk menjawab pertanyaan Anda, fungsi bebas memulai () dan mengakhiri () secara default tidak melakukan apa-apa selain memanggil fungsi anggota .begin () dan .end (). Dari <iterator>, termasuk secara otomatis ketika Anda menggunakan salah satu wadah standar seperti <vector>, <list>, dll, Anda mendapatkan:

template< class C > 
auto begin( C& c ) -> decltype(c.begin());
template< class C > 
auto begin( const C& c ) -> decltype(c.begin()); 

Bagian kedua dari pertanyaan Anda adalah mengapa memilih fungsi gratis jika yang mereka lakukan hanyalah memanggil fungsi anggota. Itu benar-benar tergantung pada objek apa yang vada dalam kode contoh Anda. Jika tipe v adalah tipe kontainer standar, seperti vector<T> v;maka tidak masalah jika Anda menggunakan fungsi bebas atau anggota, mereka melakukan hal yang sama. Jika objek Anda vlebih umum, seperti dalam kode berikut:

template <class T>
void foo(T& v) {
  auto i = v.begin();     
  auto e = v.end(); 
  for(; i != e; i++) { /* .. do something with i .. */ } 
}

Kemudian menggunakan fungsi anggota memecah kode Anda untuk T = C array, string C, enum, dll. Dengan menggunakan fungsi non-anggota, Anda mengiklankan antarmuka yang lebih umum yang orang dapat dengan mudah memperluas. Dengan menggunakan antarmuka fungsi gratis:

template <class T>
void foo(T& v) {
  auto i = begin(v);     
  auto e = end(v); 
  for(; i != e; i++) { /* .. do something with i .. */ } 
}

Kode sekarang bekerja dengan T = C array dan string C. Sekarang menulis sejumlah kecil kode adaptor:

enum class color { RED, GREEN, BLUE };
static color colors[]  = { color::RED, color::GREEN, color::BLUE };
color* begin(const color& c) { return begin(colors); }
color* end(const color& c)   { return end(colors); }

Kami bisa membuat kode Anda agar kompatibel dengan enum yang dapat diubah juga. Saya pikir poin utama Herb adalah bahwa menggunakan fungsi bebas sama mudahnya dengan menggunakan fungsi anggota, dan itu memberikan kode Anda kompatibilitas mundur dengan jenis urutan C dan kompatibilitas maju dengan jenis urutan non-stl (dan jenis-stl masa depan!), dengan biaya rendah untuk pengembang lain.

Nate
sumber
Contoh yang bagus. Saya tidak akan mengambil enumatau jenis fundamental lainnya dengan referensi, meskipun; mereka akan lebih murah untuk menyalin daripada tidak langsung.
underscore_d
6

Salah satu manfaat std::begindan std::endadalah mereka berfungsi sebagai titik ekstensi untuk mengimplementasikan antarmuka standar untuk kelas eksternal.

Jika Anda ingin menggunakan CustomContainerkelas dengan rentang berbasis untuk fungsi loop atau templat yang diharapkan .begin()dan .end()metode, Anda jelas harus menerapkan metode tersebut.

Jika kelas memang menyediakan metode tersebut, itu tidak masalah. Jika tidak, Anda harus memodifikasinya *.

Ini tidak selalu layak, misalnya ketika menggunakan perpustakaan eksternal, terutama yang komersial dan sumber tertutup.

Dalam situasi seperti itu, std::begindan std::endsangat berguna, karena seseorang dapat menyediakan API iterator tanpa memodifikasi kelas itu sendiri, tetapi kelebihan fungsi bebas.

Contoh: misalkan Anda ingin menerapkan count_iffungsi yang mengambil wadah alih-alih sepasang iterator. Kode tersebut mungkin terlihat seperti ini:

template<typename ContainerType, typename PredicateType>
std::size_t count_if(const ContainerType& container, PredicateType&& predicate)
{
    using std::begin;
    using std::end;

    return std::count_if(begin(container), end(container),
                         std::forward<PredicateType&&>(predicate));
}

Sekarang, untuk setiap kelas yang ingin Anda gunakan dengan kebiasaan ini count_if, Anda hanya perlu menambahkan dua fungsi gratis, daripada memodifikasi kelas-kelas itu.

Sekarang, C ++ memiliki mechanisim yang disebut Argument Dependent Lookup (ADL), yang membuat pendekatan seperti itu bahkan lebih fleksibel.

Singkatnya, ADL berarti, bahwa ketika kompiler menyelesaikan fungsi yang tidak memenuhi syarat (yaitu fungsi tanpa namespace, seperti beginbukan std::begin), ia juga akan mempertimbangkan fungsi yang dideklarasikan dalam ruang nama argumen. Sebagai contoh:

namesapce some_lib
{
    // let's assume that CustomContainer stores elements sequentially,
    // and has data() and size() methods, but not begin() and end() methods:

    class CustomContainer
    {
        ...
    };
}

namespace some_lib
{    
    const Element* begin(const CustomContainer& c)
    {
        return c.data();
    }

    const Element* end(const CustomContainer& c)
    {
        return c.data() + c.size();
    }
}

// somewhere else:
CustomContainer c;
std::size_t n = count_if(c, somePredicate);

Dalam hal ini, tidak masalah bahwa nama yang memenuhi syarat adalah some_lib::begindan some_lib::end - karena CustomContainerada some_lib::juga, kompiler akan menggunakan kelebihan itu di count_if.

Itu juga alasan untuk memiliki using std::begin;dan using std::end;masuk count_if. Ini memungkinkan kita untuk menggunakan tanpa pengecualian begindan end, karenanya memungkinkan ADL dan memungkinkan kompiler untuk memilih std::begindan std::endketika tidak ada alternatif lain ditemukan.

Kita bisa makan cookie dan memiliki cookie - yaitu memiliki cara untuk menyediakan implementasi kustom begin/ endsementara kompiler dapat kembali ke yang standar.

Beberapa catatan:

  • Untuk alasan yang sama, ada fungsi serupa lainnya: std::rbegin/ rend, std::sizedan std::data.

  • Seperti jawaban lain menyebutkan, std::versi memiliki kelebihan untuk array kosong. Itu berguna, tetapi hanya kasus khusus dari apa yang saya jelaskan di atas.

  • Menggunakan std::begindan berteman adalah ide yang bagus ketika menulis kode templat, karena ini membuat templat-templat tersebut lebih umum. Untuk non-templat Anda mungkin juga menggunakan metode, jika berlaku.

PS Saya tahu bahwa postingan ini sudah hampir 7 tahun. Saya menemukan itu karena saya ingin menjawab pertanyaan yang ditandai sebagai duplikat dan menemukan bahwa tidak ada jawaban di sini yang menyebutkan ADL.

joe_chip
sumber
Jawaban yang bagus, terutama menjelaskan ADL secara terbuka, daripada menyerahkannya pada imajinasi seperti yang dilakukan orang lain - bahkan ketika mereka menunjukkannya dalam aksi!
underscore_d
5

Sedangkan fungsi non-anggota tidak memberikan manfaat apa pun untuk wadah standar, menggunakannya tidak memberikan gaya yang lebih konsisten dan fleksibel. Jika suatu saat Anda ingin memperluas kelas penampung non-std yang ada, Anda lebih suka mendefinisikan kelebihan fungsi-fungsi gratis, daripada mengubah definisi kelas yang ada. Jadi untuk wadah non-std mereka sangat berguna dan selalu menggunakan fungsi bebas membuat kode Anda lebih fleksibel karena Anda dapat mengganti wadah std dengan wadah non-std lebih mudah dan jenis wadah yang mendasarinya lebih transparan untuk kode Anda karena mendukung beragam implementasi kontainer yang jauh lebih luas.

Tapi tentu saja ini harus selalu ditimbang dengan benar dan abstraksi yang berlebihan juga tidak baik. Meskipun menggunakan fungsi-fungsi gratis tidak terlalu banyak abstraksi, itu tetap merusak kompatibilitas dengan kode C ++ 03, yang pada usia muda ini C ++ 11 mungkin masih menjadi masalah bagi Anda.

Christian Rau
sumber
3
Di C ++ 03, Anda bisa menggunakan boost::begin()/ end(), jadi tidak ada ketidakcocokan nyata :)
Marc Mutz - mmutz
1
@ MarcMutz-mmutz Yah, meningkatkan dependensi tidak selalu merupakan pilihan (dan cukup banyak jika digunakan hanya untuk begin/end). Jadi saya akan menganggap bahwa ketidakcocokan dengan C + + 03 murni juga. Tapi seperti dikatakan, itu adalah ketidakcocokan yang agak kecil (dan semakin kecil), karena C ++ 11 (setidaknya begin/endkhususnya) semakin banyak diadopsi.
Christian Rau
0

Pada akhirnya manfaatnya adalah dalam kode yang digeneralisasi sehingga menjadi wadah agnostik. Itu dapat beroperasi pada std::vector, array, atau rentang tanpa perubahan pada kode itu sendiri.

Selain itu, kontainer, bahkan kontainer yang tidak dimiliki dapat dipasang sedemikian rupa sehingga mereka juga dapat digunakan secara agnostik dengan kode menggunakan pengakses berbasis rentang non-anggota.

Lihat di sini untuk detail lebih lanjut.

Jonathan Mee
sumber