Atama operatörünü ve "if (this! = & Rhs)" öğesini taşıyın


126

Bir sınıfın atama operatöründe, genellikle atanan nesnenin çağıran nesne olup olmadığını kontrol etmeniz gerekir, böylece işleri batırmazsınız:

Class& Class::operator=(const Class& rhs) {
    if (this != &rhs) {
        // do the assignment
    }

    return *this;
}

Taşıma atama operatörü için aynı şeye ihtiyacınız var mı? Hiç this == &rhsdoğru olabilecek bir durum var mı ?

? Class::operator=(Class&& rhs) {
    ?
}

12
Sorulan Q ile alakasız ve sadece bu Q'yu zaman çizelgesinde okuyan yeni kullanıcıların (çünkü Seth'in bunu zaten bildiğini biliyorum) yanlış fikirlere kapılmasın diye, Kopyala ve Değiştir , Atama Operatörünü uygulamanın doğru yoludur. kendi kendine atama et-all için kontrol etmenize gerek yoktur.
Alok Save

5
@VaughnCato: A a; a = std::move(a);.
Xeo

11
@VaughnCato Kullanımı std::movenormaldir. Öyleyse, takma adı hesaba katın ve bir çağrı yığınının derinliklerinde olduğunuzda ve bir referansınız Tve başka bir referansınız olduğunda T... kimliği burada kontrol edecek misiniz? Aynı argümanı iki kez geçiremeyeceğinizi belgelemenin, bu iki referansın takma ad olmayacağını statik olarak kanıtlayacağı ilk çağrıyı (veya çağrıları) bulmak ister misiniz? Yoksa kendini atamayı sadece işe yarayacak mı?
Luc Danton

2
@LucDanton Atama operatöründe bir iddiayı tercih ederim. Eğer std :: move, bir rvalue kendi kendine atama ile sonuçlanabilecek şekilde kullanılmış olsaydı, düzeltilmesi gereken bir hata olarak düşünürdüm.
Vaughn Cato

4
@VaughnCato Kendi kendini değiştirmenin normal olduğu yerlerden biri ya std::sortda std::shuffle- bir dizinin iinci ve jinci elemanlarını i != jönce kontrol etmeden değiştirdiğinizde . ( std::swaptaşıma ataması açısından uygulanır.)
Quuxplusone

Yanıtlar:


143

Vay canına, burada temizlenecek çok şey var ...

İlk olarak, Kopyala ve Değiştir , Kopyalama Atamasını uygulamanın her zaman doğru yolu değildir. Neredeyse kesinlikle söz konusu olduğunda dumb_array, bu yetersiz bir çözümdür.

Kullanımı Kopyala ve Swap içindir dumb_arrayalt katmanında tam özellikleri ile en pahalı operasyonu koyarak klasik bir örneğidir. En iyi özelliği isteyen ve performans cezasını ödemeye istekli müşteriler için mükemmeldir. Tam olarak istediklerini alıyorlar.

Ancak, tam özelliğe ihtiyaç duymayan ve bunun yerine en yüksek performansı arayan müşteriler için felakettir. Onlar için dumb_arrayçok yavaş olduğundan onlar yeniden yazmak zorunda yazılımın sadece başka parçasıdır. dumb_arrayFarklı şekilde tasarlanmış olsaydı , her iki müşteriyi de hiçbir müşteriden ödün vermeden tatmin edebilirdi.

Her iki istemciyi de tatmin etmenin anahtarı, en düşük düzeyde en hızlı işlemleri oluşturmak ve ardından daha fazla maliyetle daha eksiksiz özellikler için bunun üzerine API eklemektir. Yani güçlü istisna garantisine ihtiyacın var, tamam, parasını sen ödüyorsun. İhtiyacın yok mu? İşte daha hızlı bir çözüm.

Somut olalım: İşte aşağıdakiler için hızlı, temel istisna garantisi Kopyalama Ataması operatörü dumb_array:

dumb_array& operator=(const dumb_array& other)
{
    if (this != &other)
    {
        if (mSize != other.mSize)
        {
            delete [] mArray;
            mArray = nullptr;
            mArray = other.mSize ? new int[other.mSize] : nullptr;
            mSize = other.mSize;
        }
        std::copy(other.mArray, other.mArray + mSize, mArray);
    }
    return *this;
}

