Apa aturan dasar dan idiom untuk overloading operator?

2145

Catatan: Jawaban diberikan dalam urutan tertentu , tetapi karena banyak pengguna mengurutkan jawaban berdasarkan suara, daripada waktu mereka diberikan, inilah indeks jawaban sesuai urutan yang paling masuk akal:

(Catatan: Ini dimaksudkan sebagai entri untuk FAQ C ++ Stack Overflow . Jika Anda ingin mengkritik gagasan memberikan FAQ dalam formulir ini, maka posting pada meta yang memulai semua ini akan menjadi tempat untuk melakukan itu. Jawaban untuk pertanyaan itu dipantau di chatroom C ++ , di mana ide FAQ dimulai sejak awal, jadi jawaban Anda sangat mungkin untuk dibaca oleh mereka yang mengemukakan ide itu.)

sbi
sumber
63
Jika kita akan melanjutkan dengan tag C ++ - FAQ, beginilah cara entri harus diformat.
John Dibling
Saya telah menulis serangkaian artikel singkat untuk komunitas C ++ Jerman tentang overloading operator: Bagian 1: overloading operator di C ++ mencakup semantik, penggunaan tipikal dan spesialisasi untuk semua operator. Ada beberapa tumpang tindih dengan jawaban Anda di sini, namun ada beberapa informasi tambahan. Bagian 2 dan 3 membuat tutorial untuk menggunakan Boost.Operators. Apakah Anda ingin saya menerjemahkannya dan menambahkannya sebagai jawaban?
Arne Mertz
Oh, dan terjemahan bahasa Inggris juga tersedia: dasar-dasar dan praktik umum
Arne Mertz

Jawaban:

1044

Operator umum kelebihan beban

Sebagian besar pekerjaan di operator kelebihan muatan adalah kode boiler-plate. Itu tidak mengherankan, karena operator hanyalah gula sintaksis, pekerjaan mereka yang sebenarnya dapat dilakukan dengan (dan sering diteruskan ke) fungsi sederhana. Tetapi penting bahwa Anda mendapatkan kode pelat ketel ini dengan benar. Jika Anda gagal, kode operator Anda tidak dapat dikompilasi atau kode pengguna Anda tidak akan dikompilasi atau kode pengguna Anda akan berperilaku mengejutkan.

Operator Penugasan

Ada banyak yang bisa dikatakan tentang tugas. Namun, sebagian besar sudah dikatakan di FAQ Copy-and-Swap terkenal GMan , jadi saya akan melewatkan sebagian besar di sini, hanya daftar operator penugasan yang sempurna untuk referensi:

X& X::operator=(X rhs)
{
  swap(rhs);
  return *this;
}

Operator Bitshift (digunakan untuk Stream I / O)

Operator bitshift <<dan >>, meskipun masih digunakan dalam interfacing perangkat keras untuk fungsi manipulasi bit yang mereka warisi dari C, telah menjadi lebih lazim sebagai operator input dan output stream yang kelebihan beban di sebagian besar aplikasi. Untuk panduan kelebihan muatan sebagai operator manipulasi bit, lihat bagian di bawah ini tentang Operator Aritmatika Biner. Untuk menerapkan format kustom dan logika parsing saat objek Anda digunakan dengan iostreams, lanjutkan.

Operator stream, di antara operator yang paling sering kelebihan beban, adalah operator infiks biner dimana sintaksnya tidak menetapkan batasan apakah mereka harus anggota atau bukan. Karena mereka mengubah argumen kiri mereka (mereka mengubah keadaan aliran), mereka harus, menurut aturan praktis, diimplementasikan sebagai anggota tipe operan kiri mereka. Namun, operan kiri mereka adalah stream dari library standar, dan sementara sebagian besar output stream dan operator input didefinisikan oleh library standar memang didefinisikan sebagai anggota kelas stream, ketika Anda mengimplementasikan operasi output dan input untuk tipe Anda sendiri, Anda tidak dapat mengubah jenis aliran perpustakaan standar. Itu sebabnya Anda perlu mengimplementasikan operator ini untuk tipe Anda sendiri sebagai fungsi non-anggota. Bentuk kanonik keduanya adalah sebagai berikut:

std::ostream& operator<<(std::ostream& os, const T& obj)
{
  // write obj to stream

  return os;
}

std::istream& operator>>(std::istream& is, T& obj)
{
  // read obj from stream

  if( /* no valid object of T found in stream */ )
    is.setstate(std::ios::failbit);

  return is;
}

Saat menerapkan operator>>, mengatur keadaan aliran secara manual hanya diperlukan ketika pembacaan itu sendiri berhasil, tetapi hasilnya tidak seperti yang diharapkan.

Operator panggilan fungsi

Operator fungsi panggilan, yang digunakan untuk membuat objek fungsi, juga dikenal sebagai functors, harus didefinisikan sebagai fungsi anggota , sehingga selalu memiliki thisargumen implisit fungsi anggota. Selain ini, dapat kelebihan beban untuk mengambil sejumlah argumen tambahan, termasuk nol.

Berikut ini contoh sintaksnya:

class foo {
public:
    // Overloaded call operator
    int operator()(const std::string& y) {
        // ...
    }
};

Pemakaian:

foo f;
int a = f("hello");

Di seluruh pustaka standar C ++, objek fungsi selalu disalin. Objek fungsi Anda sendiri karenanya harus murah untuk disalin. Jika objek fungsi benar-benar perlu menggunakan data yang mahal untuk disalin, lebih baik menyimpan data itu di tempat lain dan meminta objek fungsi merujuknya.

