fungsi anggota publik teman swap

169

Dalam jawaban yang indah untuk idiom copy-and-swap ada sepotong kode saya butuh bantuan:

class dumb_array
{
public:
    // ...
    friend void swap(dumb_array& first, dumb_array& second) // nothrow
    {
        using std::swap; 
        swap(first.mSize, second.mSize); 
        swap(first.mArray, second.mArray);
    }
    // ...
};

dan dia menambahkan catatan

Ada klaim lain bahwa kita harus mengkhususkan std :: swap untuk tipe kita, menyediakan swap di kelas di samping swap fungsi-bebas, dll. Tapi ini semua tidak perlu: setiap penggunaan swap yang tepat akan melalui panggilan yang tidak memenuhi syarat , dan fungsi kami akan ditemukan melalui ADL. Satu fungsi akan dilakukan.

Dengan friendsaya sedikit pada istilah "tidak ramah", saya harus mengakui. Jadi, pertanyaan utama saya adalah:

  • terlihat seperti fungsi bebas , tetapi di dalam tubuh kelas?
  • mengapa ini tidak swapstatis ? Itu jelas tidak menggunakan variabel anggota.
  • "Adakah penggunaan swap yang tepat akan menemukan swap melalui ADL" ? ADL akan mencari ruang nama, kan? Tetapi apakah itu juga terlihat di dalam kelas? Atau di sinilah tempatnya friend?

Pertanyaan sampingan:

  • Dengan C ++ 11, harus saya menandai saya swaps dengan noexcept?
  • Dengan C ++ 11 dan range-for-nya , haruskah saya menempatkan friend iter begin()dan friend iter end()dengan cara yang sama di dalam kelas? Saya pikir friendtidak diperlukan di sini, kan?
towi
sumber
Mempertimbangkan pertanyaan sampingan tentang range-range untuk: lebih baik menulis fungsi-fungsi anggota dan membiarkan akses range on begin () dan end () di std namespace (§24.6.5), range-based untuk penggunaan internal ini dari global atau namespace std (lihat §6.5.4). Namun, ada kekurangannya bahwa fungsi-fungsi ini adalah bagian dari header <iterator>, jika Anda tidak memasukkannya, Anda mungkin ingin menuliskannya sendiri.
Vitus
2
mengapa tidak statis - karena suatu friendfungsi sama sekali bukan fungsi anggota.
aschepler

Jawaban:

175

Ada beberapa cara untuk menulis swap, beberapa lebih baik daripada yang lain. Namun seiring waktu, ditemukan satu definisi yang paling baik. Mari kita pikirkan bagaimana kita berpikir tentang menulis suatu swapfungsi.


Kami pertama kali melihat bahwa wadah seperti std::vector<>memiliki fungsi anggota argumen tunggal swap, seperti:

struct vector
{
    void swap(vector&) { /* swap members */ }
};

Jadi, tentu saja, kelas kita juga seharusnya, kan? Yah, tidak juga. Perpustakaan standar memiliki segala macam hal yang tidak perlu , dan anggota swapadalah salah satunya. Mengapa? Ayo pergi.


Yang harus kita lakukan adalah mengidentifikasi apa yang kanonik, dan apa yang perlu dilakukan kelas kita untuk mengatasinya. Dan metode kanonik swapping adalah dengan std::swap. Inilah sebabnya mengapa fungsi anggota tidak berguna: mereka bukan bagaimana kita harus bertukar sesuatu, secara umum, dan tidak berpengaruh pada perilaku std::swap.

Kalau begitu, untuk membuat std::swappekerjaan kita harus menyediakan (dan std::vector<>seharusnya menyediakan) spesialisasi std::swap, kan?

namespace std
{
    template <> // important! specialization in std is OK, overloading is UB
    void swap(myclass&, myclass&)
    {
        // swap
    }
}

Yah itu pasti akan berhasil dalam kasus ini, tetapi ia memiliki masalah mencolok: fungsi spesialisasi tidak dapat parsial. Artinya, kami tidak dapat mengkhususkan kelas template dengan ini, hanya instantiations tertentu:

namespace std
{
    template <typename T>
    void swap<T>(myclass<T>&, myclass<T>&) // error! no partial specialization
    {
        // swap
    }
}

Metode ini berfungsi beberapa saat, tetapi tidak selalu. Pasti ada cara yang lebih baik.


Ada! Kita dapat menggunakan suatu friendfungsi, dan menemukannya melalui ADL :

namespace xyz
{
    struct myclass
    {
        friend void swap(myclass&, myclass&);
    };
}

Ketika kami ingin menukar sesuatu, kami kaitkan std::swap dan kemudian membuat panggilan yang tidak memenuhi syarat:

using std::swap; // allow use of std::swap...
swap(x, y); // ...but select overloads, first

// that is, if swap(x, y) finds a better match, via ADL, it
// will use that instead; otherwise it falls back to std::swap

Apa itu friendfungsi? Ada kebingungan di sekitar area ini.

Sebelum C ++ distandarisasi, friendfungsi melakukan sesuatu yang disebut "injeksi nama teman", di mana kode berperilaku seolah-olah fungsi tersebut telah ditulis dalam namespace sekitarnya. Misalnya, ini adalah pra-standar yang setara:

struct foo
{
    friend void bar()
    {
        // baz
    }
};

// turned into, pre-standard:    

struct foo
{
    friend void bar();
};

void bar()
{
    // baz
}

Namun, ketika ADL ditemukan ini dihapus. The friendFungsi bisa kemudian hanya ditemukan melalui ADL; jika Anda menginginkannya sebagai fungsi bebas, itu perlu dinyatakan demikian ( lihat ini , misalnya). Tapi lihat! Ada masalah.

Jika Anda hanya menggunakan std::swap(x, y), kelebihan Anda tidak akan pernah ditemukan, karena Anda telah secara eksplisit mengatakan "lihat std, dan tempat lain"! Inilah sebabnya mengapa beberapa orang menyarankan untuk menulis dua fungsi: satu sebagai fungsi yang dapat ditemukan melalui ADL , dan yang lainnya untuk menangani std::kualifikasi eksplisit .

Tapi seperti yang kita lihat, ini tidak bisa bekerja di semua kasus, dan kita berakhir dengan kekacauan yang jelek. Alih-alih, bertukar idiomatik memilih jalan lain: alih-alih menjadikannya tugas kelas untuk menyediakan std::swap, itu adalah tugas para penukar untuk memastikan mereka tidak menggunakan kualifikasi swap, seperti di atas. Dan ini cenderung bekerja dengan baik, selama orang tahu tentang itu. Tapi di situlah masalahnya: tidak perlu untuk menggunakan panggilan yang tidak memenuhi syarat!

Untuk membuat ini lebih mudah, beberapa perpustakaan seperti Boost menyediakan fungsi boost::swap, yang hanya melakukan panggilan wajar tanpa pengecualian swap, dengan std::swapsebagai namespace terkait. Ini membantu membuat hal-hal ringkas lagi, tetapi masih mengecewakan.

Perhatikan bahwa tidak ada perubahan dalam C ++ 11 untuk perilaku std::swap, yang saya dan orang lain salah sangka akan menjadi kasusnya. Jika Anda kesal dengan ini, baca di sini .


Singkatnya: fungsi anggota hanyalah noise, spesialisasi jelek dan tidak lengkap, tetapi friendfungsinya lengkap dan berfungsi. Dan saat Anda bertukar, gunakan boost::swapatau tidak berkualifikasi swapdengan std::swapterkait.


† Secara informal, nama dikaitkan jika akan dipertimbangkan selama panggilan fungsi. Untuk detailnya, baca §3.4.2. Dalam hal ini, std::swapbiasanya tidak dipertimbangkan; tetapi kami dapat mengaitkannya (menambahkannya ke kumpulan kelebihan yang dianggap tidak memenuhi syarat swap), memungkinkannya ditemukan.

GManNickG
sumber
10
Saya tidak setuju bahwa fungsi anggota hanyalah noise. Fungsi anggota memungkinkan misalnya std::vector<std::string>().swap(someVecWithData);, yang tidak mungkin dengan swapfungsi bebas karena kedua argumen dilewatkan oleh referensi non-const.
ildjarn
3
@ildjarn: Anda dapat melakukannya dengan dua baris. Memiliki fungsi anggota melanggar prinsip KERING.
GManNickG
4
@ GM: Prinsip KERING tidak berlaku jika salah satu diterapkan dalam hal yang lain. Jika tidak ada yang akan menganjurkan kelas dengan implementasi operator=, operator+dan operator+=, tapi jelas mereka operator di kelas yang relevan diterima / diharapkan ada untuk simetri. Hal yang sama berlaku untuk anggota swap+ namespace-lingkup swapdalam pendapat saya.
ildjarn
3
@ GMan saya pikir itu mempertimbangkan terlalu banyak fungsi. Sedikit diketahui, tetapi bahkan function<void(A*)> f; if(!f) { }dapat gagal hanya karena Amenyatakan operator!bahwa yang menerima fsama baiknya dengan fmiliknya sendiri operator!(tidak mungkin, tetapi dapat terjadi). Jika function<>penulis berpikir "ohh saya punya 'operator bool', mengapa saya harus menerapkan 'operator!'? Itu akan melanggar KERING!", Itu akan berakibat fatal. Anda hanya perlu operator!menerapkan untuk A, dan Amemiliki konstruktor untuk function<...>, dan segala sesuatunya akan rusak, karena kedua kandidat akan memerlukan konversi yang ditentukan pengguna.
Johannes Schaub - litb
1
Mari kita pikirkan bagaimana kita berpikir tentang menulis fungsi swap [anggota]. Jadi, tentu saja, kelas kita juga seharusnya, kan? Yah, tidak juga. Perpustakaan standar memiliki segala macam hal yang tidak perlu , dan pertukaran anggota adalah salah satunya. GotW mengadvokasi pendukung untuk fungsi swap anggota.
Xeverous
7