Açıklama:

Modern donanım üzerinde yapabileceğiniz daha pahalı şeylerden biri de yığına bir yolculuk yapmaktır. Yığına bir yolculuktan kaçınmak için yapabileceğiniz her şey, harcanan zaman ve emektir. İstemcileri dumb_arraygenellikle aynı boyutta diziler atamak isteyebilir. Ve yaptıklarında, yapmanız gereken tek şey bir memcpy(altında gizli std::copy). Aynı boyutta yeni bir dizi ayırmak ve ardından aynı boyuttaki eski diziyi serbest bırakmak istemezsiniz!

Şimdi, gerçekten güçlü istisna güvenliği isteyen müşterileriniz için:

template <class C>
C&
strong_assign(C& lhs, C rhs)
{
    swap(lhs, rhs);
    return lhs;
}

Veya belki C ++ 11'de taşıma atamasından yararlanmak istiyorsanız, bu şöyle olmalıdır:

template <class C>
C&
strong_assign(C& lhs, C rhs)
{
    lhs = std::move(rhs);
    return lhs;
}

dumb_arrayİstemcileri hıza değer veriyorsa operator=,. Güçlü istisna güvenliğine ihtiyaç duyarlarsa, çok çeşitli nesneler üzerinde çalışacak ve yalnızca bir kez uygulanması gereken, arayabilecekleri genel algoritmalar vardır.

Şimdi orijinal soruya geri dönelim (bu noktada bir type-o var):

Class&
Class::operator=(Class&& rhs)
{
    if (this == &rhs)  // is this check needed?
    {
       // ...
    }
    return *this;
}

Bu aslında tartışmalı bir sorudur. Bazıları evet diyecek, kesinlikle, bazıları hayır diyecek.

Benim kişisel fikrim hayır, bu çeke ihtiyacın yok.

Gerekçe:

Bir nesne bir rvalue referansına bağlandığında, bu iki şeyden biridir:

  1. Geçici.
  2. Arayan kişinin, geçici olduğuna inanmanızı istediği bir nesne.

Gerçek bir geçici olan bir nesneye bir referansınız varsa, o zaman tanım gereği, o nesneye benzersiz bir referansınız vardır. Programınızın tamamında başka herhangi bir yerde buna başvurulamaz. Yani this == &temporary mümkün değil .

Şimdi, eğer müşteriniz size yalan söylediyse ve siz olmadığınızda geçici olarak alacağınıza söz verdiyse, o zaman umursamanıza gerek olmadığından emin olmak müşterinin sorumluluğundadır. Gerçekten dikkatli olmak istiyorsanız, bunun daha iyi bir uygulama olacağına inanıyorum:

Class&
Class::operator=(Class&& other)
{
    assert(this != &other);
    // ...
    return *this;
}

Yani , bir öz referans iletilirse, bu, istemci tarafında düzeltilmesi gereken bir hatadır.

Tamlık için, burada aşağıdakiler için bir taşıma atama operatörü verilmiştir dumb_array:

dumb_array& operator=(dumb_array&& other)
{
    assert(this != &other);
    delete [] mArray;
    mSize = other.mSize;
    mArray = other.mArray;
    other.mSize = 0;
    other.mArray = nullptr;
    return *this;
}

Taşıma atamasının tipik kullanım durumunda, *thisnesneden taşınan bir nesne olacaktır ve bu nedenle delete [] mArray;işlemsiz olmalıdır. Gerçekleştirmelerin nullptr'de olabildiğince hızlı bir şekilde silinmesi çok önemlidir.

Uyarı:

Bazıları bunun swap(x, x)iyi bir fikir veya sadece gerekli bir kötülük olduğunu iddia edecek . Ve bu, eğer takas varsayılan takasa giderse, kendi kendine hareket atamasına neden olabilir.

Bunu katılmıyorum swap(x, x)olduğunu hiç iyi bir fikir. Kendi kodumda bulunursa, bunu bir performans hatası olarak kabul edip düzelteceğim. Ancak, buna izin vermek istemeniz durumunda, swap(x, x)yalnızca kendiliğinden hareket etme-atama ağının taşınan bir değer üzerinde olduğunu fark edin . Ve bizim dumb_arrayörneğimizde, iddiayı atlarsak veya onu taşınan durumla sınırlarsak, bu tamamen zararsız olacaktır:

dumb_array& operator=(dumb_array&& other)
{
    assert(this != &other || mSize == 0);
    delete [] mArray;
    mSize = other.mSize;
    mArray = other.mArray;
    other.mSize = 0;
    other.mArray = nullptr;
    return *this;
}

Eğer iki taşınmış (boş) dumb_array'ları kendi kendinize atarsanız, programınıza gereksiz talimatlar eklemenin dışında yanlış bir şey yapmazsınız. Aynı gözlem, nesnelerin büyük çoğunluğu için yapılabilir.

<Güncelleme>

Bu konuyu biraz daha düşündüm ve konumumu biraz değiştirdim. Artık atamanın kendi kendine atamaya toleranslı olması gerektiğine inanıyorum, ancak kopya atama ve taşıma atamasındaki görevlendirme koşulları farklı:

Kopyalama ataması için:

x = y;

değerinin ydeğiştirilmemesi gereken bir son koşul olmalıdır. O &x == &yzaman bu son koşul şuna çevrildiğinde: kendi kendine kopya atamanın değeri üzerinde hiçbir etkisi olmamalıdır x.

Taşıma ataması için:

x = std::move(y);

ygeçerli ancak belirtilmemiş bir duruma sahip bir son koşul olmalıdır . O &x == &yzaman bu son koşul şuna çevrildiğinde: xgeçerli ancak belirtilmemiş bir duruma sahiptir. Yani kendi kendine hareket atamasının işlemsiz olması gerekmez. Ama çökmemeli. Bu son koşul, swap(x, x)sadece çalışmaya izin vermekle tutarlıdır :

template <class T>
void
swap(T& x, T& y)
{
    // assume &x == &y
    T tmp(std::move(x));
    // x and y now have a valid but unspecified state
    x = std::move(y);
    // x and y still have a valid but unspecified state
    y = std::move(tmp);
    // x and y have the value of tmp, which is the value they had on entry
}

Yukarıdakiler, çökmediği sürece çalışır x = std::move(x). xHerhangi bir geçerli ancak belirtilmemiş durumda bırakabilir .

Bunu dumb_arraybaşarmak için taşıma atama operatörünü programlamanın üç yolunu görüyorum :

dumb_array& operator=(dumb_array&& other)
{
    delete [] mArray;
    // set *this to a valid state before continuing
    mSize = 0;
    mArray = nullptr;
    // *this is now in a valid state, continue with move assignment
    mSize = other.mSize;
    mArray = other.mArray;
    other.mSize = 0;
    other.mArray = nullptr;
    return *this;
}

Yukarıdaki uygulama öz atama katlanmasını ama *thisve otheröz hareket görevden sonra bir sıfır boyutlu dizi, orijinal değeri ne olursa olsun olma sonunda *thisolduğunu. Bu iyi.

dumb_array& operator=(dumb_array&& other)
{
    if (this != &other)
    {
        delete [] mArray;
        mSize = other.mSize;
        mArray = other.mArray;
        other.mSize = 0;
        other.mArray = nullptr;
    }
    return *this;
}

Yukarıdaki uygulama, kopya atama operatörünün yaptığı gibi, işlemsiz hale getirerek kendi kendine atamayı tolere eder. Bu da iyidir.

dumb_array& operator=(dumb_array&& other)
{
    swap(other);
    return *this;
}

Yukarıdakiler, yalnızca dumb_array"hemen" imha edilmesi gereken kaynakları tutmuyorsa uygundur. Örneğin, tek kaynak bellekse, yukarıdakiler uygundur. Eğer dumb_arraymuhtemelen muteks kilitlerini veya dosyaları açık halini tutunabileceği, müşteri makul hareket atama lhs üzerinde bu kaynakların derhal serbest bırakılmasını bekliyor olabilir ve bu nedenle bu uygulaması sorunlu olabilir.

İlkinin maliyeti fazladan iki mağaza. İkincisinin maliyeti bir test ve daldır. İkisi de çalışır. Her ikisi de C ++ 11 standardında Tablo 22 MoveAssignable gereksinimlerinin tüm gereksinimlerini karşılar. Üçüncüsü de bellek dışı kaynak sorununu modulo olarak çalışır.