Operator pembanding

Operator pembanding infiks biner harus, sesuai dengan aturan praktis, diimplementasikan sebagai fungsi non-anggota 1 . Negasi awalan unary !harus (sesuai dengan aturan yang sama) diimplementasikan sebagai fungsi anggota. (tapi biasanya itu bukan ide yang baik untuk membebani itu.)

Algoritme pustaka standar (mis. std::sort()) Dan tipe (mis std::map) akan selalu hanya berharap operator<untuk hadir. Namun, pengguna tipe Anda akan mengharapkan semua operator lain juga hadir , jadi jika Anda mendefinisikan operator<, pastikan untuk mengikuti aturan mendasar ketiga dari operator yang berlebihan dan juga mendefinisikan semua operator perbandingan boolean lainnya. Cara kanonik untuk mengimplementasikannya adalah ini:

inline bool operator==(const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator!=(const X& lhs, const X& rhs){return !operator==(lhs,rhs);}
inline bool operator< (const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator> (const X& lhs, const X& rhs){return  operator< (rhs,lhs);}
inline bool operator<=(const X& lhs, const X& rhs){return !operator> (lhs,rhs);}
inline bool operator>=(const X& lhs, const X& rhs){return !operator< (lhs,rhs);}

Yang penting untuk dicatat di sini adalah bahwa hanya dua dari operator ini yang benar-benar melakukan apa saja, yang lain hanya meneruskan argumen mereka ke salah satu dari dua ini untuk melakukan pekerjaan yang sebenarnya.

Sintaks untuk overloading operator boolean biner yang tersisa ( ||, &&) mengikuti aturan operator perbandingan. Namun, sangat tidak mungkin bahwa Anda akan menemukan use case yang masuk akal untuk 2 ini .

1 Seperti semua aturan praktis lainnya, terkadang mungkin ada alasan untuk melanggarnya. Jika demikian, jangan lupa bahwa operan kiri dari operator perbandingan biner, yang untuk fungsi anggota akan *this, perlu constjuga demikian. Jadi operator perbandingan yang diimplementasikan sebagai fungsi anggota harus memiliki tanda tangan ini:

bool operator<(const X& rhs) const { /* do actual comparison with *this */ }

(Catat constdi bagian akhir.)

2 Perlu dicatat bahwa versi built-in ||dan &&menggunakan cara pintas semantik. Sementara yang ditentukan pengguna (karena mereka adalah sintaksis gula untuk panggilan metode) tidak menggunakan semantik pintasan. Pengguna akan mengharapkan operator ini untuk memiliki semantik pintasan, dan kode mereka mungkin bergantung padanya, Oleh karena itu sangat disarankan untuk tidak mendefinisikannya.

Operator Aritmatika

Operator aritmatika unary

Operator kenaikan dan penurunan unary datang dalam rasa awalan dan postfix. Untuk membedakan satu dari yang lain, varian postfix mengambil argumen int dummy tambahan. Jika Anda menambah atau mengurangi secara berlebihan, pastikan untuk selalu menerapkan versi awalan dan postfix. Berikut ini adalah implementasi kenaikan kanonik, penurunan mengikuti aturan yang sama:

class X {
  X& operator++()
  {
    // do actual increment
    return *this;
  }
  X operator++(int)
  {
    X tmp(*this);
    operator++();
    return tmp;
  }
};

Perhatikan bahwa varian postfix diimplementasikan dalam bentuk awalan. Perhatikan juga bahwa postfix melakukan salinan tambahan. 2

Overload minus dan plus unary tidak terlalu umum dan mungkin sebaiknya dihindari. Jika diperlukan, mereka mungkin harus kelebihan beban sebagai fungsi anggota.

2 Juga perhatikan bahwa varian postfix bekerja lebih banyak dan karenanya kurang efisien untuk digunakan daripada varian prefix. Ini adalah alasan yang bagus untuk secara umum lebih memilih kenaikan awalan daripada kenaikan postfix. Sementara kompiler biasanya dapat mengoptimalkan pekerjaan tambahan dari penambahan postfix untuk tipe bawaan, mereka mungkin tidak dapat melakukan hal yang sama untuk tipe yang ditentukan pengguna (yang bisa menjadi sesuatu yang tampak seperti daftar iterator). Setelah Anda terbiasa melakukannya i++, menjadi sangat sulit untuk diingat untuk melakukan ++isebaliknya ketika ibukan dari tipe built-in (ditambah Anda harus mengubah kode ketika mengubah jenis), jadi lebih baik untuk membuat kebiasaan untuk selalu menggunakan peningkatan awalan, kecuali postfix secara eksplisit diperlukan.

Operator aritmatika biner

Untuk operator aritmatika biner, jangan lupa mematuhi aturan dasar operator ketiga yang berlebihan: Jika Anda memberikan +, juga memberikan +=, jika Anda memberikan -, jangan menghilangkan -=, dll. Andrew Koenig dikatakan sebagai orang pertama yang mengamati penugasan majemuk. operator dapat digunakan sebagai pangkalan untuk rekanan non-majemuk mereka. Artinya, operator +diimplementasikan dalam hal +=, -diimplementasikan dalam hal -=dll.

Menurut aturan praktis kami, +dan teman-temannya harus bukan anggota, sedangkan rekan tugas penugasan mereka ( +=dll.), Mengubah argumen kiri mereka, harus menjadi anggota. Berikut adalah contoh teladan untuk +=dan +; operator aritmatika biner lainnya harus diimplementasikan dengan cara yang sama:

class X {
  X& operator+=(const X& rhs)
  {
    // actual addition of rhs to *this
    return *this;
  }
};
inline X operator+(X lhs, const X& rhs)
{
  lhs += rhs;
  return lhs;
}

operator+=mengembalikan hasilnya per referensi, sementara operator+mengembalikan salinan hasilnya. Tentu saja, mengembalikan referensi biasanya lebih efisien daripada mengembalikan salinan, tetapi dalam kasus operator+, tidak ada jalan lain untuk menyalin. Ketika Anda menulis a + b, Anda mengharapkan hasilnya menjadi nilai baru, itulah sebabnya mengapa operator+harus mengembalikan nilai baru. 3 Juga perhatikan bahwa operator+mengambil operan kirinya dengan menyalin daripada dengan referensi const. Alasan untuk ini sama dengan alasan memberi untuk operator=mengambil argumennya per salinan.

Operator manipulasi bit ~ & | ^ << >>harus diimplementasikan dengan cara yang sama seperti operator aritmatika. Namun, (kecuali untuk overloading <<dan >>untuk output dan input) ada beberapa kasus penggunaan yang wajar untuk overloading ini.

3 Sekali lagi, pelajaran yang bisa diambil dari ini adalah bahwa a += b, secara umum, lebih efisien daripada a + bdan harus disukai jika memungkinkan.

Array Berlangganan

Operator subscript array adalah operator biner yang harus diimplementasikan sebagai anggota kelas. Ini digunakan untuk tipe seperti wadah yang memungkinkan akses ke elemen data mereka dengan kunci. Bentuk kanonik dari menyediakan ini adalah ini:

class X {
        value_type& operator[](index_type idx);
  const value_type& operator[](index_type idx) const;
  // ...
};

Kecuali Anda tidak ingin pengguna kelas Anda dapat mengubah elemen data yang dikembalikan oleh operator[](dalam hal ini Anda dapat menghilangkan varian non-const), Anda harus selalu menyediakan kedua varian operator.

Jika value_type diketahui merujuk ke tipe bawaan, varian const operator sebaiknya mengembalikan salinan daripada referensi const:

class X {
  value_type& operator[](index_type idx);
  value_type  operator[](index_type idx) const;
  // ...
};

Operator untuk Jenis Pointer-like

Untuk menentukan iterator atau smart pointer Anda sendiri, Anda harus membebani operator dereference awalan unary *dan operator akses anggota penunjuk infix biner ->:

class my_ptr {
        value_type& operator*();
  const value_type& operator*() const;
        value_type* operator->();
  const value_type* operator->() const;
};

Perhatikan bahwa ini juga hampir selalu membutuhkan versi const dan non-const. Untuk ->operator, jika value_typedari class(atau structatau union) tipe, yang lain operator->()disebut secara rekursif, sampai operator->()mengembalikan nilai tipe non-kelas.

Operator alamat-unary tidak boleh kelebihan beban.

Untuk operator->*()melihat pertanyaan ini . Ini jarang digunakan dan karena itu jarang kelebihan beban. Bahkan, iterator sekalipun tidak membebani terlalu banyak.


Lanjutkan ke Operator Konversi

sbi
sumber
89
operator->()sebenarnya sangat aneh. Ini tidak diharuskan untuk mengembalikan value_type*- pada kenyataannya, itu dapat mengembalikan tipe kelas yang lain, asalkan tipe kelas itu memilikioperator->() , yang kemudian akan dipanggil selanjutnya. Panggilan rekursif ini operator->()berlangsung hingga value_type*jenis kembali terjadi. Kegilaan! :)
j_random_hacker
2
Ini bukan tentang efektivitas. Ini tentang kita tidak dapat melakukannya dengan cara tradisional-idiomatik dalam (sangat) beberapa kasus: ketika definisi dari kedua operan perlu tetap tidak berubah saat kita menghitung hasilnya. Dan seperti yang saya katakan, ada dua contoh klasik: multiplikasi matriks, dan multiplikasi polinomial. Kita dapat mendefinisikan *dalam hal *=tetapi itu akan menjadi canggung karena salah satu operasi pertama *=akan membuat objek baru, hasil dari perhitungan. Kemudian, setelah for-ijk loop, kita akan menukar objek sementara ini dengan *this. yaitu. 1. copy, 2.operator *, 3.swap
Luc Hermitte
6
Saya tidak setuju dengan versi const / non-const dari operator seperti-pointer Anda, misalnya `const value_type & operator * () const;` - ini akan seperti memiliki T* constpengembalian const T&pada dereferencing, yang tidak demikian. Atau dengan kata lain: pointer const tidak menyiratkan pointee const. Bahkan, itu tidak sepele untuk meniru T const *- yang merupakan alasan untuk seluruh const_iteratorhal di perpustakaan standar. Kesimpulan: tanda tangan harusreference_type operator*() const; pointer_type operator->() const
Arne Mertz
6
Satu komentar: Implementasi operator aritmatika biner yang disarankan tidak seefisien mungkin. Se Boost operator tajuk simetri catatan: boost.org/doc/libs/1_54_0/libs/utility/operators.htm#symmetry Satu salinan lagi dapat dihindari jika Anda menggunakan salinan lokal dari parameter pertama, lakukan + =, dan kembalikan salinan lokal. Ini memungkinkan optimasi NRVO.
Manu343726
3
Seperti yang saya sebutkan dalam obrolan, L <= Rjuga bisa dinyatakan sebagai !(R < L)bukan !(L > R). Mungkin menyimpan lapisan tambahan inlining dalam ekspresi yang sulit dioptimalkan (dan itu juga bagaimana Boost.Operators mengimplementasikannya).
TemplateRex
494

Tiga Aturan Dasar Operator Overloading di C ++

Ketika datang ke operator overloading di C ++, ada tiga aturan dasar yang harus Anda ikuti . Seperti halnya semua aturan semacam itu, memang ada pengecualian. Kadang-kadang orang menyimpang dari mereka dan hasilnya bukan kode yang buruk, tetapi penyimpangan positif seperti itu sangat sedikit. Paling tidak, 99 dari 100 penyimpangan yang saya lihat tidak bisa dibenarkan. Namun, mungkin hanya 999 dari 1000. Jadi sebaiknya Anda tetap pada aturan berikut.

  1. Kapan pun makna operator tidak jelas dan tidak perlu dipersoalkan, itu tidak boleh kelebihan beban. Sebagai gantinya, berikan fungsi dengan nama yang dipilih dengan baik.
    Pada dasarnya, aturan pertama dan terpenting untuk operator yang kelebihan beban, pada intinya, mengatakan: Jangan lakukan itu . Itu mungkin tampak aneh, karena ada banyak yang harus diketahui tentang kelebihan operator dan begitu banyak artikel, bab buku, dan teks-teks lain berurusan dengan semua ini. Tetapi terlepas dari bukti yang tampak jelas ini, hanya ada beberapa kasus yang mengejutkan di mana operator kelebihan beban sesuai. Alasannya adalah bahwa sebenarnya sulit untuk memahami semantik di balik aplikasi operator kecuali jika penggunaan operator dalam domain aplikasi diketahui dan tidak perlu dipersoalkan. Bertentangan dengan kepercayaan populer, ini hampir tidak pernah terjadi.

  2. Selalu berpegang pada semantik yang terkenal dari operator.
    C ++ tidak memberikan batasan pada semantik dari operator yang kelebihan beban. Kompiler Anda akan dengan senang hati menerima kode yang mengimplementasikan+operatorbineruntuk mengurangi dari operan yang tepat. Namun, pengguna dari operator tersebut tidak akan pernah menduga ekspresia + buntuk mengurangiadarib. Tentu saja, ini mengandaikan bahwa semantik operator dalam domain aplikasi tidak perlu dipersoalkan lagi.

  3. Selalu sediakan semua dari serangkaian operasi terkait.
    Operator terkait satu sama lain dan dengan operasi lain. Jika tipe Anda mendukunga + b, pengguna juga diharapkan dapat menelepona += b. Jika mendukung peningkatan awalan++a, mereka akana++bekerja dengan baik. Jika mereka dapat memeriksa apakaha < b, mereka pasti akan berharap juga untuk dapat memeriksa apakaha > b. Jika mereka dapat menyalin-membangun tipe Anda, mereka mengharapkan tugas juga berfungsi.


Lanjutkan ke Keputusan antara Anggota dan Non-anggota .

sbi
sumber
16
Satu-satunya yang saya tahu yang melanggar semua ini adalah boost::spiritlol.
Billy ONeal
66
@Billy: Menurut beberapa orang, menyalahgunakan +penggabungan string adalah pelanggaran, tetapi sekarang sudah menjadi praksis yang sudah mapan, sehingga tampak alami. Meskipun saya ingat kelas string home-brew yang saya lihat di tahun 90-an yang menggunakan biner &untuk tujuan ini (merujuk pada BASIC untuk praksis yang dibuat). Tapi, ya, memasukkannya ke std lib pada dasarnya mengatur ini di atas batu. Hal yang sama berlaku untuk penyalahgunaan <<dan >>untuk IO, BTW. Mengapa pergeseran kiri menjadi operasi output yang jelas? Karena kita semua mempelajarinya ketika kita melihat "Halo, dunia!" aplikasi. Dan tanpa alasan lain.
sbi
5
@curiousguy: Jika Anda harus menjelaskannya, itu jelas tidak jelas dan tidak perlu dipersoalkan. Demikian juga jika Anda perlu mendiskusikan atau mempertahankan kelebihan muatan.
sbi
5
@sbi: "peer review" selalu merupakan ide yang bagus. Bagi saya operator yang dipilih dengan buruk tidak berbeda dari nama fungsi yang dipilih dengan buruk (saya melihat banyak). Operator hanyalah fungsi. Tidak lebih, tidak kurang. Aturannya sama saja. Dan untuk memahami jika sebuah ide bagus, cara terbaik adalah memahami berapa lama waktu yang dibutuhkan untuk dipahami. (Karenanya, peer review adalah suatu keharusan, tetapi teman sebaya harus dipilih di antara orang-orang yang bebas dari dogma dan prasangka.)
Emilio Garavaglia
5
@ sbi Bagi saya, satu-satunya fakta yang benar-benar jelas dan tidak dapat dibantah operator==adalah bahwa itu harus menjadi hubungan ekivalensi (TKI, Anda tidak boleh menggunakan NaN non pensinyalan). Ada banyak hubungan kesetaraan yang berguna pada wadah. Apa arti kesetaraan? " asama dengan b" berarti itu adan bmemiliki nilai matematika yang sama. Konsep nilai matematika dari (non-NaN) floatjelas, tetapi nilai matematika dari suatu wadah dapat memiliki banyak definisi yang berguna (tipe rekursif). Definisi kesetaraan terkuat adalah "mereka adalah objek yang sama", dan itu tidak berguna.
curiousguy
265

Sintaks Umum operator kelebihan muatan di C ++

Anda tidak dapat mengubah arti operator untuk tipe bawaan di C ++, operator hanya bisa kelebihan beban untuk tipe yang ditentukan pengguna 1 . Artinya, setidaknya satu operan harus dari tipe yang ditentukan pengguna. Seperti halnya fungsi kelebihan beban lainnya, operator dapat kelebihan beban untuk satu set parameter tertentu hanya sekali.

Tidak semua operator dapat kelebihan muatan di C ++. Di antara operator yang tidak dapat kelebihan beban adalah: . :: sizeof typeid .*dan satu-satunya operator ternary di C ++,?:

Di antara operator yang dapat kelebihan beban di C ++ adalah ini:

  • operator aritmatika: + - * / %dan += -= *= /= %=(semua infiks biner); + -(awalan unary); ++ --(awalan dan postfix unary)
  • manipulasi bit: & | ^ << >>dan &= |= ^= <<= >>=(semua infix biner); ~(awalan unary)
  • aljabar boolean: == != < > <= >= || &&(semua infiks biner); !(awalan unary)
  • manajemen memori: new new[] delete delete[]
  • operator konversi tersirat
  • miscellany: = [] -> ->* , (semua infix biner); * &(semua awalan unary) ()(panggilan fungsi, infix n-ary)

Namun, fakta bahwa Anda dapat membebani semua ini tidak berarti Anda harus melakukannya. Lihat aturan dasar overloading operator.

Dalam C ++, operator kelebihan beban dalam bentuk fungsi dengan nama khusus . Seperti fungsi lainnya, operator kelebihan beban umumnya dapat diimplementasikan baik sebagai fungsi anggota dari jenis operan kiri mereka atau sebagai fungsi non-anggota . Apakah Anda bebas memilih atau terikat untuk menggunakan salah satu tergantung pada beberapa kriteria. 2 Operator unary @3 , diterapkan pada objek x, dipanggil sebagai operator@(x)atau sebagai x.operator@(). Operator infiks biner @, diterapkan pada objek xdan y, disebut sebagai operator@(x,y)atau sebagai x.operator@(y). 4

Operator yang diimplementasikan sebagai fungsi non-anggota kadang-kadang berteman dengan tipe operan mereka.

1 Istilah "yang ditentukan pengguna" mungkin sedikit menyesatkan. C ++ membuat perbedaan antara tipe bawaan dan tipe yang ditentukan pengguna. Untuk mantan milik misalnya int, char, dan double; yang terakhir termasuk semua tipe struct, class, union, dan enum, termasuk yang dari library standar, meskipun mereka tidak, dengan demikian, didefinisikan oleh pengguna.

2 Ini dibahas di bagian selanjutnya dari FAQ ini.

3 Ini @bukan operator yang valid di C ++ yang mengapa saya menggunakannya sebagai pengganti.

4 Satu-satunya operator ternary di C ++ tidak dapat kelebihan beban dan satu-satunya operator n-ary harus selalu diimplementasikan sebagai fungsi anggota.


Lanjutkan ke Tiga Aturan Dasar tentang Kelebihan Operator di C ++ .

sbi
sumber
~adalah awalan unary, bukan infix biner.
mrkj
1
.*hilang dari daftar operator yang tidak kelebihan muatan.
celticminstrel
1
@Mateen Saya ingin menggunakan placeholder alih-alih operator sungguhan untuk menjelaskan bahwa ini bukan tentang operator khusus, tetapi berlaku untuk semuanya. Dan, jika Anda ingin menjadi programmer C ++, Anda harus belajar untuk memperhatikan bahkan pada smallprint. :)
sbi
1
@ HR: Seandainya Anda membaca panduan ini, Anda akan tahu apa yang salah. Saya biasanya menyarankan Anda harus membaca tiga jawaban pertama yang dikaitkan dari pertanyaan. Itu seharusnya tidak lebih dari setengah jam hidup Anda, dan memberi Anda pemahaman dasar. Sintaks khusus operator yang dapat Anda cari nanti. Masalah khusus Anda menunjukkan Anda mencoba untuk membebani operator+()sebagai fungsi anggota, tetapi memberinya tanda tangan dari fungsi bebas. Lihat di sini .
sbi
1
@ sbi: Saya sudah membaca tiga posting pertama dan terima kasih telah membuatnya. :) Saya akan mencoba menyelesaikan masalah jika tidak saya pikir lebih baik untuk menanyakannya pada pertanyaan terpisah. Sekali lagi terima kasih telah membuat hidup jadi mudah bagi kami! : D
Hosein Rahnama
251

