Kopya oluşturucuları ne zaman kullanmalıyız?


87

C ++ derleyicisinin bir sınıf için bir kopya yapıcısı oluşturduğunu biliyorum. Hangi durumda kullanıcı tanımlı bir kopya oluşturucu yazmamız gerekir? Bazı örnekler verebilir misiniz?



1
Kendi copy-ctor'unuzu yazmanız gereken durumlardan biri: Derin kopya yapmanız gerektiğinde. Ayrıca, bir ctor oluşturur oluşturmaz, sizin için oluşturulmuş varsayılan bir ctor olmadığını unutmayın (varsayılan anahtar kelimeyi kullanmadığınız sürece).
harshvchawla

Yanıtlar:


75

Derleyici tarafından oluşturulan kopya oluşturucu, üye bazında kopyalama yapar. Bazen bu yeterli değildir. Örneğin:

class Class {
public:
    Class( const char* str );
    ~Class();
private:
    char* stored;
};

Class::Class( const char* str )
{
    stored = new char[srtlen( str ) + 1 ];
    strcpy( stored, str );
}

Class::~Class()
{
    delete[] stored;
}

bu durumda üyenin üye bazında kopyalanması storedtamponu çoğaltmayacaktır (sadece işaretçi kopyalanacaktır), bu nedenle yok edilecek ilk kopya tamponu paylaşarak delete[]başarılı bir şekilde çağıracak ve ikincisi tanımlanmamış davranışla çalışacaktır. Derin kopyalama kopyalama yapıcısına (ve ayrıca atama operatörüne) ihtiyacınız var.

Class::Class( const Class& another )
{
    stored = new char[strlen(another.stored) + 1];
    strcpy( stored, another.stored );
}

void Class::operator = ( const Class& another )
{
    char* temp = new char[strlen(another.stored) + 1];
    strcpy( temp, another.stored);
    delete[] stored;
    stored = temp;
}

10
Bit bazında değil, özellikle sınıf tipi üyeler için kopya-ctor'u çağıran üye bazlı kopyalama gerçekleştirir.
Georg Fritzsche

7
Değerlendirme operatörünü böyle yazmayın. Güvenli bir istisna değil. (eğer yeni bir istisna atarsa, nesne tanımlanmamış bir durumda bırakılır ve deponun hafızanın ayrılmış bir kısmına işaret etmesi (hafızayı YALNIZCA atabilen tüm işlemler başarılı bir şekilde tamamlandıktan sonra serbest bırakın)). Basit bir çözüm, kopya takas deyimini kullanmaktır.
Martin York

@sharptooth, sahip olduğunuz alttan 3. satır delete stored[];ve bence olması gerektiğine inanıyorumdelete [] stored;
Peter Ajtai

4
Bunun sadece bir örnek olduğunu biliyorum, ancak daha iyi çözümün kullanmak olduğunu belirtmelisiniz std::string. Genel fikir, yalnızca kaynakları yöneten yardımcı sınıfların Büyük Üçü aşırı yüklemesi gerektiğidir ve diğer tüm sınıfların, Büyük Üçten herhangi birini tanımlama ihtiyacını ortadan kaldırarak yalnızca bu yardımcı sınıfları kullanması gerektiğidir.
GManNickG

2
@Martin: Taşa oyulduğundan emin olmak istedim. : P
GManNickG

46

Kuralına Rule of Fiveatıfta bulunulmaması beni biraz sinirlendirdi .

Bu kural çok basit:

Beş Kuralı :
Destructor, Copy Constructor, Copy Assignment Operator, Move Constructor veya Move Assignment Operator'dan birini yazdığınızda, muhtemelen diğer dördünü de yazmanız gerekir.

Ancak, istisnai durum korumalı kod yazma ihtiyacından kaynaklanan, izlemeniz gereken daha genel bir kılavuz var:

Her kaynak özel bir nesne tarafından yönetilmelidir

Buradaki @sharptoothkod hala (çoğunlukla) iyidir, ancak sınıfına ikinci bir nitelik ekleseydi bu olmazdı. Aşağıdaki sınıfı düşünün:

class Erroneous
{
public:
  Erroneous();
  // ... others
private:
  Foo* mFoo;
  Bar* mBar;
};

Erroneous::Erroneous(): mFoo(new Foo()), mBar(new Bar()) {}

new BarAtarsa ne olur ? İle gösterilen nesneyi nasıl silersiniz mFoo? Çözümler var (işlev seviyesi dene / yakala ...), sadece ölçeklenmiyorlar.

Durumla başa çıkmanın doğru yolu, ham işaretçiler yerine uygun sınıfları kullanmaktır.