Her üç uygulamanın da donanıma bağlı olarak farklı maliyetleri olabilir: Bir şube ne kadar pahalıdır? Çok sayıda kayıt var mı yoksa çok az mı?

Çıkarılacak şey, kendi kendine taşıma atamasının, kendi kendine kopyalama atamasından farklı olarak, mevcut değeri korumak zorunda olmamasıdır.

</Güncelleme>

Luc Danton'ın yorumundan esinlenerek (umarız) son bir düzenleme:

Belleği doğrudan yönetmeyen (ancak temelleri veya üyeleri olan) yüksek seviyeli bir sınıf yazıyorsanız, o zaman en iyi taşıma ataması uygulaması genellikle:

Class& operator=(Class&&) = default;

Bu, sırayla her bir üssün ve her üyenin atamasını değiştirecek ve bir this != &otherkontrol içermeyecektir . Bu, üsleriniz ve üyeleriniz arasında hiçbir değişmezin muhafaza edilmesi gerekmediğini varsayarak size en yüksek performansı ve temel istisna güvenliğini verecektir. Güçlü istisna güvenliği talep eden müşterileriniz için onları yönlendirin strong_assign.


6
Bu cevap hakkında nasıl hissedeceğimi bilmiyorum. Bu tür sınıfları (hafızalarını çok açık bir şekilde yöneten) uygulamak yaygın bir şeymiş gibi görünmesini sağlar. Bu zaman doğrudur yapmak yazma böyle bir sınıf bir arayüz özlü fakat kullanışlı olması için çok aşırı istisna emniyet teminat konusunda dikkatli ve tatlı nokta bulma olmalı, ama soru genel tavsiye isteyen gibi görünüyor.
Luc Danton

Evet, kesinlikle kopyala ve değiştir kullanmıyorum çünkü kaynakları ve şeyleri yöneten sınıflar için zaman kaybı (neden gidip tüm verilerinizin başka bir kopyasını yapasınız?). Ve teşekkürler, bu sorumu yanıtlıyor.
Seth Carnegie

5
Hareket-atama-den-öz gerektiğini telkin downvoted hiç assert-fail veya "belirtilmemiş" sonucunu üretir. Kendinden-atama, kelimenin tam anlamıyla doğru yapılması en kolay durumdur . Sınıfınız çökerse, std::swap(x,x)daha karmaşık işlemleri doğru bir şekilde yerine getirmesi için neden ona güveneyim?
Quuxplusone

1
@Quuxplusone: Cevabımın güncellemesinde belirtildiği gibi, assert-fail konusunda sizinle aynı fikirdeyim. Bildiğim kadarıyla std::swap(x,x), belirsiz bir sonuç ürettiğinde bile çalışırx = std::move(x) . Dene! Bana inanmak zorunda değilsin.
Howard Hinnant

@HowardHinnant iyi nokta, herhangi bir hareket kabiliyetine sahip durumda yapraklar swapolduğu sürece çalışır . Ve / algoritmaları, işlemsiz kopyalar üzerinde tanımlanmamış davranışlar üretecek şekilde tanımlanır (ouch; 20 yaşındaki önemsiz durumu doğru alır ama yapmaz!). Sanırım henüz kendini atama için bir "smaç" düşünmedim. Ama belli ki kendi kendini atama, Standart onu kutsasa da kutlamasa da, gerçek kodda çokça gerçekleşen bir şeydir. x = move(x)xstd::copystd::movememmovestd::move
Quuxplusone

11

İlk olarak, taşıma atama operatörünün imzasını yanlış aldın. Hareketler kaynak nesneden kaynakları çaldığı için, kaynağın constr-değeri olmayan bir referans olması gerekir.

Class &Class::operator=( Class &&rhs ) {
    //...
    return *this;
}

Yine de bir (olmayan const) l- değeri referansı aracılığıyla döndüğünüzü unutmayın .