Keputusan antara Anggota dan Non-anggota

Operator biner =(penugasan), [](langganan langganan), ->(akses anggota), serta ()operator n-ary (panggilan fungsi), harus selalu diimplementasikan sebagai fungsi anggota , karena sintaks bahasa mengharuskannya.

Operator lain dapat diimplementasikan sebagai anggota atau bukan anggota. Beberapa dari mereka, bagaimanapun, biasanya harus diimplementasikan sebagai fungsi non-anggota, karena operan kiri mereka tidak dapat dimodifikasi oleh Anda. Yang paling menonjol dari ini adalah operator input dan output <<dan >>, yang operan kirinya adalah kelas aliran dari perpustakaan standar yang tidak dapat Anda ubah.

Untuk semua operator di mana Anda harus memilih untuk mengimplementasikannya sebagai fungsi anggota atau fungsi bukan anggota, gunakan aturan praktis berikut untuk memutuskan:

  1. Jika itu adalah operator unary , implementasikan sebagai fungsi anggota .
  2. Jika operator biner memperlakukan kedua operan secara sama (tidak berubah), implementasikan operator ini sebagai fungsi non-anggota .
  3. Jika operator biner tidak memperlakukan kedua operandnya dengan sama (biasanya operan kirinya akan berubah), mungkin berguna untuk menjadikannya fungsi anggota dari tipe operan kirinya, jika harus mengakses bagian pribadi operan.

