Operatörün aşırı yüklenmesi için temel kurallar ve deyimler nelerdir?


2141

Not: Yanıtlar belirli bir sırayla verildi , ancak birçok kullanıcı yanıtları verildikleri zaman yerine oylara göre sıraladığından, yanıtların en anlamlı oldukları sıraya göre bir listesi:

(Not: Bu, Stack Overflow'ın C ++ SSS'si için bir giriş anlamına gelir . Bu formda bir SSS sağlama fikrini eleştirmek istiyorsanız, tüm bunları başlatan metadaki yayınlama bunu yapmak için yer olacaktır. bu soru SSS fikrinin ilk başta başladığı C ++ sohbet odasında izlenir , bu nedenle cevabınızın bu fikri ortaya çıkaranlar tarafından okunması muhtemeldir.)


63
C ++ - FAQ etiketi ile devam edersek, girişler bu şekilde biçimlendirilmelidir.
John Dibling

Alman C ++ topluluğu için operatör aşırı yüklenmesi hakkında kısa bir makale dizisi yazdım: Bölüm 1: C ++ 'da operatör aşırı yüklenmesi, tüm operatörler için semantik, tipik kullanım ve özellikleri kapsar. Burada yanıtlarınızla bazı örtüşmeler var, yine de bazı ek bilgiler var. Bölüm 2 ve 3 Boost.Operators kullanımı için bir öğretici yapar. Onları tercüme etmemi ve cevap olarak eklememi ister misiniz?
Arne Mertz

Oh, ve İngilizce bir çeviri de mevcuttur: temel bilgiler ve ortak uygulama
Arne Mertz

Yanıtlar:


1042

Aşırı yüklenecek ortak operatörler

Aşırı yükleme operatörlerindeki çalışmaların çoğu kazan plakası kodudur. Operatörler sadece sözdizimsel şeker oldukları için, gerçek çalışmaları düz işlevlerle yapılabilir (ve genellikle iletilir). Ancak bu kazan plakası kodunu doğru almanız önemlidir. Başarısız olursanız, operatörünüzün kodu derlenmez veya kullanıcılarınızın kodu derlenmez veya kullanıcılarınızın kodu şaşırtıcı şekilde davranır.

Atama operatörü

Görevlendirme hakkında söylenecek çok şey var. Bununla birlikte, çoğu GMan'ın ünlü Kopyala-Takas SSS bölümünde zaten söylendi , bu yüzden çoğunu burada atlayacağım, sadece referans için mükemmel atama operatörünü listeleyeceğim:

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

Bitshift Operatörleri (Akış G / Ç için kullanılır)

Bitshift operatörleri <<ve C'den devraldıkları >>bit manipülasyon fonksiyonları için hala donanım arayüzünde kullanılmasına rağmen, çoğu uygulamada aşırı yüklü akım giriş ve çıkış operatörleri olarak daha yaygın hale gelmiştir. Bit manipülasyon operatörleri olarak aşırı yükleme için, aşağıdaki İkili Aritmetik Operatörler bölümüne bakınız. Nesneniz iostreams ile kullanıldığında kendi özel biçiminizi uygulamak ve mantığı ayrıştırmak için devam edin.

Akış işleçleri, en çok aşırı yüklenen işleçler arasında, sözdiziminin üye olup olmamaları konusunda herhangi bir kısıtlama belirtmediği ikili infix işleçleridir. Sol argümanlarını değiştirdiklerinden (akışın durumunu değiştirdiler), başparmak kurallarına göre sol işlenen türünün üyeleri olarak uygulanmalıdırlar. Bununla birlikte, sol işlenenleri standart kitaplıktan gelen akışlardır ve standart kitaplık tarafından tanımlanan akış çıktısı ve girdi işleçlerinin çoğu gerçekten de kendi sınıflarınız için çıktı ve girdi işlemleri uyguladığınızda, akış sınıflarının üyeleri olarak tanımlanırken, standart kitaplığın akış türlerini değiştiremez. Bu nedenle bu operatörleri üye olmayan işlevler olarak kendi türleriniz için uygulamanız gerekir. İkisinin kanonik biçimleri şunlardır:

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;
}

Uygulama yaparken operator>>, akışın durumunu manuel olarak ayarlamak yalnızca okuma başarılı olduğunda gereklidir, ancak sonuç beklenen şey değildir.

İşlev çağrısı operatörü

İşlev nesneleri oluşturmak için kullanılan ve işlevler olarak da bilinen işlev çağrısı işlecinin üye işlevi olarak tanımlanması gerekir , bu nedenle her zaman thisüye işlevlerinin örtük argümanına sahiptir. Bunun dışında, sıfır da dahil olmak üzere herhangi bir sayıda ek argüman almak aşırı yüklenebilir.

İşte sözdiziminin bir örneği:

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

Kullanımı:

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

C ++ standart kitaplığı boyunca, işlev nesneleri her zaman kopyalanır. Bu nedenle kendi işlev nesnelerinizin kopyalanması ucuz olmalıdır. Bir işlev nesnesinin kesinlikle kopyalanması pahalı olan verileri kullanması gerekiyorsa, bu verileri başka bir yerde saklamak ve işlev nesnesinin başvurmasını sağlamak daha iyidir.

Karşılaştırma operatörleri