Her iki tür doğrudan atama için de standart, kendi kendine atamayı kontrol etmek değildir, ancak bir kendi kendine atamanın çökmeye ve yanmaya neden olmadığından emin olmaktır. Genel olarak, hiç kimse açıkça yapmaz x = xveya y = std::move(y)çağırmaz, ancak özellikle birden fazla işlev aracılığıyla takma ad, kendi kendine atamalara yol açabilir a = bveya buna neden olabilir c = std::move(d). Kendi kendine atama için açık bir kontrol, yani this == &rhs, doğru olduğunda fonksiyonun etini atlayan, kendi kendine atama güvenliğini sağlamanın bir yoludur. Ancak en kötü yollardan biri, çünkü (umarız) nadir bir durumu optimize ederken, daha yaygın durum için bir anti-optimizasyondur (dallanma ve muhtemelen önbellek eksikliğinden dolayı).

Şimdi, işlenenlerden biri (en azından) doğrudan geçici bir nesne olduğunda, asla kendi kendini atama senaryosuna sahip olamazsınız. Bazı insanlar bu durumu varsaymayı savunur ve kodu o kadar optimize eder ki, varsayım yanlış olduğunda kod intihar edecek şekilde aptal hale gelir. Kullanıcılara aynı nesne kontrolünü atmanın sorumsuz olduğunu söylüyorum. Kopya atama için bu argümanı yapmayız; neden hareket ataması için konumu tersine çevirelim?

Başka bir katılımcıdan değiştirilmiş bir örnek verelim:

dumb_array& dumb_array::operator=(const dumb_array& other)
{
    if (mSize != other.mSize)
    {
        delete [] mArray;
        mArray = nullptr;  // clear this...
        mSize = 0u;        // ...and this in case the next line throws
        mArray = other.mSize ? new int[other.mSize] : nullptr;
        mSize = other.mSize;
    }
    std::copy(other.mArray, other.mArray + mSize, mArray);
    return *this;
}

Bu kopya atama, kendi kendine atamayı açık bir kontrol olmaksızın zarif bir şekilde yönetir. Kaynak ve hedef boyutları farklıysa, ayırma ve yeniden tahsis kopyalama işleminden önce gelir. Aksi takdirde, sadece kopyalama yapılır. Kendi kendine atama optimize edilmiş bir yol elde etmez, kaynak ve hedef boyutları eşit başladığında olduğu gibi aynı yola atılır. Kopyalama, iki nesne eşdeğer olduğunda teknik olarak gereksizdir (aynı nesne oldukları zaman dahil), ancak bu, bir eşitlik kontrolü (değer veya adres açısından) yapmadığınız zamanki fiyattır, çünkü söz konusu kontrolün kendisi çoğu zaman bir israf olacaktır. zamanın. Buradaki nesne kendi kendine atamasının, bir dizi öğe düzeyinde kendi kendine atamaya neden olacağını unutmayın; öğe türü bunu yapmak için güvenli olmalıdır.

Kaynak örneği gibi, bu kopyalama ataması da temel istisna güvenlik garantisini sağlar. Güçlü garanti istiyorsanız, hem kopyalama hem de taşıma atamasını işleyen orijinal Kopyala ve Değiştir sorgusundaki birleştirilmiş atama operatörünü kullanın. Ancak bu örneğin amacı, hız kazanmak için güvenliği bir kademe azaltmaktır. (BTW, tek tek öğelerin değerlerinin bağımsız olduğunu varsayıyoruz; bazı değerleri diğerlerine kıyasla sınırlayan değişmez bir kısıtlama yoktur.)

Aynı tür için bir hareket atamasına bakalım:

class dumb_array
{
    //...
    void swap(dumb_array& other) noexcept
    {
        // Just in case we add UDT members later
        using std::swap;

        // both members are built-in types -> never throw
        swap( this->mArray, other.mArray );
        swap( this->mSize, other.mSize );
    }

    dumb_array& operator=(dumb_array&& other) noexcept
    {
        this->swap( other );
        return *this;
    }
    //...
};

void  swap( dumb_array &l, dumb_array &r ) noexcept  { l.swap( r ); }