Tentu saja, seperti semua aturan praktis lainnya, ada pengecualian. Jika Anda memiliki tipe

enum Month {Jan, Feb, ..., Nov, Dec}

dan Anda ingin membebani operator kenaikan dan penurunan secara berlebihan, Anda tidak dapat melakukan ini sebagai fungsi anggota, karena di C ++, tipe enum tidak dapat memiliki fungsi anggota. Jadi, Anda harus membebani itu sebagai fungsi gratis. Dan operator<()untuk templat kelas yang bersarang di dalam templat kelas jauh lebih mudah untuk menulis dan membaca bila dilakukan sebagai fungsi anggota sebaris dalam definisi kelas. Tapi ini memang pengecualian yang jarang.

(Namun, jika Anda membuat pengecualian, jangan lupa masalah const-ness untuk operan yang, untuk fungsi anggota, menjadi thisargumen implisit . Jika operator sebagai fungsi non-anggota akan mengambil argumen paling kiri sebagai constreferensi , operator yang sama sebagai fungsi anggota perlu memiliki constdi akhir untuk membuat *thissebuah constreferensi.)


Lanjutkan ke Operator umum untuk melakukan overload .

sbi
sumber
9
Item Herb Sutter dalam C ++ Efektif (atau apakah C ++ Standar Pengkodean?) Mengatakan seseorang harus lebih memilih fungsi non-teman yang bukan anggota daripada fungsi anggota, untuk meningkatkan enkapsulasi kelas. IMHO, alasan enkapsulasi lebih diutamakan dari aturan praktis Anda, tetapi tidak mengurangi nilai kualitas aturan praktis Anda.
paercebal
8
@paercebal: C ++ Efektif adalah oleh Meyers, C ++ Standar Pengodean oleh Sutter. Yang mana yang Anda maksud? Bagaimanapun, saya tidak menyukai gagasan, katakanlah, operator+=()tidak menjadi anggota. Ia harus mengubah operan tangan kirinya, jadi menurut definisi ia harus menggali lebih dalam ke jeroan. Apa yang akan Anda dapatkan dengan tidak menjadikannya anggota?
sbi
9
@ sbi: Butir 44 dalam C ++ Standar Pengkodean (Sutter) Lebih suka menulis fungsi bukan teman yang bukan anggota , tentu saja, itu hanya berlaku jika Anda benar-benar dapat menulis fungsi ini hanya dengan menggunakan antarmuka publik dari kelas tersebut. Jika Anda tidak bisa (atau bisa tetapi itu akan menghambat kinerja), maka Anda harus menjadikannya anggota atau teman.
Matthieu M.
3
@sbi: Ups, Efektif, Luar Biasa ... Tidak heran saya mencampur nama-nama itu. Pokoknya keuntungannya adalah membatasi sebanyak mungkin jumlah fungsi yang memiliki akses ke objek data pribadi / dilindungi. Dengan cara ini, Anda meningkatkan enkapsulasi kelas Anda, menjadikan pemeliharaan / pengujian / evolusi lebih mudah.
paercebal
12
@sbi: Salah satu contoh. Katakanlah Anda sedang mengkode kelas String, dengan metode operator +=dan keduanya append. The appendMetode ini lebih lengkap, karena Anda dapat menambahkan sebuah substring parameter dari indeks i indeks n -1: append(string, start, end)Tampaknya logis untuk memiliki +=panggilan append dengan start = 0dan end = string.size. Pada saat itu, append dapat menjadi metode anggota, tetapi operator +=tidak perlu menjadi anggota, dan menjadikannya non-anggota akan mengurangi jumlah bermain kode dengan String dalam, jadi itu adalah hal yang baik .... ^ _ ^ ...
paercebal
165