İkili infix karşılaştırma operatörleri, başparmak kurallarına göre üye olmayan fonksiyonlar 1 olarak uygulanmalıdır . Tekli önek olumsuzlaması !(aynı kurallara göre) bir üye işlevi olarak uygulanmalıdır. (ancak aşırı yüklenmesi genellikle iyi bir fikir değildir.)

Standart kütüphanenin algoritmaları (örn. std::sort()) Ve türleri (ör. std::map) Her zaman sadece operator<mevcut olmayı bekler . Bununla birlikte, türünüzün kullanıcıları diğer tüm operatörlerin de var olmasını bekler , bu nedenle, tanımlarsanız operator<, operatör aşırı yüklemesinin üçüncü temel kuralına uyduğunuzdan ve diğer tüm boolean karşılaştırma operatörlerini tanımladığınızdan emin olun. Onları uygulamanın kanonik yolu şudur:

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);}

Burada dikkat edilmesi gereken önemli nokta, bu operatörlerden sadece ikisinin gerçekte bir şey yapmasıdır, diğerleri sadece gerçek işi yapmak için argümanlarını bu ikisinden birine iletmektedir.

Kalan ikili boole işleçlerinin ( ||, &&) aşırı yüklenmesi sözdizimi , karşılaştırma işleçlerinin kurallarına uyar. Ancak, öyle çok Bunlar için makul kullanım davayı bulacağını olası 2 .

1 Tüm temel kurallarda olduğu gibi, bazen bunu kırmak için de nedenler olabilir. Eğer öyleyse, üye fonksiyonları için ikili karşılaştırma operatörlerinin sol tarafındaki işlenenin de olması *thisgerektiğini unutmayın const. Dolayısıyla, üye işlevi olarak uygulanan bir karşılaştırma operatörünün bu imzası olması gerekir:

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

(Sonundaki notu not edin const.)

2 Yerleşik sürümünün kısayol anlambiliminin kullanıldığı ||ve belirtildiği unutulmamalıdır &&. Kullanıcı tanımlı olanlar (yöntem çağrıları için sözdizimsel şeker oldukları için) kısayol anlambilimi kullanmazlar. Kullanıcı bu operatörlerin kısayol anlambilimine sahip olmasını bekler ve kodları buna bağlı olabilir, Bu nedenle ASLA bunları tanımlamanız kesinlikle tavsiye edilmez.

Aritmetik operatörler

Tekli aritmetik işleçler

Tekli arttırma ve azaltma operatörleri hem önek hem de düzeltme sonrası lezzetinde gelir. Birinden diğerine söylemek için postfix varyantları ek bir kukla int argümanı alır. Artışı veya azalmayı aşırı yüklerseniz, her zaman hem önek hem de son düzeltme sürümlerini uyguladığınızdan emin olun. İşte artışın kanonik uygulaması, azaltma aynı kuralları izler:

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

Postfix varyantının önek açısından uygulandığını unutmayın. Postfix'in fazladan bir kopya yaptığını da unutmayın. 2

Tekli eksi ve artı aşırı yükleme çok yaygın değildir ve muhtemelen en iyi şekilde önlenir. Gerekirse, üye işlevleri olarak aşırı yüklenmeleri gerekir.

2 Ayrıca postfix varyantının önek varyantından daha fazla çalıştığını ve bu nedenle kullanımı daha az verimli olduğunu unutmayın. Bu genellikle düzeltme sonrası artışa göre önek artışını tercih etmek için iyi bir nedendir. Derleyiciler genellikle yerleşik türler için ek düzeltme sonrası artış çalışmasını optimize edebilirken, kullanıcı tanımlı türler için aynı şeyi yapamayabilirler (bu, liste yineleyici gibi masum bir şekilde görünebilir). Yapmanız alıştım kez i++, bunu yapmak için hatırlamak çok zor olur ++izaman yerine idaha iyidir, böylece yerleşik bir türü (bir tür değiştirirken artı değişim koduna olurdu), daima alışkanlık haline ila değildir postfix açıkça gerekmedikçe önek artışı kullanılarak.

İkili aritmetik işleçler

İkili aritmetik operatörler, üçüncü temel kural operatör aşırı yüklenmesini itaat etmeyi unutmayın: Eğer sağlarsanız +, aynı zamanda sağlamak +=sağladığınız takdirde -, değil omit yok -=vs. Andrew Koenig bileşik atama olduğunu gözlemleyen ilk olduğu söylenir operatörler, bileşik olmayan muadilleri için bir baz olarak kullanılabilir. Yani, operatör olduğu +anlamında uygulanan +=, -açısından uygulanmaktadır -=vb

Temel kurallarımıza göre +ve arkadaşları, üye olmamalı, bileşik atama muadilleri ( +=vb.), Sol argümanlarını değiştirerek üye olmalıdır. Burada için örnek teşkil eden bir kod +=ve +; diğer ikili aritmetik işleçler aynı şekilde uygulanmalıdır:

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+=referans başına sonucunu döndürürken sonucunun operator+bir kopyasını döndürür. Tabii ki, bir referansın döndürülmesi genellikle bir kopyayı döndürmekten daha etkilidir, ancak bu durumda operator+kopyalamanın etrafında bir yol yoktur. Eğer yazarken a + b, sonuç neden olan yeni bir değer, olmasını bekliyoruz operator+yeni bir değer döndürmek zorundadır. 3 Ayrıca operator+sol işlenenini const referansı yerine kopya ile alan not edin . Bunun nedeni, operator=her kopya için argüman alma nedeniyle aynıdır .