class Righteous
{
public:
private:
  std::unique_ptr<Foo> mFoo;
  std::unique_ptr<Bar> mBar;
};

Aynı yapıcı uygulamasıyla (veya aslında kullanarak make_unique), artık ücretsiz olarak istisna güvenliğine sahibim !!! Heyecanlı değil mi? Ve en iyisi, artık uygun bir yıkıcı için endişelenmeme gerek yok! Ben kendi yazmaya gerek yok Copy Constructorve Assignment Operatorçünkü olsa unique_ptrbu işlemleri tanımlamaz ... ama mesele burada değil;)

Ve bu nedenle, sharptooths sınıfı yeniden ziyaret edildi:

class Class
{
public:
  Class(char const* str): mData(str) {}
private:
  std::string mData;
};

Seni bilmiyorum ama benimkini daha kolay buluyorum;)


C ++ 11 için - Oluşturucuyu Taşı ve Atamayı Taşı Operatörünü üç kuralına ekleyen beş kuralı.
Robert Andrzejuk

1
@Robb: Son örnekte gösterildiği gibi, aslında genellikle Sıfır Kuralını hedeflemeniz gerektiğini unutmayın . Yalnızca uzmanlaşmış (jenerik) teknik sınıflar bir kaynağı kullanmalı, diğer tüm sınıflar bu akıllı işaretçileri / kapsayıcıları kullanmalı ve bunun için endişelenmemelidir.
Matthieu M.

@MatthieuM. Katılıyorum :-) Beş Kuralından bahsettim, çünkü bu cevap C ++ 11'den önce ve "Büyük Üç" ile başlıyor, ancak şimdi "Büyük Beş" in alakalı olduğu belirtilmelidir. Sorulan bağlamda doğru olduğu için bu yanıta olumsuz oy vermek istemiyorum.
Robert Andrzejuk

@Robb: İyi nokta, cevabı Büyük Üç yerine Beş Kuralından bahsedecek şekilde güncelledim. Umarım çoğu insan şimdiye kadar C ++ 11 uyumlu derleyicilere geçmiştir (ve hala sahip olmayanlara üzülüyorum).
Matthieu M.

31

Pratiğimden hatırlayabilirim ve kopya oluşturucuyu açıkça bildirmek / tanımlamakla uğraşmak zorunda olduğum aşağıdaki durumları düşünebilirim. Vakaları iki kategoriye ayırdım

  • Doğruluk / Anlambilim - Kullanıcı tanımlı bir kopya yapıcı sağlamazsanız, bu türü kullanan programlar derlenemeyebilir veya yanlış çalışabilir.
  • Optimizasyon - derleyici tarafından oluşturulan kopya yapıcısına iyi bir alternatif sağlamak, programı daha hızlı hale getirmeye olanak tanır.


Doğruluk / Anlambilim

Bu bölüme, bu türü kullanan programların doğru çalışması için kopya oluşturucunun bildirilmesi / tanımlanmasının gerekli olduğu durumları yerleştiriyorum.

Bu bölümü okuduktan sonra, derleyicinin kopya oluşturucuyu kendi başına oluşturmasına izin vermenin birkaç tuzağını öğreneceksiniz. Bu nedenle, sanıldığı gibi onun belirtildiği cevap , yeni bir sınıf için copyability kapatın ve her zaman güvenlidir kasten gerçekten gerektiğinde daha sonra etkinleştirmek.

C ++ 03'te bir sınıfı kopyalanamaz hale getirme

Özel bir kopya-yapıcı bildirin ve bunun için bir uygulama sağlamayın (böylece, bu türdeki nesneler sınıfın kendi kapsamında veya arkadaşları tarafından kopyalanmış olsa bile derleme bağlama aşamasında başarısız olur).

C ++ 11 veya daha yeni bir sınıfta kopyalanamaz hale getirme

Copy-constructor'ı =deletesonunda ile bildirin .


Sığ ve Derin Kopya

Bu en iyi anlaşılan durumdur ve aslında diğer cevaplarda bahsedilen tek durumdur. shaprtooth etti kaplı oldukça iyi. Yalnızca nesneye özel olarak sahip olunması gereken derin kopyalama kaynaklarının, dinamik olarak ayrılan belleğin yalnızca bir türü olduğu her tür kaynak için geçerli olabileceğini eklemek istiyorum. Gerekirse, bir nesneyi derinlemesine kopyalamak da gerekli olabilir

  • diske geçici dosyaları kopyalamak
  • ayrı bir ağ bağlantısı açmak
  • ayrı bir çalışan iş parçacığı oluşturmak
  • ayrı bir OpenGL çerçeve arabelleği ayırma
  • vb