Operator Konversi (juga dikenal sebagai Konversi Buatan Pengguna)

Di C ++ Anda dapat membuat operator konversi, operator yang memungkinkan kompiler untuk mengkonversi antara tipe Anda dan tipe yang ditentukan lainnya. Ada dua jenis operator konversi, yang implisit dan eksplisit.

Operator Konversi Tersirat (C ++ 98 / C ++ 03 dan C ++ 11)

Operator konversi implisit memungkinkan kompiler untuk secara implisit mengkonversi (seperti konversi antara intdan long) nilai tipe yang ditentukan pengguna ke beberapa tipe lainnya.

Berikut ini adalah kelas sederhana dengan operator konversi implisit:

class my_string {
public:
  operator const char*() const {return data_;} // This is the conversion operator
private:
  const char* data_;
};

Operator konversi tersirat, seperti konstruktor satu argumen, adalah konversi yang ditentukan pengguna. Kompiler akan memberikan satu konversi yang ditentukan pengguna ketika mencoba mencocokkan panggilan ke fungsi yang kelebihan beban.

void f(const char*);

my_string str;
f(str); // same as f( str.operator const char*() )

Pada awalnya ini tampaknya sangat membantu, tetapi masalah dengan ini adalah bahwa konversi implisit bahkan menendang ketika itu tidak diharapkan. Dalam kode berikut, void f(const char*)akan dipanggil karena my_string()bukan nilai , jadi yang pertama tidak cocok:

void f(my_string&);
void f(const char*);

f(my_string());

Pemula dengan mudah mendapatkan kesalahan ini dan bahkan programmer C ++ yang berpengalaman terkadang terkejut karena kompiler mengambil kelebihan yang tidak mereka duga. Masalah-masalah ini dapat dikurangi oleh operator konversi eksplisit.

Operator Konversi Eksplisit (C ++ 11)

Tidak seperti operator konversi implisit, operator konversi eksplisit tidak akan pernah memulai ketika Anda tidak mengharapkannya. Berikut ini adalah kelas sederhana dengan operator konversi eksplisit:

class my_string {
public:
  explicit operator const char*() const {return data_;}
private:
  const char* data_;
};

Perhatikan explicit. Sekarang ketika Anda mencoba untuk mengeksekusi kode yang tidak terduga dari operator konversi implisit, Anda mendapatkan kesalahan kompilator:

prog.cpp: Dalam fungsi 'int main ()':
prog.cpp: 15: 18: error: tidak ada fungsi yang cocok untuk panggilan ke 'f (my_string)'
prog.cpp: 15: 18: note: kandidat adalah:
prog.cpp: 11: 10: note: void f (my_string &)
prog.cpp: 11: 10: note: tidak ada konversi untuk argumen 1 dari 'my_string' menjadi 'my_string &'
prog.cpp: 12: 10: note: void f (const char *)
prog.cpp: 12: 10: note: tidak ada konversi untuk argumen 1 dari 'my_string' ke 'const char *'