Bit manipülasyon operatörleri ~ & | ^ << >>, aritmetik operatörlerle aynı şekilde uygulanmalıdır. Bununla birlikte, (aşırı yükleme <<ve >>çıkış ve giriş hariç), bunların aşırı yüklenmesi için çok az makul kullanım durumu vardır.

3 Yine, bundan alınacak ders a += b, genel olarak, a + bmümkünse daha verimli ve tercih edilmesi gerektiğidir.

Dizi Aboneliği

Dizi alt simge işleci, sınıf üyesi olarak uygulanması gereken bir ikili işleçtir. Veri öğelerine bir tuşla erişilmesini sağlayan kapsayıcı benzeri türler için kullanılır. Bunları sağlamanın kanonik şekli şudur:

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

Sınıfınızdaki kullanıcıların döndürdüğü veri öğelerini değiştirmesini istemiyorsanız operator[](bu durumda sabit olmayan değişkeni atlayabilirsiniz), her zaman operatörün her iki değişkenini de sağlamalısınız.

Value_type öğesinin yerleşik bir türe başvurduğu biliniyorsa, operatörün const varyantı const referansı yerine bir kopyasını daha iyi döndürmelidir:

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

İşaretçi Türleri için İşleçler

Kendi yineleyicilerinizi veya akıllı işaretçilerinizi tanımlamak için, tekli önek dereference operatörünü *ve ikili infix pointer üye erişim operatörünü aşırı yüklemeniz gerekir ->:

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

Bunların da neredeyse her zaman hem sabit hem de sabit olmayan bir sürüme ihtiyacı olacağını unutmayın. İçin ->operatörün, eğer value_typeait class(ya da structya da union, başka bir) tip operator->()bir kadar ardışık olarak adlandırılır operator->()olmayan sınıf türü getirileri bir değer.

Operatörün normal adresi asla aşırı yüklenmemelidir.

İçin operator->*()bkz bu soruyu . Nadiren kullanılır ve bu nedenle nadiren aşırı yüklenir. Aslında, yineleyiciler bile aşırı yüklenmez.


Dönüşüm Operatörlerine Devam Et


89
operator->()aslında son derece garip. A döndürmek gerekli değildir value_type*- aslında, daha sonra çağrılacak olan sınıf türünün bir olması koşuluylaoperator->() başka bir sınıf türünü döndürebilir . Bu operator->()s'nin özyinelemeli çağrılması bir value_type*dönüş türü oluşana kadar devam eder . Delilik! :)
j_random_hacker

2
Tam olarak etkililikle ilgili değil. Bu, (çok) birkaç durumda geleneksel-deyimsel şekilde yapamayız: sonucu hesaplarken her iki işlenen tanımının değişmeden kalması gerektiğinde. Dediğim gibi iki klasik örnek var: matrislerin çoğalması ve polinomların çoğalması. Biz tanımlayabiliriz *açısından *=ancak ilk operasyonda biri çünkü garip olurdu *=olurdu hesaplama sonucu yeni bir nesne oluşturmak için. Sonra, for-ijk döngüsünden sonra, bu geçici nesneyi takas ederiz *this. yani. 1. kopya, 2. operatör *, 3. değişim
Luc

6
Ben senin pointer benzeri operatörleri, örneğin `const VALUE_TYPE & operatör * () const const / const olmayan sürümleri ile katılmıyorum;` - bu gibi olur T* constgeri dönen bir const T&durum değildir, hangi dereferencing üzerinde. Veya başka bir deyişle: bir sabit işaretçi bir sabit işaret anlamına gelmez. Aslında, taklit etmek önemsiz değildir T const *- const_iteratorstandart kütüphanedeki tüm şeylerin nedeni budur . Sonuç: İmzareference_type operator*() const; pointer_type operator->() const
Arne Mertz

6
Bir yorum: Önerilen ikili aritmetik işleçlerin uygulanması olabildiğince verimli değildir. Se Boost operatörleri üstbilgileri simmetry notu: boost.org/doc/libs/1_54_0/libs/utility/operators.htm#symmetry İlk parametrenin yerel bir kopyasını kullanırsanız, + = ve yerel kopya. Bu NRVO optimizasyonunu mümkün kılar.
Manu343726

3
Sohbette bahsettiğim L <= Rgibi, !(R < L)yerine de ifade edilebilir !(L > R). Optimize edilmesi zor ifadelerde fazladan bir satır içi katman kaydedebilir (ve Boost.Operators bunu nasıl uygular).
TemplateRex

494

C ++ 'da Operatör Aşırı Yüklemesinin Üç Temel Kuralı