Özelleştirme gerektiren değiştirilebilir bir tür, türle swapaynı ad alanında çağrılan iki bağımsız değişkenden bağımsız bir işleve sahip olmalıdır . (Ad kısıtlama çalışmalarına takas için vasıfsız aramaları sağlar.) Bir kap türü de bir kamu eklemek gerekir swapstandart konteyner maç için üye işlevi. Bir üye swapsağlanmadıysa, o zaman serbest işlevin swapmuhtemelen takas edilebilir tipte bir arkadaş olarak işaretlenmesi gerekir. Kullanılacak hareketleri özelleştirirseniz swap, kendi takas kodunuzu sağlamanız gerekir; standart kod, türün hareket kodunu çağırır, bu da hareketle özelleştirilmiş türler için sonsuz karşılıklı yineleme ile sonuçlanır.

Yıkıcılar gibi, takas işlevleri ve taşıma işlemleri mümkünse asla atılmamalı ve muhtemelen böyle işaretlenmelidir (C ++ 11'de). Standart kitaplık türleri ve rutinleri, atılamayan hareketli türler için optimizasyonlara sahiptir.

Taşınmanın bu ilk versiyonu temel sözleşmeyi yerine getiriyor. Kaynağın kaynak işaretçileri hedef nesneye aktarılır. Kaynak nesne artık onları yönettiği için eski kaynaklar sızdırılmayacak. Ve kaynak nesne, atama ve imha dahil olmak üzere daha fazla işlemin uygulanabileceği kullanılabilir bir durumda bırakılır.

swapÇağrı olduğu için, bu taşıma atamasının kendi kendine atama için otomatik olarak güvenli olduğunu unutmayın. Aynı zamanda son derece güvenli. Sorun, gereksiz kaynak tutmadır. Hedef için eski kaynaklara kavramsal olarak artık ihtiyaç yoktur, ancak burada hala sadece kaynak nesnenin geçerli kalabilmesi için buralardadırlar. Kaynak nesnenin planlı imhası çok uzaksa, kaynak alanını boşa harcıyoruz veya daha kötüsü, toplam kaynak alanı sınırlıysa ve (yeni) kaynak nesne resmi olarak ölmeden önce diğer kaynak dilekçeleri gerçekleşecek.

Bu sorun, hareket ataması sırasında kendi kendine hedefleme ile ilgili tartışmalı güncel uzman tavsiyesine neden olan şeydir. Kaynakları kaybetmeden hareket ödevi yazmanın yolu şuna benzer:

class dumb_array
{
    //...
    dumb_array& operator=(dumb_array&& other) noexcept
    {
        delete [] this->mArray;  // kill old resources
        this->mArray = other.mArray;
        this->mSize = other.mSize;
        other.mArray = nullptr;  // reset source
        other.mSize = 0u;
        return *this;
    }
    //...
};

Kaynak varsayılan koşullara sıfırlanırken eski hedef kaynaklar yok edilir. Kendi kendini atama durumunda, mevcut nesneniz intihar eder. Bunun etrafından dolaşmanın ana yolu, eylem kodunu bir if(this != &other)blokla çevrelemek veya onu vidalamak ve müşterilerin bir assert(this != &other)başlangıç ​​satırı yemesine izin vermektir (eğer kendinizi iyi hissediyorsanız).

Bir alternatif, birleşik atama olmadan kopya atamanın nasıl son derece güvenli hale getirileceğini incelemek ve bunu taşıma atamasına uygulamaktır:

class dumb_array
{
    //...
    dumb_array& operator=(dumb_array&& other) noexcept
    {
        dumb_array  temp{ std::move(other) };

        this->swap( temp );
        return *this;
    }
    //...
};

Ne zaman otherve thisne zaman farklıdır, otherhareketle boşaltılır tempve bu şekilde kalır. Daha sonra orijinal olarak sahip olduğu kaynakları alırken thiseski kaynaklarını kaybeder . O zaman eski kaynaklar öldüğünde ölür .tempotherthistemp

Öz atama gerçekleştiğinde, boşaltılması otheriçin tempboşaltır thisde. Ardından hedef nesne kaynaklarını ne zaman geri alır tempve thisdeğiştirir. Ölümü, temppratikte işlemsiz olması gereken boş bir nesne iddia ediyor. this/ otherNesne kaynaklarını tutar.

Hareket-atama, hareket-inşası ve takas da olduğu sürece asla atılmamalıdır. Kendi kendine atama sırasında da güvenli olmanın maliyeti, düşük seviyeli tiplere göre birkaç talimat daha fazladır ve bunlar, serbest bırakma çağrısı ile ortadan kaldırılmalıdır.


deleteİkinci kod bloğunuzu aramadan önce herhangi bir bellek tahsis edilip edilmediğini kontrol etmeniz gerekiyor mu?
user3728501

3
İkinci kod örneğiniz, kendi kendine atama kontrolü olmayan kopya atama operatörü yanlış. std::copykaynak ve hedef aralıkları çakışırsa tanımsız davranışa neden olur (çakıştıkları durum dahil). Bkz. C ++ 14 [alg.copy] / 3.
MM

6

Kendini atamalı güvenli operatörler isteyenlerin kampındayım, ancak uygulamalarında kendi kendine atama kontrolleri yazmak istemiyorum operator=. Ve aslında hiç uygulamak bile istemiyorum operator=, varsayılan davranışın 'kutudan çıkar çıkmaz' çalışmasını istiyorum. En iyi özel üyeler ücretsiz gelenlerdir.

Bununla birlikte, Standartta bulunan MoveAssignable gereksinimleri aşağıdaki şekilde açıklanmaktadır (17.6.3.1 Şablon bağımsız değişken gereksinimleri [utility.arg.requirements], n3290):

İfade Dönüş türü Dönüş değeri Son koşul
t = rv T & tt, atamadan önceki rv değerine eşdeğerdir

yer tutucular şu şekilde tanımlanır: " t[bir] T tipi değiştirilebilir bir değerdir;" ve " rvT tipi bir r değeridir;". Bunların, Standard kitaplığının şablonlarına argüman olarak kullanılan türlere konulan gereksinimler olduğuna dikkat edin, ancak Standardın başka yerlerine baktığımda, taşıma atamasına ilişkin her gereksinimin buna benzer olduğunu fark ettim.

Bu a = std::move(a), "güvenli" olması gerektiği anlamına gelir . İhtiyacınız olan şey bir kimlik testi ise (örn. this != &other), O zaman deneyin, yoksa nesnelerinizi bile koyamazsınız std::vector! (Bu elemanları kullanmayan sürece / do işlemleri MoveAssignable gerektirir; ama boşver o.) Bildirimi önceki örnekte olduğu o a = std::move(a), o zaman this == &othergerçekten de yapacak.


Çalışmamanın a = std::move(a)bir sınıfın nasıl çalışmamasını sağlayacağını açıklayabilir misiniz std::vector? Misal?
Paul J. Lucas

@ PaulJ.Lucas MoveAssignable std::vector<T>::eraseolmadığı sürece Tçağrıya izin verilmez . (IIRC'nin yanı sıra, bazı MoveAssignable gereksinimleri C ++ 14'te MoveInsertable'a getirildi.)
Luc Danton

Tamam, bu yüzden TMoveAssignable olmalı, ama neden erase()bir öğeyi kendisine taşımaya bağlı olsun ki ?
Paul J. Lucas

@ PaulJ.Lucas Bu soruya tatmin edici bir cevap yok. Her şey 'sözleşmeleri bozmayın' şeklinde özetlenebilir.
Luc Danton

2

Mevcut operator=fonksiyonunuz yazılırken, rvalue-referans argümanını yaptığınız için const, işaretçileri "çalmanız" ve gelen rvalue referansının değerlerini değiştirmeniz mümkün değildir ... sadece onu değiştiremezsiniz, siz sadece ondan okuyabiliyordu. Çağrı yapmaya olsaydı sadece bir sorunu görecekti deletesizin vb, işaretçileri thisnormal bir lvaue referans yazar gibi nesne operator=yöntemine ama yenilgilerden bu tür rvalue-versiyonunun noktası ... yani it would Normalde const-lvalue operator=yöntemine bırakılan aynı işlemleri temelde yapmak için rvalue sürümünü kullanmak gereksiz görünmektedir .

Şimdi, eğer kendi operator=değerinizi olmayan bir constreferans almak için tanımladıysanız , o zaman bir kontrolün gerekli olduğunu görebilmemin tek yolu, thisnesneyi geçici yerine kasıtlı olarak bir rvalue referansı döndüren bir işleve geçirmenizdi.

Örneğin, birisinin bir operator+fonksiyon yazmaya çalıştığını ve nesne türünde bazı yığınlanmış toplama işlemleri sırasında fazladan geçici değerlerin oluşturulmasını "önlemek" için rvalue referansları ve lvalue referanslarının bir karışımını kullandığını varsayalım :

struct A; //defines operator=(A&& rhs) where it will "steal" the pointers
          //of rhs and set the original pointers of rhs to NULL

A&& operator+(A& rhs, A&& lhs)
{
    //...code

    return std::move(rhs);
}

A&& operator+(A&& rhs, A&&lhs)
{
    //...code

    return std::move(rhs);
}

int main()
{
    A a;

    a = (a + A()) + A(); //calls operator=(A&&) with reference bound to a

    //...rest of code
}

Şimdi, rvalue referansları hakkında anladığım kadarıyla, yukarıdakileri yapmak tavsiye edilmez (yani, rvalue referansı değil, sadece geçici bir referans döndürmelisiniz), ancak birisi hala bunu yapacaksa, o zaman yapmak için kontrol etmek istersiniz. gelen rvalue referansının thisişaretçi ile aynı nesneye başvurmadığından emin olun .


"A = std :: move (a)" nın bu duruma sahip olmanın önemsiz bir yolu olduğuna dikkat edin. Cevabınız yine de geçerli.
Vaughn Cato

1
Bunun en basit yol olduğuna kesinlikle katılıyorum, ancak çoğu insanın bunu kasıtlı olarak yapmayacağını düşünürdüm :-) ... Ancak rvalue referansı ise const, sadece ondan okuyabileceğinizi unutmayın, bu yüzden tek ihtiyacınız olan Bir kontrol operator=(const T&&)yapmak, takas tarzı bir işlemden ziyade thistipik bir operator=(const T&)yöntemde yapacağınız yeniden başlatmanın aynısını yapmaya karar verdiyseniz (yani, derin kopyalar yapmak yerine işaretçileri çalmak vb.) olacaktır.
Jason

1

Cevabım hala, taşınma görevinin kendini atamaya karşı kurtarılması gerekmediği, ancak farklı bir açıklaması olduğu. Std :: unique_ptr düşünün. Birini uygulayacak olsaydım, şöyle bir şey yapardım:

unique_ptr& operator=(unique_ptr&& x) {
  delete ptr_;
  ptr_ = x.ptr_;
  x.ptr_ = nullptr;
  return *this;
}

Bunu açıklayan Scott Meyers'e bakarsanız, benzer bir şey yapar. (Eğer dolaşırsanız neden takas yapmayasınız - fazladan bir yazma vardır). Ve bu kendi kendine atama için güvenli değil.

Bazen bu talihsizdir. Tüm çift sayıları vektörden çıkarmayı düşünün:

src.erase(
  std::partition_copy(src.begin(), src.end(),
                      src.begin(),
                      std::back_inserter(even),
                      [](int num) { return num % 2; }
                      ).first,
  src.end());

Bu tamsayılar için uygundur, ancak bunun gibi bir şeyi hareket semantiği ile çalıştırabileceğinize inanmıyorum.

Sonuç olarak: atamayı nesnenin kendisine taşımak doğru değildir ve buna dikkat etmeniz gerekir.

Küçük güncelleme.

  1. Howard'a katılmıyorum, bu kötü bir fikir, ama yine de - "taşınmış" nesnelerin kendi kendine hareket atamasının işe yarayacağını düşünüyorum, çünkü swap(x, x)işe yaramalı. Algoritmalar bu tür şeyleri sever! Bir köşe davası işe yaradığında her zaman iyidir. (Ve henüz ücretsiz olmadığı bir vakayı görmedim. Bu, var olmadığı anlamına gelmez).
  2. Unique_ptr'lerin atanması libc ++ 'da böyle uygulanır: unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...} Kendi kendine taşıma ataması için güvenlidir.
  3. Temel Yönergeler, atamayı kendi kendine taşımanın uygun olması gerektiğini düşünüyor.

0

Aklıma gelen (bu == rhs) bir durum var. Bu ifade için: Myclass obj; std :: hareket (obj) = std :: hareket (obj)


Sınıfım obj; std :: hareket (obj) = std :: hareket (obj);
little_monster
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.