Untuk memanggil operator pemeran eksplisit, Anda harus menggunakan static_cast, pemeran gaya-C, atau pemain gaya konstruktor (yaitu T(value)).

Namun, ada satu pengecualian untuk ini: Kompiler diizinkan untuk secara implisit dikonversi ke bool. Selain itu, kompiler tidak diperbolehkan untuk melakukan konversi implisit lain setelah dikonversi menjadi bool(kompiler diperbolehkan untuk melakukan 2 konversi implisit sekaligus, tetapi hanya 1 konversi yang ditentukan pengguna pada maks).

Karena kompiler tidak akan menampilkan "masa lalu" bool, operator konversi eksplisit sekarang menghapus kebutuhan akan idiom Bool Aman . Misalnya, pointer cerdas sebelum C ++ 11 menggunakan idiom Safe Bool untuk mencegah konversi ke tipe integral. Dalam C ++ 11, smart pointer menggunakan operator eksplisit karena compiler tidak diperbolehkan untuk secara implisit mengkonversi ke tipe integral setelah secara eksplisit mengkonversi tipe menjadi bool.

Lanjutkan ke Overloading newdandelete .

JKor
sumber
148

Overloading newdandelete

Catatan: Ini hanya berkaitan dengan sintaks kelebihan muatannewdandelete, bukan dengan implementasi dari operator kelebihan beban tersebut. Saya pikir semantik overloadingnew dan deletelayak mendapatkan FAQ mereka sendiri , dalam topik overloading operator saya tidak pernah bisa melakukannya dengan adil.

Dasar-dasar

Di C ++, ketika Anda menulis ekspresi baru seperti new T(arg)dua hal terjadi ketika ekspresi ini dievaluasi: Pertama operator newdipanggil untuk mendapatkan memori mentah, dan kemudian konstruktor yang sesuai Tdipanggil untuk mengubah memori mentah ini menjadi objek yang valid. Demikian juga, ketika Anda menghapus objek, pertama-tama destruktornya dipanggil, dan kemudian memori dikembalikan ke operator delete.
C ++ memungkinkan Anda untuk menyempurnakan kedua operasi ini: manajemen memori dan konstruksi / penghancuran objek pada memori yang dialokasikan. Yang terakhir dilakukan dengan menulis konstruktor dan destruktor untuk suatu kelas. Pengaturan memori yang baik dilakukan dengan menulis sendiri operator newdan operator delete.

Yang pertama dari aturan dasar overloading operator - jangan lakukan itu - berlaku terutama untuk overloading newdan delete. Hampir satu-satunya alasan untuk membebani operator ini adalah masalah kinerja dan kendala memori , dan dalam banyak kasus, tindakan lain, seperti perubahan pada algoritma yang digunakan, akan memberikan rasio biaya / keuntungan yang jauh lebih tinggi daripada mencoba mengubah manajemen memori.

Pustaka standar C ++ dilengkapi dengan seperangkat yang telah ditentukan newdan deleteoperator. Yang paling penting adalah ini:

void* operator new(std::size_t) throw(std::bad_alloc); 
void  operator delete(void*) throw(); 
void* operator new[](std::size_t) throw(std::bad_alloc); 
void  operator delete[](void*) throw(); 

Dua yang pertama mengalokasikan / membatalkan alokasi memori untuk suatu objek, dua yang terakhir untuk array objek. Jika Anda memberikan versi Anda sendiri dari ini, mereka tidak akan kelebihan beban, tetapi ganti yang dari perpustakaan standar.
Jika Anda kelebihan beban operator new, Anda harus selalu juga membebani pencocokan operator delete, bahkan jika Anda tidak pernah bermaksud menyebutnya. Alasannya adalah, jika konstruktor melempar selama evaluasi ekspresi baru, sistem run-time akan mengembalikan memori ke operator deletepencocokan operator newyang dipanggil untuk mengalokasikan memori untuk membuat objek. Jika Anda tidak memberikan pencocokan operator delete, yang standar disebut, yang hampir selalu salah.
Jika Anda kelebihan newdan delete, Anda harus mempertimbangkan kelebihan varian array juga.