C ++ 'da operatör aşırı yüklenmesi söz konusu olduğunda, izlemeniz gereken üç temel kural vardır . Tüm bu kurallarda olduğu gibi, gerçekten de istisnalar vardır. Bazen insanlar onlardan saptılar ve sonuç kötü kod değildi, ancak bu tür olumlu sapmalar az ve çok arasında. En azından, gördüğüm 100 sapmadan 99'u haksızdı. Ancak, 1000'den 999 da olabilir. Bu nedenle, aşağıdaki kurallara bağlı kalsanız iyi olur.

  1. Bir operatörün anlamı açık ve anlaşılır değilse, aşırı yüklenmemelidir. Bunun yerine, iyi seçilmiş bir ada sahip bir işlev sağlayın.
    Temel olarak, aşırı yük operatörleri için ilk ve en önemli kural, kalbinde yer almaktadır: Yapmayın . Bu garip görünebilir, çünkü operatörün aşırı yüklenmesi ile ilgili bilinecek çok şey var ve bu nedenle birçok makale, kitap bölümü ve diğer metinler bunlarla ilgileniyor. Ancak bu bariz kanıtlara rağmen, operatörün aşırı yüklenmesinin uygun olduğu sadece şaşırtıcı derecede az sayıda durum var. Bunun nedeni, operatörün uygulama etki alanında kullanımı iyi bilinmediği ve tartışılmadığı sürece, bir işleç uygulamasının arkasındaki anlambilimi anlamak zordur. Popüler inanışın aksine, bu neredeyse hiç böyle değildir.

  2. Her zaman operatörün iyi bilinen semantiğine sadık kalın.
    C ++, aşırı yüklenmiş işleçlerin anlambilimi üzerinde herhangi bir sınırlama getirmez. Derleyiciniz, ikili+işleci doğru işleneninden çıkarmak içinuygulayan kodu memnuniyetle kabul edecektir. Ancak, böyle bir operatörün kullanıcıları ifadesini şüpheli aslaa + bçıkarmak içinagelenb. Tabii ki, bu, uygulama alanındaki operatörün anlambiliminin tartışmasız olduğunu varsayar.

  3. Her zaman ilgili işlemlerin tümünü sağlayın.
    Operatörler birbirleriyle ve diğer operasyonlarla ilgilidir. Türünüz destekliyorsaa + b, kullanıcılar da arayabilira += b. Önek artışını destekliyorsada çalışmayı++abeklerlera++. Eğer olup olmadığını kontrol edebilirlersea < b, kesinlikle de olup olmadığını kontrol etmeyi beklerlera > b. Türünüzü kopyalayıp oluşturabilirlerse, atamanın da çalışmasını beklerler.


Üye ve Üye Olmayanlar arasındaki Karara devam edin .


16
Bunlardan herhangi birini ihlal ettiğinin farkında olduğum tek şey boost::spiritlol.
Billy ONeal

66
@Billy: Bazılarına göre, +dize birleştirme için kötüye kullanım bir ihlaldir, ancak şu ana kadar köklü bir praksis haline geldi, böylece doğal görünüyor. Ev yapımı bir dize sınıfını hatırlasam da &, bu amaçla ikili kullanan 90'larda gördüm (yerleşik praksiler için BASIC'e atıfta bulunarak). Ama, evet, onu std lib'e koymak temelde bunu taşa koydu. Aynı şey kötüye kullanım <<ve >>IO, BTW için de geçerlidir. Neden sola kayma bariz çıktı işlemi olur? Çünkü ilk "Merhaba dünyamızı!" uygulama. Ve başka bir sebep olmadan.
sbi

5
@curiousguy: Eğer açıklamak zorunda kalırsanız, açık ve tartışmasız değil. Aynı şekilde aşırı yüklemeyi tartışmanız veya savunmanız gerekiyorsa.
sbi

5
@sbi: "akran değerlendirmesi" her zaman iyi bir fikirdir. Bana göre kötü seçilmiş bir operatör kötü seçilmiş bir fonksiyon isminden farklı değil (çok gördüm). Operatör sadece işlevlerdir. Ne fazla ne az. Kurallar aynı. Bir fikrin iyi olup olmadığını anlamak için en iyi yol anlaşılmanın ne kadar sürdüğünü anlamaktır. (Bu nedenle, hakemlik bir zorunluluktur, ancak akranlar dogmalar ve önyargısız insanlar arasında seçilmelidir.)
Emilio Garavaglia

5
Bana göre, kesinlikle açık ve tartışılmaz tek gerçek operator==, bunun bir denklik ilişkisi olması gerektiğidir (IOW, sinyalsiz NaN kullanmamalısınız). Konteynerler üzerinde birçok yararlı denklik ilişkisi vardır. Eşitlik ne demektir? " aeşittir b" anlamına gelir ave baynı matematiksel değere sahiptir. Bir (NaN olmayan) matematiksel değer kavramı floataçıktır, ancak bir kabın matematiksel değeri birçok farklı (tip özyinelemeli) yararlı tanımlamaya sahip olabilir. Eşitliğin en güçlü tanımı “aynı nesnelerdir” ve faydasızdır.
curiousguy

265

C ++ 'da aşırı operatör yüklemesinin Genel Sözdizimi

C ++ 'da yerleşik türler için işleçlerin anlamını değiştiremezsiniz, işleçler yalnızca kullanıcı tanımlı türler 1 için aşırı yüklenebilir . Yani, işlenenlerden en az birinin kullanıcı tanımlı tipte olması gerekir. Diğer aşırı yüklenmiş fonksiyonlarda olduğu gibi, operatörler belirli bir parametre seti için sadece bir kez aşırı yüklenebilir.