Kendinden kayıt yapan nesneler

Tüm nesnelerin - nasıl inşa edildikleri önemli değil - bir şekilde kaydedilmesi ZORUNLU olduğu bir sınıf düşünün. Bazı örnekler:

  • En basit örnek: şu anda var olan nesnelerin toplam sayısını korumak. Nesne kaydı, statik sayacı artırmakla ilgilidir.

  • Daha karmaşık bir örnek, bu türdeki tüm mevcut nesnelere yapılan referansların depolandığı (böylece bildirimlerin hepsine iletilebilmesi için) tek bir kayıt defterine sahip olmaktır.

  • Referans sayılan akıllı işaretçiler bu kategoride yalnızca özel bir durum olarak kabul edilebilir: yeni işaretçi, kendisini genel bir kayıt defteri yerine paylaşılan kaynakla "kaydeder".

Böyle bir kendi kendine kayıt işlemi, türün HERHANGİ bir kurucusu tarafından gerçekleştirilmelidir ve kopya oluşturucu bir istisna değildir.


Dahili çapraz referanslara sahip nesneler

Bazı nesneler, farklı alt nesneleri arasında doğrudan çapraz referanslara sahip önemsiz olmayan bir iç yapıya sahip olabilir (aslında, bu durumu tetiklemek için böyle bir dahili çapraz referans yeterlidir). Derleyici tarafından sağlanan kopya yapıcısı, dahili nesne içi ilişkileri keserek bunları nesneler arası ilişkilere dönüştürür .

Bir örnek:

struct MarriedMan;
struct MarriedWoman;

struct MarriedMan {
    // ...
    MarriedWoman* wife;   // association
};

struct MarriedWoman {
    // ...
    MarriedMan* husband;  // association
};

struct MarriedCouple {
    MarriedWoman wife;    // aggregation
    MarriedMan   husband; // aggregation

    MarriedCouple() {
        wife.husband = &husband;
        husband.wife = &wife;
    }
};

MarriedCouple couple1; // couple1.wife and couple1.husband are spouses

MarriedCouple couple2(couple1);
// Are couple2.wife and couple2.husband indeed spouses?
// Why does couple2.wife say that she is married to couple1.husband?
// Why does couple2.husband say that he is married to couple1.wife?

Yalnızca belirli kriterleri karşılayan nesnelerin kopyalanmasına izin verilir

Nesnelerin bazı durumlarda kopyalanmasının güvenli olduğu (örneğin, varsayılan-oluşturulmuş durumda) ve başka türlü kopyalanmasının güvenli olmadığı sınıflar olabilir . Kopyalanması güvenli nesnelerin kopyalanmasına izin vermek istiyorsak, o zaman - savunmaya yönelik programlama yapılıyorsa - kullanıcı tanımlı kopya yapıcısında çalışma zamanı kontrolüne ihtiyacımız var.


Kopyalanamayan alt nesneler

Bazen kopyalanabilir olması gereken bir sınıf, kopyalanamayan alt nesneleri toplar. Genellikle bu, gözlemlenemeyen duruma sahip nesneler için olur (bu durum, aşağıdaki "Optimizasyon" bölümünde daha ayrıntılı olarak tartışılmaktadır). Derleyici yalnızca bu durumu tanımaya yardımcı olur.


Yarı kopyalanabilir alt nesneler

Kopyalanabilir olması gereken bir sınıf, kopyalanabilir türden bir alt nesneyi toplayabilir. Yarı kopyalanabilir bir tür, tam anlamıyla bir kopya kurucu sağlamaz, ancak nesnenin kavramsal bir kopyasını oluşturmaya izin veren başka bir kurucuya sahiptir. Bir türü yarı kopyalanabilir hale getirmenin nedeni, türün kopyalama semantiği hakkında tam bir anlaşma olmamasıdır.

Örneğin, nesnenin kendi kendine kayıt durumunu tekrar gözden geçirerek, bir nesnenin yalnızca tamamen bağımsız bir nesne olması durumunda küresel nesne yöneticisine kaydedilmesi gereken durumlar olabileceğini iddia edebiliriz. Başka bir nesnenin alt nesnesiyse, onu yönetmenin sorumluluğu, kapsayıcı nesnesindedir.

Ya da hem sığ hem de derin kopyalama desteklenmelidir (hiçbiri varsayılan değildir).

Daha sonra nihai karar bu türdeki kullanıcılara bırakılır - nesneleri kopyalarken, amaçlanan kopyalama yöntemini (ek argümanlar aracılığıyla) açıkça belirtmeleri gerekir.