Penempatan new

C ++ memungkinkan operator baru dan menghapus untuk mengambil argumen tambahan.
Apa yang disebut penempatan baru memungkinkan Anda membuat objek di alamat tertentu yang diteruskan ke:

class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{ 
  X* p = new(buffer) X(/*...*/);
  // ... 
  p->~X(); // call destructor 
} 

Pustaka standar dilengkapi dengan kelebihan yang tepat dari operator baru dan hapus untuk ini:

void* operator new(std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete(void* p,void*) throw(); 
void* operator new[](std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete[](void* p,void*) throw(); 

Perhatikan bahwa, dalam kode contoh untuk penempatan yang baru diberikan di atas, operator deletetidak pernah dipanggil, kecuali jika konstruktor X melempar pengecualian.

Anda juga bisa kelebihan beban newdan deletedengan argumen lain. Seperti argumen tambahan untuk penempatan baru, argumen ini juga tercantum dalam tanda kurung setelah kata kunci new. Hanya karena alasan historis, varian seperti itu sering juga disebut penempatan baru, bahkan jika argumennya bukan untuk menempatkan objek pada alamat tertentu.

Khusus kelas baru dan hapus

Paling umum Anda akan ingin menyempurnakan manajemen memori karena pengukuran telah menunjukkan bahwa instance kelas tertentu, atau sekelompok kelas terkait, sering dibuat dan dihancurkan dan bahwa manajemen memori default sistem run-time, disesuaikan untuk kinerja umum, berurusan secara tidak efisien dalam kasus khusus ini. Untuk meningkatkan ini, Anda dapat membebani yang baru dan menghapus untuk kelas tertentu:

class my_class { 
  public: 
    // ... 
    void* operator new();
    void  operator delete(void*,std::size_t);
    void* operator new[](size_t);
    void  operator delete[](void*,std::size_t);
    // ... 
}; 

Kelebihan itu, baru dan hapus berperilaku seperti fungsi anggota statis. Untuk objek dari my_class, std::size_targumennya akan selalu sizeof(my_class). Namun, operator ini juga dipanggil untuk objek yang diturunkan secara dinamis dari kelas turunan , dalam hal ini mungkin lebih besar dari itu.

Global baru dan hapus

Untuk membebani global baru dan menghapus, cukup ganti operator standar perpustakaan dengan milik kami. Namun, hal ini jarang perlu dilakukan.

sbi
sumber
11
Saya juga tidak setuju bahwa mengganti operator global yang baru dan hapus biasanya untuk kinerja: sebaliknya, biasanya untuk penelusuran bug.
Yttrill
1
Anda juga harus mencatat, bahwa jika Anda menggunakan operator baru yang kelebihan beban Anda diharuskan juga memberikan operator penghapusan dengan argumen yang cocok. Anda mengatakan bahwa di bagian global baru / hapus di tempat yang tidak terlalu menarik.
Yttrill
13
@Ttrill Anda hal-hal yang membingungkan. The makna akan kelebihan beban. Apa yang dimaksud dengan "kelebihan operator" adalah bahwa artinya kelebihan beban. Ini tidak berarti bahwa fungsi secara harfiah kelebihan beban, dan khususnya operator yang baru tidak akan membebani versi Standar. @sbi tidak mengklaim sebaliknya. Sudah biasa menyebutnya "kelebihan muatan baru" seperti yang biasa dikatakan "operator tambahan kelebihan muatan".
Johannes Schaub - litb
1
@sbi: Lihat (atau lebih baik, tautkan ke) gotw.ca/publications/mill15.htm . Ini hanya praktik yang baik terhadap orang-orang yang terkadang menggunakan yang nothrowbaru.
Alexandre C.
1
"Jika Anda tidak memberikan penghapusan operator yang cocok, yang defaultnya disebut" -> Sebenarnya, jika Anda menambahkan argumen dan tidak membuat penghapusan yang cocok, tidak ada operator hapus dipanggil sama sekali, dan Anda memiliki kebocoran memori. (15.2.2, penyimpanan yang ditempati oleh objek dideallocated hanya jika ada ... operator delete ditemukan)
dascandy
46

Mengapa tidak bisa operator<<berfungsi untuk streaming objek ke std::coutatau ke file menjadi fungsi anggota?

Katakanlah Anda memiliki:

struct Foo
{
   int a;
   double b;

   std::ostream& operator<<(std::ostream& out) const
   {
      return out << a << " " << b;
   }
};

Karena itu, Anda tidak dapat menggunakan:

Foo f = {10, 20.0};
std::cout << f;

Karena operator<<kelebihan beban sebagai fungsi anggota Foo, LHS operator harus menjadi Fooobjek. Yang berarti, Anda akan diminta untuk menggunakan:

Foo f = {10, 20.0};
f << std::cout

yang sangat tidak intuitif.

Jika Anda mendefinisikannya sebagai fungsi non-anggota,

struct Foo
{
   int a;
   double b;
};

std::ostream& operator<<(std::ostream& out, Foo const& f)
{
   return out << f.a << " " << f.b;
}

Anda akan dapat menggunakan:

Foo f = {10, 20.0};
std::cout << f;

yang sangat intuitif.

R Sahu
sumber