Tüm işleçler C ++ ile aşırı yüklenemez. Aşırı yüklenemeyen operatörler arasında şunlar vardır: . :: sizeof typeid .*ve C ++ 'daki tek üçlü operatör,?:

C ++ 'da aşırı yüklenebilen operatörler arasında şunlar vardır:

  • aritmetik işleçler: + - * / %ve += -= *= /= %=(tüm ikili ekler); + -(tekli önek); ++ --(tekli önek ve düzeltme)
  • bit manipülasyonu: & | ^ << >>ve &= |= ^= <<= >>=(tüm ikili girişler); ~(tekli önek)
  • boolean cebiri: == != < > <= >= || &&(tümü ikili giriş); !(tekli önek)
  • hafıza yönetimi: new new[] delete delete[]
  • örtük dönüşüm operatörleri
  • miscellany: = [] -> ->* , (tüm ikili kodlar); * &(tüm tekli önekler) ()(işlev çağrısı, n-ary infix)

Ancak, tüm bunları aşırı yükleyebilmeniz , bunu yapmanız gerektiği anlamına gelmez . Operatörün aşırı yüklenmesi ile ilgili temel kurallara bakın.

C ++ 'da işleçler, özel adlara sahip işlevler biçiminde aşırı yüklenir . Diğer fonksiyonlarda olduğu gibi, aşırı yüklenmiş operatörler genellikle sol işlenen tipinin bir üye fonksiyonu olarak veya üye olmayan fonksiyonlar olarak uygulanabilir . Bunlardan birini seçmekte veya kullanmakta özgür olmanız birkaç kritere bağlıdır. 2 Tekli operatör @3 bir amacı x uygulanan olarak ya çağrılır operator@(x)ya da x.operator@(). İkili bir infix operatörü@ , nesnelere uygulanan xve yaynı ya da adlandırılır operator@(x,y)ya da x.operator@(y). 4

Üye olmayan işlevler olarak uygulanan işleçler bazen kendi işlenen türlerinin dostudur.

1 “Kullanıcı tanımlı” terimi biraz yanıltıcı olabilir. C ++ yerleşik türler ile kullanıcı tanımlı türler arasında ayrım yapar. Birincisine örneğin int, char ve double; ikincisine, kullanıcılar tarafından tanımlanmadığı halde, standart kütüphaneden olanlar da dahil olmak üzere tüm yapı, sınıf, birleşim ve numaralandırma türleri aittir.

2 Bu kapsam dahilindedir bu SSS'nin sonraki bölümlerinde ele alınmaktadır.

3 @ Ben bir yer tutucu olarak kullanabilirsiniz nedenle C geçerli bir operatör ++ değil.

4 C ++ 'daki tek üçlü operatör aşırı yüklenemez ve tek n-ary operatörünün her zaman bir üye işlevi olarak uygulanması gerekir.


C ++ 'da İşleç Aşırı Yüklemesinin Üç Temel Kuralı ile devam edin .


~ikili önek değil, tekli önektir.
mrkj

1
.*yüklenemeyen operatörler listesinde eksik.
celticminstrel

1
@Mateen Bunun özel bir operatörle ilgili olmadığını, ancak hepsine uygulandığını netleştirmek için gerçek bir operatör yerine bir yer tutucu kullanmak istedim . Ve, bir C ++ programcısı olmak istiyorsanız, küçük baskıya bile dikkat etmeyi öğrenmelisiniz. :)
sbi

1
@HR: Bu kılavuzu okumuş olsaydın, neyin yanlış olduğunu bilirdin. Genel olarak, soruyla bağlantılı ilk üç cevabı okumanızı tavsiye ederim. Bu hayatınızın yarım saatinden fazla olmamalı ve size temel bir anlayış kazandırmalıdır. Daha sonra bakabileceğiniz operatöre özgü sözdizimi. Özel sorununuz, operator+()üye işlevi olarak aşırı yüklemeyi denemenizi , ancak ona serbest bir işlevin imzasını vermenizi önerir . Buraya bakın .
sbi

1
@sbi: İlk üç yazıyı çoktan okudum ve yaptığınız için teşekkür ederim. :) Sorunu çözmeye çalışacağım, aksi takdirde ayrı bir soruya sormanın daha iyi olduğunu düşünüyorum. Hayatı bizim için kolaylaştırdığımız için tekrar teşekkür ederiz! : D
Hosein Rahnama

251

Üye ve Üye Olmayanlar Arasındaki Karar

İkili işleçler =(atama), [](dizi aboneliği), ->(üye erişimi) ve n-ary ()(işlev çağrısı) operatörü her zaman üye işlevleri olarak uygulanmalıdır , çünkü dilin sözdizimi bunları gerektirir.

Diğer operatörler üye veya üye olmayan olarak uygulanabilir. Bununla birlikte, bazılarının genellikle üye olmayan işlevler olarak uygulanması gerekir, çünkü sol işlenenleri sizin tarafınızdan değiştirilemez. Bunlardan en önemlisi, giriş ve çıkış işleçleridir <<ve >>sol işlenenleri değiştiremeyeceğiniz standart kitaplıktan gelen akış sınıflarıdır.