Programlamaya savunmasız bir yaklaşım olması durumunda, hem normal bir kopya-yapıcı hem de bir yarı-kopya-kurucunun mevcut olması da mümkündür. Nadir ancak iyi anlaşılmış durumlarda alternatif kopyalama yöntemlerinin kullanılması gerektiğinde, çoğu durumda tek bir kopyalama yönteminin uygulanması gerektiğinde bu haklı görülebilir. Bu durumda derleyici, kopya yapıcısını örtük olarak tanımlayamadığından şikayet etmez; bu türden bir alt nesnenin bir yarı-kopya-kurucu aracılığıyla kopyalanması gerekip gerekmediğini hatırlamak ve kontrol etmek kullanıcının sorumluluğunda olacaktır.


Nesnenin kimliğiyle güçlü bir şekilde ilişkilendirilen durumu kopyalamayın

Nadir durumlarda, nesnenin gözlemlenebilir durumunun bir alt kümesi, nesnenin kimliğinin ayrılmaz bir parçasını oluşturabilir (veya kabul edilebilir) ve diğer nesnelere aktarılmamalıdır (bu biraz tartışmalı olsa da).

Örnekler:

  • Nesnenin UID'si (ancak bu aynı zamanda yukarıdan "kendi kendine kayıt" durumuna da aittir, çünkü kimlik bir kendi kendine kayıt işleminde elde edilmelidir).

  • Yeni nesnenin kaynak nesnenin geçmişini devralmaması, bunun yerine " <OTHER_OBJECT_ID> ' den <TIME> tarihinde kopyalandı " tek bir geçmiş öğesiyle başlaması gerektiğinde nesnenin geçmişi (örneğin Geri Al / Yinele yığını) .

Bu gibi durumlarda, kopya yapıcısının ilgili alt nesneleri kopyalamayı atlaması gerekir.


Kopya oluşturucunun doğru imzasını zorunlu kılma

Derleyici tarafından sağlanan kopya oluşturucunun imzası, alt nesneler için hangi kopya yapıcılarının kullanılabilir olduğuna bağlıdır. En az bir alt nesnenin gerçek bir kopya yapıcısı yoksa (kaynak nesneyi sabit referansla alarak), bunun yerine değişen bir kopya kurucusuna sahipse (kaynak nesneyi sabit olmayan referansla alarak), derleyicinin seçeneği olmayacaktır. ancak örtük olarak bildirmek ve sonra değişen bir kopya yapıcıyı tanımlamak için.

Şimdi, ya alt nesnenin türünün "değişen" kopya yapıcısı, kaynak nesneyi gerçekten değiştirmiyorsa (ve constanahtar kelimeyi bilmeyen bir programcı tarafından yazılmışsa )? Eksik constolanı ekleyerek bu kodu düzeltemiyorsak, diğer seçenek, kendi kullanıcı tanımlı kopya oluşturucumuzu doğru bir imzayla bildirmek ve bir const_cast.


Yazma üzerine kopyalama (COW)

Dahili verilerine doğrudan referanslar veren bir COW kabı, yapım sırasında derin kopyalanmalıdır, aksi takdirde bir referans sayma tutamacı gibi davranabilir.

COW bir optimizasyon tekniği olmasına rağmen, kopyalama yapıcısındaki bu mantık, doğru uygulaması için çok önemlidir. Bu nedenle, bu durumu, daha sonra gideceğimiz "Optimizasyon" bölümü yerine buraya yerleştirdim.



Optimizasyon

Aşağıdaki durumlarda, optimizasyon kaygıları nedeniyle kendi kopya oluşturucunuzu tanımlamak isteyebilir / buna ihtiyaç duyabilirsiniz:


Kopyalama sırasında yapı optimizasyonu

Öğe kaldırma işlemlerini destekleyen bir kap düşünün, ancak bunu yalnızca kaldırılan öğeyi silinmiş olarak işaretleyerek yapabilir ve daha sonra yuvasını geri dönüştürebilir. Böyle bir kabın bir kopyası yapıldığında, "silinmiş" yuvaları olduğu gibi korumak yerine hayatta kalan verileri sıkıştırmak mantıklı olabilir.


Gözlemlenemeyen durumu kopyalamayı atla

