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_array
alt 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_array
Farklı ş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_array
genellikle 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:
- Geçici.
- 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, *this
nesneden 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 y
değiştirilmemesi gereken bir son koşul olmalıdır. O &x == &y
zaman 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);
y
geçerli ancak belirtilmemiş bir duruma sahip bir son koşul olmalıdır . O &x == &y
zaman bu son koşul şuna çevrildiğinde: x
geç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)
. x
Herhangi bir geçerli ancak belirtilmemiş durumda bırakabilir .
Bunu dumb_array
baş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 *this
ve other
öz hareket görevden sonra bir sıfır boyutlu dizi, orijinal değeri ne olursa olsun olma sonunda *this
olduğ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_array
muhtemelen 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 != &other
kontrol 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
.