Bunları bir üye işlevi veya üye olmayan bir işlev olarak uygulamayı seçmeniz gereken tüm işleçler için, karar vermek için aşağıdaki temel kuralları kullanın :

  1. Bir ise tekli operatör , bir şekilde bunu uygulamaya üye fonksiyonu.
  2. İkili bir operatör her iki işlenene de eşit davranırsa (onları değiştirmeden bırakır), bu operatörü üye olmayan olarak uygulayın işlev olarak uygulayın.
  3. İkili operatör yoksa değil işlenenlerinden hem tedavi eşit (genellikle sol işlenen değişecek), bunu bir hale getirmek için yararlı olabilecek üyesi İşlenen özel kısımlarına erişmek için varsa, sol işlenen en türünün işleyişini.

Tabii ki, tüm temel kurallarda olduğu gibi, istisnalar vardır. Bir türün varsa

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

ve bunun için arttırma ve azaltma işleçlerini aşırı yüklemek isterseniz, bunu bir üye işlevi olarak yapamazsınız, çünkü C ++ 'da enum türlerinde üye işlevleri olamaz. Bu yüzden onu özgür bir işlev olarak aşırı yüklemelisiniz. Veoperator<() sınıf şablonu içinde yuvalanmış bir sınıf şablonu için sınıf tanımında bir üye işlevi satır içi olarak yapıldığında yazmak ve okumak çok daha kolaydır. Ancak bunlar gerçekten nadir istisnalardır.

(Bununla birlikte, eğer bir istisna yapmak, konusunu unutma constüye fonksiyonları için, örtülü olur, o işlem gören için -lık thisargüman. Üye olmayan bir fonksiyonu olarak operatör bir olarak en soldaki argüman alacaktı Eğer constreferans bir üye fonksiyonu olarak aynı operatör olması gerekir constyapmak için sonunda *thisbir constreferans).


Aşırı yükleme için Ortak operatörlere devam edin .


9
Herb Sutter'in Etkili C ++ 'daki öğesi (ya da C ++ Kodlama Standartları mı?), Sınıfın kapsüllenmesini artırmak için üye olmayan arkadaş işlevlerini üye işlevlerine tercih etmesi gerektiğini söylüyor. IMHO, kapsülleme sebebi temel kuralınıza göre önceliklidir, ancak temel kuralınızın kalite değerini düşürmez.
paercebal

8
@paercebal: Etkili C ++ Meyers, C ++ Kodlama Standartları Sutter'dir . Hangisine atıfta bulunuyorsunuz? Her neyse, diyelim ki operator+=()üye olmama fikrinden hoşlanmıyorum . Sol taraftaki işleneni değiştirmek zorundadır, bu yüzden tanım gereği içlerine derinlemesine kazmak zorundadır. Üye olmamakla ne kazanırdınız?
sbi

9
@sbi: C ++ Kodlama Standartlarında Öğe 44 (Sutter) Üye olmayan arkadaş olmayan işlevler yazmayı tercih edin , elbette, bu işlevi yalnızca sınıfın genel arabirimini kullanarak yazabiliyorsanız geçerlidir. Yapamazsanız (veya yapamazsanız, ancak performansı kötü bir şekilde engelleyecektir), o zaman üye veya arkadaş yapmalısınız.
Matthieu M.

3
@sbi: Hata! Etkili, Olağanüstü ... İsimleri karıştırmam şaşırtıcı değil. Her neyse, kazanç, bir nesneye özel / korumalı verilere erişimi olan işlevlerin sayısını mümkün olduğunca sınırlamaktır. Bu şekilde, sınıfınızın kapsüllenmesini artırarak bakım / test / evrimini kolaylaştırırsınız.
paercebal

12
@sbi: Bir örnek. Diyelim ki hem String hem operator +=de appendyöntemlerle bir String sınıfı kodluyorsunuz. Bu appendyöntem daha eksiksizdir, çünkü indeks i'den indeks n -1'e parametrenin bir alt dizesini ekleyebilirsiniz: ve ile çağrı eklemenin append(string, start, end)mantıklı görünmesi . O anda, append bir üye yöntemi olabilir, ancak bir üye olması gerekmez ve onu bir üye olmayan yapmak String innards ile oynayan kod miktarını azaltacaktır, bu yüzden iyi bir şey .... ^ _ ^ ...+=start = 0end = string.sizeoperator +=
paercebal

165

Dönüşüm Operatörleri (Kullanıcı Tanımlı Dönüşümler olarak da bilinir)

C ++ 'da dönüştürme işleçleri, derleyicinin türleriniz ve diğer tanımlanmış türler arasında dönüştürme yapmasına izin veren işleçler oluşturabilirsiniz. Örtük ve açık olan iki tür dönüşüm operatörü vardır.

Örtülü Dönüştürme İşleçleri (C ++ 98 / C ++ 03 ve C ++ 11)

Örtük bir dönüşüm operatörü, derleyicinin örtük olarak dönüştürmesine izin verir ( intvelong kullanıcı tanımlı bir türün değerini başka bir türe ) sağlar.

Aşağıdaki, örtük bir dönüştürme işlecine sahip basit bir sınıftır:

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

Örtülü dönüşüm işleçleri, tek bağımsız değişken kurucuları gibi, kullanıcı tanımlı dönüşümlerdir. Derleyiciler, bir çağrıyı aşırı yüklenmiş bir işlevle eşleştirmeye çalışırken kullanıcı tanımlı bir dönüşüm verir.