Kode itu setara ( hampir di setiap cara) untuk:

class dumb_array
{
public:
    // ...
    friend void swap(dumb_array& first, dumb_array& second);
    // ...
};

inline void swap(dumb_array& first, dumb_array& second) // nothrow
{
    using std::swap; 
    swap(first.mSize, second.mSize); 
    swap(first.mArray, second.mArray);
}

Fungsi teman yang didefinisikan di dalam kelas adalah:

  • ditempatkan di namespace terlampir
  • secara otomatis inline
  • dapat merujuk pada anggota statis kelas tanpa kualifikasi lebih lanjut

Aturan persisnya ada di bagian [class.friend](saya kutip paragraf 6 dan 7 dari konsep C ++ 0x):

Fungsi dapat didefinisikan dalam deklarasi teman kelas jika dan hanya jika kelas tersebut adalah kelas non-lokal (9,8), nama fungsi tidak memenuhi syarat, dan fungsi tersebut memiliki lingkup namespace.

Fungsi semacam itu secara implisit sejalan. Fungsi teman yang didefinisikan dalam suatu kelas adalah dalam ruang lingkup (leksikal) dari kelas di mana ia didefinisikan. Fungsi teman yang didefinisikan di luar kelas tidak.

Ben Voigt
sumber
2
Sebenarnya, fungsi teman tidak ditempatkan di namespace terlampir, dalam standar C ++. Perilaku lama disebut "injeksi nama teman", tetapi digantikan oleh ADL, diganti dalam standar pertama. Lihat bagian atas ini . (Perilaku ini sangat mirip.)
GManNickG
1
Tidak benar-benar setara. Kode dalam pertanyaan membuatnya swaphanya terlihat oleh ADL. Itu adalah anggota namespace yang melampirkan, tetapi namanya tidak terlihat oleh formulir pencarian nama lainnya. EDIT: Saya melihat bahwa @ GM dapat lebih cepat lagi :) @ Kalau selalu seperti itu di ISO C ++ :)
Johannes Schaub - litb
2
@ Ben: Tidak, injeksi teman tidak pernah ada dalam standar, tapi itu digunakan secara luas sebelum itu sebabnya ide (dan dukungan kompiler) cenderung untuk melanjutkan, tetapi secara teknis tidak ada. friendfungsi hanya ditemukan oleh ADL, dan jika mereka hanya perlu fungsi bebas dengan friendakses, keduanya harus dinyatakan sebagai frienddi dalam kelas, dan sebagai deklarasi fungsi bebas normal di luar kelas. Anda dapat melihat keharusan itu dalam jawaban ini , misalnya.
GManNickG
2
@towi: Karena fungsi teman ada di lingkup namespace, jawaban untuk ketiga pertanyaan Anda akan menjadi jelas: (1) Ini adalah fungsi bebas, plus memiliki akses teman ke anggota kelas yang pribadi dan dilindungi. (2) Ini bukan anggota sama sekali, baik instance maupun statis. (3) ADL tidak mencari di dalam kelas tetapi ini ok karena fungsi teman memiliki lingkup namespace.
Ben Voigt
1
@ Ben. Dalam spek, fungsi adalah anggota namespace, dan frasa "fungsi memiliki lingkup namespace" dapat diartikan untuk mengatakan bahwa fungsi tersebut adalah anggota namespace (itu cukup banyak tergantung pada konteks pernyataan seperti itu). Dan itu menambahkan nama ke namespace yang hanya dapat dilihat oleh ADL (sebenarnya, IIRC beberapa bagian bertentangan dengan bagian lain dalam spesifikasi tentang apakah ada nama yang ditambahkan. Tetapi penambahan nama diperlukan untuk mendeteksi deklarasi yang tidak kompatibel ditambahkan ke yang namespace, sehingga sebenarnya, nama tak terlihat yang ditambahkan. Lihat catatan di 3.3.1p4).
Johannes Schaub - litb