Bir nesne, gözlemlenebilir durumunun parçası olmayan veriler içerebilir. Genellikle, bu, nesne tarafından gerçekleştirilen belirli yavaş sorgu işlemlerini hızlandırmak için nesnenin ömrü boyunca toplanan önbelleğe alınmış / belleğe alınmış verilerdir. İlgili işlemler gerçekleştirildiğinde (ve eğer!) Yeniden hesaplanacağından, bu verilerin kopyalanmasını atlamak güvenlidir. Bu verilerin kopyalanması gerekçesiz olabilir, çünkü nesnenin gözlemlenebilir durumu (önbelleğe alınan verinin türetildiği) mutasyon işlemleriyle değiştirilirse (ve nesneyi değiştirmeyeceksek, neden derin bir veri oluşturuyoruz) o zaman kopyala?)

Bu optimizasyon, yalnızca yardımcı veriler, gözlemlenebilir durumu temsil eden verilere kıyasla büyükse gerekçelendirilir.


Örtülü kopyalamayı devre dışı bırakın

C ++, kopya yapıcısını bildirerek örtük kopyalamayı devre dışı bırakmaya izin verir explicit. Daha sonra bu sınıfın nesneleri işlevlere aktarılamaz ve / veya işlevlerden değere göre döndürülemez. Bu numara, hafif gibi görünen ancak kopyalanması gerçekten çok pahalı olan bir tür için kullanılabilir (yine de, kopyalanabilir hale getirmek daha iyi bir seçim olabilir).

C ++ 03'te bir kopya oluşturucunun bildirilmesi, onu da tanımlamayı gerektiriyordu (tabii ki, kullanmayı planlıyorsanız). Bu nedenle, böyle bir kopya oluşturucuya başvurmak, yalnızca tartışılan endişe dışında, derleyicinin sizin için otomatik olarak oluşturacağı aynı kodu yazmanız gerektiği anlamına gelir.

C ++ 11 ve daha yeni standartlar, özel üye işlevlerinin (varsayılan ve kopya oluşturucular, kopya atama operatörü ve yıkıcı) , varsayılan uygulamayı kullanmak için açık bir istekle (bildirimi ile sonlandırın =default) bildirilmesine izin verir.



YAPILACAKLAR

Bu cevap aşağıdaki şekilde geliştirilebilir:

  • Daha fazla örnek kod ekleyin
  • "Dahili çapraz referanslara sahip nesneler" durumunu gösterin
  • Bazı bağlantılar ekleyin

6

Dinamik olarak ayrılmış içeriğe sahip bir sınıfınız varsa. Örneğin, bir kitabın başlığını karakter * olarak kaydedersiniz ve başlığı yeniyle ayarlarsınız, kopya çalışmaz.

title = new char[length+1]Ve sonra bunu yapan bir kopya oluşturucu yazmanız gerekir strcpy(title, titleIn). Kopya oluşturucu yalnızca "sığ" bir kopya yapacaktır.


2

Copy Constructor, bir nesne değere göre iletildiğinde, değere göre döndürüldüğünde veya açıkça kopyalandığında çağrılır. Kopya yapıcısı yoksa, c ++ yüzeysel bir kopya oluşturan varsayılan bir kopya yapıcısı oluşturur. Nesnenin dinamik olarak ayrılmış belleğe işaretçisi yoksa, yüzeysel kopyalama yapılacaktır.


0

Sınıfın özellikle ihtiyacı olmadığı sürece copy ctor ve operator = işlevini devre dışı bırakmak genellikle iyi bir fikirdir. Bu, referans amaçlandığında bir argümanı değere göre geçirmek gibi verimsizlikleri önleyebilir. Ayrıca derleyici tarafından üretilen yöntemler geçersiz olabilir.


-1

Aşağıdaki kod parçacığını ele alalım:

class base{
    int a, *p;
public:
    base(){
        p = new int;
    }
    void SetData(int, int);
    void ShowData();
    base(const base& old_ref){
        //No coding present.
    }
};
void base :: ShowData(){
    cout<<this->a<<" "<<*(this->p)<<endl;
}
void base :: SetData(int a, int b){
    this->a = a;
    *(this->p) = b;
}
int main(void)
{
    base b1;
    b1.SetData(2, 3);
    b1.ShowData();
    base b2 = b1; //!! Copy constructor called.
    b2.ShowData();
    return 0;
}

Output: 
2 3 //b1.ShowData();
1996774332 1205913761 //b2.ShowData();

b2.ShowData();verileri açıkça kopyalamak için kod yazılmadan oluşturulmuş kullanıcı tanımlı bir kopya yapıcısı olduğundan gereksiz çıktı verir. Yani derleyici aynı şeyi yaratmaz.

Çoğunuz zaten biliyor olmasına rağmen, bu bilgiyi hepinizle paylaşmayı düşündüm.

Şerefe ... Mutlu kodlamalar !!!

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.