void f(const char*);

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

İlk başta bu çok yararlı görünüyor, ancak bununla ilgili sorun, örtük dönüşümün beklenmediğinde bile devreye girmesidir. Aşağıdaki kodda, bir lvalue olmadığı void f(const char*)için çağrılır , bu nedenle ilk eşleşmez:my_string()

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

f(my_string());

Yeni başlayanlar bunu kolayca yanlış yaparlar ve hatta deneyimli C ++ programcıları bazen şaşırır, çünkü derleyici şüphelenmedikleri bir aşırı yükü seçer. Bu sorunlar, açık dönüşüm operatörleri tarafından azaltılabilir.

Açık Dönüşüm Operatörleri (C ++ 11)

Örtük dönüşüm operatörlerinden farklı olarak, açık dönüşüm operatörleri beklemediğinizde asla devreye girmez. Aşağıdaki, açık bir dönüştürme operatörü olan basit bir sınıftır:

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

Dikkat edin explicit . Şimdi, örtük dönüştürme işleçlerinden beklenmeyen kodu yürütmeye çalıştığınızda, bir derleyici hatası alırsınız:

prog.cpp: 'int main ()' işlevinde:
prog.cpp: 15: 18: hata: 'f (my_string)' çağrısı için eşleşen işlev yok
prog.cpp: 15: 18: not: adaylar:
prog.cpp: 11: 10: not: void f (my_string &)
prog.cpp: 11: 10: not: 'my_string' den 'my_string &' e argüman 1 için bilinen bir dönüşüm yok
prog.cpp: 12: 10: not: void f (const char *)
prog.cpp: 12: 10: not: 'my_string' den 'const char *' a argüman 1 için bilinen bir dönüşüm yok

Açık döküm operatörünü çağırmak için static_castbir C stili döküm veya bir yapıcı stili döküm (ie T(value)) kullanmanız gerekir.

Ancak, bunun bir istisnası vardır: Derleyici örtük olarak dönüştürmek için izin verilir bool. Buna ek olarak, derleyiciye dönüştükten sonra başka bir örtük dönüşüm boolyapmasına izin verilmez (bir derleyicinin bir seferde 2 örtük dönüşüm gerçekleştirmesine izin verilir, ancak maksimumda yalnızca 1 kullanıcı tanımlı dönüşüm).

Derleyici "geçmiş" atamayacağındanbool , açık dönüştürme işleçleri artık Güvenli Bool deyimine olan ihtiyacı ortadan kaldırmaktadır . Örneğin, C ++ 11'den önceki akıllı işaretçiler, integral türlerine dönüştürmeleri önlemek için Güvenli Bool deyimini kullandı. C ++ 11'de, akıllı işaretçiler bunun yerine açık bir işleç kullanır, çünkü derleyicinin bir türü açıkça boole dönüştürdükten sonra örtük olarak bir integral türüne dönüştürülmesine izin verilmez.

Aşırınewdelete Yüklemeye devam edin ve .


148

Aşırı yükleme newvedelete

Not: ile Bu yalnızca fırsatlar sözdizimi aşırı yüklemenewvedeletedeğil ile uygulanması bu tür aşırı operatörlerin. Aşırı yükleme semantiğininnew ve deletekendi SSS'sini hak ettiğini düşünüyorum , operatör aşırı yüklenmesi konusunda asla adalet yapamam.

temeller

C ++ 'da, bu ifade değerlendirildiğinde iki şey gibi yeni bir ifadenew T(arg) yazdığınızda: Önce operator newham bellek elde etmek için çağrılır ve sonra Tbu ham belleği geçerli bir nesneye dönüştürmek için uygun yapıcısı çağrılır. Benzer şekilde, bir nesneyi sildiğinizde, önce onun yıkıcısı çağrılır ve daha sonra bellek geri döndürülür operator delete.
C ++ bu işlemlerin her ikisini de ayarlamanızı sağlar: bellek yönetimi ve ayrılan bellekteki nesnenin yapımı / imhası. İkincisi, bir sınıf için yapıcılar ve yıkıcılar yazarak yapılır. Hafıza yönetiminin ince ayarını yapmak kendi operator newve operator delete.

Operatör aşırı yüklenmesinin temel kurallarından ilki - yapma, özellikle aşırı yükleme newve için geçerlidir delete. Bu operatörleri aşırı yüklemenin neredeyse tek nedeni performans sorunları ve bellek kısıtlamalarıdır ve çoğu durumda, kullanılan algoritmalarda yapılan değişiklikler gibi diğer eylemler, bellek yönetimini değiştirmeye çalışmaktan çok daha yüksek maliyet / kazanç oranı sağlayacaktır.

C ++ standart kütüphanesi bir dizi önceden tanımlanmış newve deleteoperatörle birlikte gelir . En önemlileri şunlardır:

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(); 

İlk ikisi bir nesne için bellek ayırır / ayırır, ikincisi bir nesne dizisi için bellek ayırır. Bunların kendi sürümlerini sağlarsanız, aşırı yüklenmezler, ancak standart kütüphaneden olanları değiştirirler .
Eğer aşırı yüklerseniz operator new, operator deleteasla aramayı düşünmeseniz bile eşleşmeyi her zaman aşırı yüklemelisiniz . Bunun nedeni, bir kurucu yeni bir ifadenin değerlendirilmesi sırasında atarsa, çalışma zamanı sistemi , nesneyi oluşturmak için belleği ayırmak için çağrılan operator deleteeşleşmeye geri döndürür operator new. Eşleştirme sağlamazsanız operator delete, varsayılan olan çağrılır, bu neredeyse her zaman yanlıştır.
Eğer aşırı olursa newve deletesen de varyantları dizi aşırı düşünmelisiniz.

Yerleştirme new

C ++ yeni ve silme operatörlerinin ek argümanlar almasına izin verir.
Yerleşim yeni olarak adlandırılan, belirli bir adreste iletilen bir nesne oluşturmanıza olanak tanır:

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

Standart kütüphane, bunun için yeni ve silme operatörlerinin uygun aşırı yüklenmeleri ile birlikte gelir:

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(); 

Örnek yerleştirmede, yukarıda verilen yeni yerleşim kodunda operator delete, X'in kurucusu bir istisna atmadıkça asla çağrılmaz.

Ayrıca aşırı yükleyebilir newve deletediğer argümanlarla. Yeni yerleşim için ek argümanda olduğu gibi, bu argümanlar anahtar kelimeden sonra parantez içinde de listelenir new. Yalnızca tarihsel nedenlerden dolayı, argümanları belirli bir adrese bir nesne yerleştirmek için olmasa bile, bu tür değişkenlere genellikle yerleşim yeni denir.

Sınıfa özgü yeni ve sil

Çoğu zaman bellek yönetimine ince ayar yapmak isteyeceksiniz, çünkü ölçüm belirli bir sınıfın veya bir grup ilgili sınıfın örneklerinin sık sık yaratıldığını ve yok edildiğini ve çalışma zamanı sisteminin varsayılan bellek yönetiminin genel performans, bu özel durumda verimsiz bir şekilde ilgilenir. Bunu geliştirmek için, yeni bir yükleme yapabilir ve belirli bir sınıf için silebilirsiniz:

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);
    // ... 
}; 

Böylece aşırı yüklenmiş, yeni ve silme statik üye işlevleri gibi davranır. Nesnesi my_classiçin std::size_targüman her zaman olacaktır sizeof(my_class). Bununla birlikte, bu operatörler türetilmiş sınıfların dinamik olarak tahsis edilmiş nesneleri için de çağrılır , bu durumda bundan daha büyük olabilir.

Global yeni ve sil

Global yeniyi aşırı yüklemek ve silmek için, standart kütüphanenin önceden tanımlanmış operatörlerini kendimizle değiştirin. Ancak, bunun nadiren yapılması gerekir.


11
Ayrıca, global operatörün yeni ve sil değiştirilmesinin genellikle performans için olduğunu kabul etmiyorum: aksine, genellikle hata izleme içindir.
Yttrill

1
Ayrıca, aşırı yüklenmiş yeni bir operatör kullanıyorsanız, eşleşen bağımsız değişkenlere sahip bir silme operatörü de sağlamanız gerektiğini unutmayın. Global new / delete bölümünde çok fazla ilgi çekici olmayan bir yerde olduğunu söylüyorsunuz.
Yttrill

13
@Bir şeyler karıştırıyorsun. Anlam yüklenmiş olur. "Operatör aşırı yüklemesi" ne demek, anlamın aşırı yüklenmiş olmasıdır. Bu, kelimenin tam anlamıyla işlevlerin aşırı yüklendiği anlamına gelmez ve özellikle yeni operatör Standart'ın sürümünü aşırı yüklemez . @sbi aksini iddia etmiyor. "Aşırı yükleme ek operatörü" demek yaygın olarak "aşırı yükleme" olarak adlandırmak yaygındır.
Johannes Schaub - litb

1
@sbi: Bkz. (veya daha iyisi, bağlantı) gotw.ca/publications/mill15.htm . Bazen nothrowyeni kullanan insanlara yönelik iyi bir uygulamadır .
Alexandre

1
"Eşleşen bir operatör silme özelliği sağlamazsanız, varsayılan olana" -> Aslında, herhangi bir bağımsız değişken ekler ve eşleşen bir silme oluşturmazsanız, hiçbir operatör silme işlemi çağrılmaz ve bellek sızıntısı oluşur. (15.2.2, nesnenin kapladığı depolama alanı yalnızca uygun bir ... operatör silme özelliği bulunduğunda
dağıtılır

46

operator<<Nesneleri std::coutbir dosyaya ya da bir dosyaya aktarmak için neden bir işlev işlevi olamaz ?

Diyelim ki:

struct Foo
{
   int a;
   double b;

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

Bu göz önüne alındığında, şunları kullanamazsınız:

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

Bu yana operator<<bir üyesi fonksiyonu olarak aşırı Foo, operatörün LHS a olmalıdır Foonesne. Bu, aşağıdakileri kullanmanız gerekeceği anlamına gelir:

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

ki bu çok sezgisel değil.

Eğer üye olmayan bir işlev olarak tanımlarsanız,

struct Foo
{
   int a;
   double b;
};

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

Şunları kullanabileceksiniz:

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

ki bu çok sezgisel.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.