Scott Meyers ile C ++ 0x ile ilgili Yazılım Mühendisliği radyo podcast röportajını dinledim . Yeni özelliklerin çoğu bana mantıklı geldi ve aslında bir tane hariç, C ++ 0x hakkında heyecanlıyım. Hala hareket semantiği almıyorum ... Tam olarak nedir?
Scott Meyers ile C ++ 0x ile ilgili Yazılım Mühendisliği radyo podcast röportajını dinledim . Yeni özelliklerin çoğu bana mantıklı geldi ve aslında bir tane hariç, C ++ 0x hakkında heyecanlıyım. Hala hareket semantiği almıyorum ... Tam olarak nedir?
Yanıtlar:
Ben hareket semantiği örnek kod ile anlamak en kolay buluyorum. Sadece yığınla ayrılmış bellek bloğuna bir işaretçi tutan çok basit bir dize sınıfıyla başlayalım:
#include <cstring>
#include <algorithm>
class string
{
char* data;
public:
string(const char* p)
{
size_t size = std::strlen(p) + 1;
data = new char[size];
std::memcpy(data, p, size);
}
Hafızayı kendimiz yönetmeyi seçtiğimiz için , üç kuralı takip etmeliyiz . Ödev operatörünün yazılmasını erteleyeceğim ve şimdilik yıkıcı ve kopya yapıcıyı uygulayacağım:
~string()
{
delete[] data;
}
string(const string& that)
{
size_t size = std::strlen(that.data) + 1;
data = new char[size];
std::memcpy(data, that.data, size);
}
Copy yapıcısı, dize nesnelerini kopyalamanın ne anlama geldiğini tanımlar. Parametre const string& that
, aşağıdaki örneklerde kopyalar oluşturmanıza izin veren tür dizesinin tüm ifadelerine bağlanır:
string a(x); // Line 1
string b(x + y); // Line 2
string c(some_function_returning_a_string()); // Line 3
Şimdi, hareket semantiğine ilişkin temel bilgiler geliyor. Sadece kopyaladığımız ilk satırda x
bu derin kopyanın gerçekten gerekli olduğunu unutmayın, çünkü x
daha sonra incelemek isteyebiliriz ve bir x
şekilde değişmiş olsaydı çok şaşırırdık. x
Üç kez nasıl söylediğimi fark ettiniz mi (bu cümleyi eklerseniz dört kez) ve her seferinde aynı nesneyi kastettiniz mi? Biz buna x
"değer" gibi ifadeler diyoruz .
Satır 2 ve 3'teki bağımsız değişkenler değer değil, değerlerdir, çünkü alttaki dize nesnelerinin adı yoktur, bu nedenle istemcinin bunları daha sonraki bir zamanda yeniden denetleme yolu yoktur. rvalues, sonraki noktalı virgülde imha edilen geçici nesneleri belirtir (daha kesin olmak gerekirse, rvalülayı sözlüksel olarak içeren tam ifadenin sonunda). Bunun nedeni başlatılması sırasında önemlidir b
ve c
biz kaynak dizesi ile istedik ve ne yapabildiğimiz, istemci bir fark anlayamadı !
C ++ 0x, diğer şeylerin yanı sıra, işlev aşırı yüklenmesi yoluyla değer argümanlarını tespit etmemizi sağlayan "değer referansı" adlı yeni bir mekanizma sunar. Tek yapmamız gereken bir rvalue referans parametresi olan bir yapıcı yazmak. Bu yapıcı İçinde yapabileceğimiz istediğimiz her şeyi biz bırakın sürece gibi kaynakla bazı geçerli duruma:
string(string&& that) // string&& is an rvalue reference to a string
{
data = that.data;
that.data = nullptr;
}
Burada ne yaptık? Yığın verilerini derinden kopyalamak yerine, işaretçiyi kopyaladık ve sonra orijinal işaretçiyi null değerine ayarladık ('kaynak nesnenin yıkıcısından' sil [] 'nin' sadece çalınan verilerimizi 'serbest bırakmasını önlemek için). Aslında, başlangıçta kaynak dizgiye ait olan verileri "çaldık". Yine, anahtar görüş, müşterinin hiçbir koşulda kaynağın değiştirildiğini tespit edememesidir. Burada gerçekten bir kopya yapmadığımızdan, bu kurucuya "hareket kurucu" diyoruz. Görevi, kaynakları kopyalamak yerine bir nesneden diğerine taşımaktır.
Tebrikler, artık hareket semantiğinin temellerini anlıyorsunuz! Ödev operatörünü uygulayarak devam edelim. Kopyalama ve takas deyimine aşina değilseniz , öğrenin ve geri gelin, çünkü bu istisna güvenliği ile ilgili harika bir C ++ deyimidir.
string& operator=(string that)
{
std::swap(data, that.data);
return *this;
}
};
Ha, bu mu? "Değer referansı nerede?" sorabilirsiniz. "Burada ihtiyacımız yok!" cevabım :)
Parametreyi that
değere göre geçirdiğimizi unutmayın , bu yüzden that
diğer tüm dize nesneleri gibi başlatılmalıdır. Tam olarak nasıl that
başlatılacak? C ++ 98'in eski günlerinde , cevap "kopya yapıcı tarafından" olurdu. C ++ 0x, derleyici, atama işleci bağımsız değişkeninin bir değer veya bir değer olup olmadığına bağlı olarak kopya yapıcısı ve taşıma yapıcısı arasında seçim yapar.
Bu nedenle a = b
, kopya oluşturucu başlatılır that
(ifade bir değer b
olduğu için) ve atama operatörü içeriği yeni oluşturulmuş derin bir kopyayla değiştirir. Bu, kopyalama ve takas deyiminin tam tanımıdır - bir kopya oluşturun, içeriği kopyayla değiştirin ve daha sonra kapsamı bırakarak kopyadan kurtulun. Burada yeni bir şey yok.
Eğer söylersen aynı a = x + y
, hareket yapıcı başlatır that
(ifade, çünkü x + y
bir rvalue), böylece dahil hiçbir derin kopyalama, sadece verimli bir hareket vardır.
that
argümandan hala bağımsız bir nesnedir, ancak yığın verilerinin kopyalanması gerekmediği için taşınması önemsizdi. Kopyalamak gerekli değildi çünkü x + y
bir değerdir ve yine, değerlerle gösterilen dize nesnelerinden taşınmak uygundur.
Özetlemek gerekirse, kopya yapıcı derin bir kopya oluşturur, çünkü kaynağa dokunulmamalıdır. Öte yandan, hareket yapıcı sadece işaretçiyi kopyalayıp kaynaktaki işaretçiyi null değerine ayarlayabilir. Kaynak nesneyi bu şekilde "geçersiz kılmak" uygundur, çünkü istemcinin nesneyi yeniden denetleme yolu yoktur.
Umarım bu örnek ana noktayı ele alır. Değerleri basitleştirmek ve kasıtlı olarak basit tutmak için semantikleri taşımak için çok daha fazlası var. Daha fazla ayrıntı istiyorsanız lütfen ek cevabımı inceleyin .
that.data = 0
, karakterler çok erken (geçici olarak öldüğü zaman) ve ayrıca iki kez yok edilirdi . Verileri çalmak istiyorsunuz, paylaşmayın!
delete[]
üzerindeki Virus721, C ++ standardı tarafından no-op olarak tanımlanır.
İlk cevabım, anlambilimi taşımak için son derece basitleştirilmiş bir girişti ve basit tutmak için birçok ayrıntı dışarıda bırakıldı. Ancak, semantiği taşımak için çok daha fazlası var ve boşlukları doldurmak için ikinci bir cevap vermenin zamanının geldiğini düşündüm. İlk cevap zaten oldukça eski ve bunu tamamen farklı bir metinle değiştirmek doğru gelmiyordu. Bence hala ilk giriş olarak hizmet ediyor. Ama daha derine inmek istiyorsanız, okuyun :)
Stephan T. Lavavej değerli geri bildirimler sağlamak için zaman ayırdı. Çok teşekkür ederim, Stephan!
Anlambilimi taşıma, bir nesnenin belirli koşullar altında başka bir nesnenin dış kaynaklarının sahipliğini almasına izin verir. Bu iki şekilde önemlidir:
Pahalı kopyaları ucuz hamlelere dönüştürmek. Bir örnek için ilk cevabıma bakın. Bir nesne en az bir harici kaynağı (doğrudan veya dolaylı olarak üye nesneleri aracılığıyla) yönetmezse, taşıma semantiğinin kopya semantiğine göre herhangi bir avantaj sağlamayacağını unutmayın. Bu durumda, bir nesneyi kopyalamak ve bir nesneyi taşımak aynı şey anlamına gelir:
class cannot_benefit_from_move_semantics
{
int a; // moving an int means copying an int
float b; // moving a float means copying a float
double c; // moving a double means copying a double
char d[64]; // moving a char array means copying a char array
// ...
};
Güvenli "sadece hareket" türlerinin uygulanması; yani, kopyalamanın mantıklı olmadığı, ancak taşındığı türler. Örnekler arasında kilitler, dosya tanıtıcıları ve benzersiz sahiplik semantiği olan akıllı işaretçiler bulunur. Not: Bu cevap std::auto_ptr
, std::unique_ptr
C ++ 11 ile değiştirilen kullanımdan kaldırılmış bir C ++ 98 standart kitaplık şablonunu tartışır . Ara C ++ programcıları muhtemelen en azından biraz tanıdıktır std::auto_ptr
ve "hareket semantiği" nedeniyle, C ++ 11'deki hareket semantiğini tartışmak için iyi bir başlangıç noktası gibi görünüyor. YMMV.
C ++ 98 standart kitaplığı, benzersiz sahiplik semantiği adı verilen akıllı bir işaretçi sunar std::auto_ptr<T>
. Eğer aşina değilseniz auto_ptr
, amacı dinamik olarak tahsis edilmiş bir nesnenin istisnalar karşısında bile her zaman serbest bırakılmasını garanti etmektir:
{
std::auto_ptr<Shape> a(new Triangle);
// ...
// arbitrary code, could throw exceptions
// ...
} // <--- when a goes out of scope, the triangle is deleted automatically
Hakkında olağandışı olan şey auto_ptr
"kopyalama" davranışı:
auto_ptr<Shape> a(new Triangle);
+---------------+
| triangle data |
+---------------+
^
|
|
|
+-----|---+
| +-|-+ |
a | p | | | |
| +---+ |
+---------+
auto_ptr<Shape> b(a);
+---------------+
| triangle data |
+---------------+
^
|
+----------------------+
|
+---------+ +-----|---+
| +---+ | | +-|-+ |
a | p | | | b | p | | | |
| +---+ | | +---+ |
+---------+ +---------+
Başlatma nasıl Not b
ile a
yok değil bunun yerine üçgen kopyalamak, ancak gelen üçgenin sahipliğini aktarır a
için b
. Ayrıca "demek a
olduğunu taşındı b
veya" üçgen olan " taşındı gelen a
etmek b
". Bu kafa karıştırıcı gelebilir, çünkü üçgenin kendisi her zaman bellekte aynı yerde kalır.
Bir nesneyi taşımak, yönettiği bir kaynağın sahipliğini başka bir nesneye aktarmak anlamına gelir.
Kopya oluşturucu auto_ptr
muhtemelen böyle bir şeye benziyor (biraz basitleştirilmiş):
auto_ptr(auto_ptr& source) // note the missing const
{
p = source.p;
source.p = 0; // now the source no longer owns the object
}
Tehlikeli olan şey auto_ptr
, sözdizimsel olarak bir kopyaya benzeyen şeyin aslında bir hareket olmasıdır. Taşınan bir konumdan bir üye işlevini çağırmaya çalışmak, auto_ptr
tanımlanmamış davranışı başlatır, bu nedenle bir auto_ptr
taşındıktan sonra kullanmamaya çok dikkat etmelisiniz :
auto_ptr<Shape> a(new Triangle); // create triangle
auto_ptr<Shape> b(a); // move a into b
double area = a->area(); // undefined behavior
Ancak her zaman tehlikeli auto_ptr
değildir . Fabrika fonksiyonları aşağıdakiler için mükemmel bir kullanım durumudur :auto_ptr
auto_ptr<Shape> make_triangle()
{
return auto_ptr<Shape>(new Triangle);
}
auto_ptr<Shape> c(make_triangle()); // move temporary into c
double area = make_triangle()->area(); // perfectly safe
Her iki örneğin de aynı sözdizimsel kalıbı nasıl izlediğine dikkat edin:
auto_ptr<Shape> variable(expression);
double area = expression->area();
Ve yine de, bunlardan biri tanımsız davranışlar çağrısındayken, diğeri tanımıyor. Yani ifadeleri arasındaki fark nedir a
ve make_triangle()
? İkisi de aynı tipte değil mi? Gerçekten de öyle, ama farklı değer kategorileri var .
Açıkçası, a
bir auto_ptr
değişkeni ifade make_triangle()
eden ifade ile bir auto_ptr
by değeri döndüren bir fonksiyonun çağrısını ifade eden ve böylece auto_ptr
her çağrıldığında yeni bir geçici nesne oluşturan ifade arasında derin bir fark olmalıdır . a
bir değerlik örneğidir , oysa make_triangle()
bir değerlik örneğidir .
Böyle bir değerden hareket etmek a
tehlikelidir, çünkü daha sonra bir üye işlevini a
tanımlanamayan davranışı çağırarak arayabiliriz . Öte yandan, make_triangle()
kopya oluşturucu işini yaptıktan sonra, geçici olanı tekrar kullanamayız gibi, değerlerden hareket etmek tamamen güvenlidir. Söz konusu geçiciliği ifade eden bir ifade yoktur; sadece make_triangle()
tekrar yazarsak, farklı bir geçici alırız . Aslında, geçici taşındı zaten bir sonraki satıra gitti:
auto_ptr<Shape> c(make_triangle());
^ the moved-from temporary dies right here
Harflerin l
ve r
ödevin sol tarafında ve sağ tarafında tarihi bir kökene sahip olduğunu unutmayın . Bu artık C ++ için geçerli değildir, çünkü bir ödevin sol tarafında görünmeyen değerler vardır (atama operatörü olmayan diziler veya kullanıcı tanımlı türler gibi) ve (sınıf türlerinin tüm değerlerinin) bir atama operatörü ile).
Sınıf türü değeri, değerlendirmesi geçici bir nesne oluşturan bir ifadedir. Normal şartlar altında, aynı kapsamdaki başka hiçbir ifade aynı geçici nesneyi göstermez.
Artık değerlerden hareket etmenin potansiyel olarak tehlikeli olduğunu, ancak değerlerden hareket etmenin zararsız olduğunu anlıyoruz. C ++, değer bağımsız değişkenlerini değer bağımsız değişkenlerinden ayırmak için dil desteğine sahip olsaydı, ya artık değer değerlerinden hareket etmeyi tamamen yasaklayabilir ya da en azından çağrı sitesinde açık değerlerden hareket ettirebiliriz, böylece artık kazayla hareket etmeyiz.
C ++ 11'in bu soruna cevabı rvalue referanslarıdır . Bir rvalue referansı, yalnızca rvalue'lara bağlanan ve sözdizimi olan yeni bir referans türüdür X&&
. Eski iyi referans X&
artık bir değer referansı olarak bilinir . (Not X&&
olan olmayan bir referans referans; böyle bir şey C orada ++).
Biz atarsak const
karışımı içine, zaten referanslardan dört farklı tür var. Ne tür ifadelere X
bağlanabilirler?
lvalue const lvalue rvalue const rvalue
---------------------------------------------------------
X& yes
const X& yes yes yes yes
X&& yes
const X&& yes yes
Pratikte unutabilirsiniz const X&&
. Gelirleri okumakla sınırlı olmak pek kullanışlı değil.
Bir rvalue referansı
X&&
, yalnızca rvalue'lara bağlanan yeni bir referans türüdür.
Rvalue referansları çeşitli versiyonlardan geçti. Sürüm 2.1, bir rvalue referans yana X&&
aynı zamanda farklı türdeki tüm değer kategorilerine bağlanan Y
, bir kapalı dönüştürme olması koşuluyla Y
için X
. Bu durumda, geçici bir tür X
oluşturulur ve rvalue referansı bu geçici dosyaya bağlanır:
void some_function(std::string&& r);
some_function("hello world");
Yukarıdaki örnekte, "hello world"
bir tür değeridir const char[12]
. Üzerinden const char[12]
sonuna örtülü bir dönüşüm const char*
olduğundan std::string
, geçici bir tür std::string
oluşturulur ve r
bu geçici öğeye bağlanır. Bu, değerler (ifadeler) ve geçiciler (nesneler) arasındaki ayrımın biraz bulanık olduğu durumlardan biridir.
X&&
Parametreli bir işleve yararlı bir örnek , hareket yapıcıdır X::X(X&& source)
. Amacı, yönetilen kaynağın sahipliğini kaynaktan mevcut nesneye aktarmaktır.
C ++ 11'de, rvalue referanslarından yararlanan std::auto_ptr<T>
yerini aldı std::unique_ptr<T>
. İçin basitleştirilmiş bir sürümünü geliştirip tartışacağım unique_ptr
. İlk olarak, ham bir işaretçiyi kapsülleyip operatörleri aşırı yüklüyoruz ->
ve *
böylece sınıfımız bir işaretçi gibi geliyor:
template<typename T>
class unique_ptr
{
T* ptr;
public:
T* operator->() const
{
return ptr;
}
T& operator*() const
{
return *ptr;
}
Yapıcı nesnenin sahipliğini alır ve yıkıcı onu siler:
explicit unique_ptr(T* p = nullptr)
{
ptr = p;
}
~unique_ptr()
{
delete ptr;
}
Şimdi ilginç kısım, hareket oluşturucu geliyor:
unique_ptr(unique_ptr&& source) // note the rvalue reference
{
ptr = source.ptr;
source.ptr = nullptr;
}
Bu hareket yapıcı, auto_ptr
kopya oluşturucunun yaptıklarını aynen yapar , ancak yalnızca değerlerle birlikte sağlanabilir:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // error
unique_ptr<Shape> c(make_triangle()); // okay
İkinci satır derlenemez, çünkü a
bir değerdir, ancak parametre unique_ptr&& source
yalnızca değerlere bağlanabilir. Bu tam olarak istediğimiz şeydi; tehlikeli hamleler asla örtük olmamalıdır. Üçüncü satır sadece iyi derler, çünkü make_triangle()
bir değerdir. Hareket yapıcı geçici Varış sahipliğini aktaracaktır c
. Yine, tam olarak istediğimiz buydu.
Taşıma yapıcısı, yönetilen bir kaynağın sahipliğini geçerli nesneye aktarır.
Son eksik parça taşıma atama operatörüdür. Görevi eski kaynağı serbest bırakmak ve yeni kaynağı argümanından elde etmektir:
unique_ptr& operator=(unique_ptr&& source) // note the rvalue reference
{
if (this != &source) // beware of self-assignment
{
delete ptr; // release the old resource
ptr = source.ptr; // acquire the new resource
source.ptr = nullptr;
}
return *this;
}
};
Taşıma atama operatörünün bu uygulamasının, hem yıkıcı hem de taşıma yapıcısının mantığını nasıl kopyaladığını unutmayın. Kopyala ve takas deyimine aşina mısınız? Semantiği taşımak ve takas deyimi olarak taşımak için de uygulanabilir:
unique_ptr& operator=(unique_ptr source) // note the missing reference
{
std::swap(ptr, source.ptr);
return *this;
}
};
Şimdi bu source
bir tür değişkeni unique_ptr
, hareket yapıcısı tarafından başlatılacak; yani, argüman parametreye taşınacaktır. Hareket yapıcısının kendisinde bir değer referans parametresi bulunduğundan, bağımsız değişken yine de bir değer olmalıdır. Kontrol akış kapanış ayracı ulaştığında operator=
, source
otomatik olarak eski kaynak bırakmadan, kapsam dışına gider.
Taşıma atama operatörü, yönetilen bir kaynağın sahipliğini geçerli nesneye aktarır ve eski kaynağı serbest bırakır. Taşıma ve değiştirme deyimi uygulamayı basitleştirir.
Bazen, değerlerden hareket etmek isteriz. Yani, bazen derleyiciye bir lvalue'yu bir rvalue gibi muamele etmesini isteriz, böylece potansiyel olarak güvenli olmasa bile, hareket yapıcısını çağırabilir. Bu amaçla, C ++ 11 std::move
üstbilgi adı verilen standart bir kütüphane işlev şablonu sunar <utility>
. Bu isim biraz talihsizdir, çünkü std::move
basitçe bir rvalueya bir lvalue verir; o mu değil kendisi tarafından bir şey taşıyın. Sadece hareket etmeyi sağlar . Belki de adlandırılmış olması gerekirdi std::cast_to_rvalue
ya std::enable_move
, ama biz artık adı ile sıkışmış.
Bir lvalue'dan açıkça nasıl hareket edeceğiniz aşağıda açıklanmıştır:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // still an error
unique_ptr<Shape> c(std::move(a)); // okay
Üçüncü satırdan sonra a
artık bir üçgen olmadığını unutmayın. Tarafından Çünkü yolunda, açıkça yazma std::move(a)
, biz niyetlerimizi yapılan net: "Eğer ne istersen Değerli yapıcı yapmak a
başlatmak için c
, ben umurumda değil a
artık ile yol var çekinmeyin. a
".
std::move(some_lvalue)
bir rvalue bir rvalue için döküm, böylece sonraki bir hareket sağlar.
Olsa Not std::move(a)
bir rvalue ise, kendi değerlendirme yok değil geçici bir nesne oluşturmak. Bu muamele komiteyi üçüncü bir değer kategorisi oluşturmaya zorladı. Geleneksel anlamda bir rvalue olmasa da, bir rvalue referansına bağlanabilecek bir şeye xvalue (eXpiring değeri) denir . Geleneksel SağDeğerler için yeniden adlandırıldı prvalues (Saf SağDeğerler).
Hem değerler hem de x değerler değerlerdir. X değerleri ve lvalues her ikisi de glvalues (Genelleştirilmiş lvalues ). İlişkilerin bir diyagramla kavranması daha kolaydır:
expressions
/ \
/ \
/ \
glvalues rvalues
/ \ / \
/ \ / \
/ \ / \
lvalues xvalues prvalues
Sadece xvalues gerçekten yeni; geri kalanı sadece yeniden adlandırma ve gruplama nedeniyle.
C ++ 98 değerleri, C ++ 11'deki değerler olarak bilinir. Önceki paragraflardaki "rvalue" tüm tekrarlarını zihinsel olarak "prevalue" ile değiştirin.
Şimdiye kadar, yerel değişkenlere ve fonksiyon parametrelerine doğru hareket gördük. Ancak ters yönde hareket etmek de mümkündür. Bir işlev değere göre döndürülürse, çağrı sitesindeki bazı nesneler (muhtemelen yerel bir değişken veya geçici, ancak herhangi bir nesne olabilir) ifade return
kurucusundan bağımsız değişken olarak ifadeden sonra ifadeyle başlatılır :
unique_ptr<Shape> make_triangle()
{
return unique_ptr<Shape>(new Triangle);
} \-----------------------------/
|
| temporary is moved into c
|
v
unique_ptr<Shape> c(make_triangle());
Belki de şaşırtıcı bir şekilde, otomatik nesneler (olarak bildirilmeyen yerel değişkenler static
) de örtük olarak işlevlerin dışına taşınabilir:
unique_ptr<Shape> make_square()
{
unique_ptr<Shape> result(new Square);
return result; // note the missing std::move
}
Nasıl hamle yapıcısı değeri result
bir argüman olarak kabul eder ? Kapsamı result
sona ermek üzeredir ve yığın çözme sırasında yok edilecektir. Daha sonra kimse bunun bir result
şekilde değiştiğinden şikayet edemezdi ; kontrol akışı arayan kişiye geri döndüğünde, result
artık mevcut değildir! Bu nedenle, C ++ 11, otomatik nesnelerin yazmak zorunda kalmadan işlevlerden döndürülmesini sağlayan özel bir kurala sahiptir std::move
. Aslında, "adlandırılmış dönüş değeri optimizasyonu" nu (NRVO) engellediğinden, otomatik nesneleri işlevlerin dışına taşımak için asla kullanmamalısınız std::move
.
std::move
Otomatik nesneleri işlevlerin dışına taşımak için asla kullanmayın .
Her iki fabrika işlevinde de dönüş türünün bir değer referansı değil, bir değer olduğunu unutmayın. Rvalue referansları hala referanstır ve her zaman olduğu gibi hiçbir zaman otomatik bir nesneye başvuru göndermemelisiniz; derleyiciyi kodunuzu kabul etmesi için kandırırsanız, arayan sarkan bir referansla sonuçlanır:
unique_ptr<Shape>&& flawed_attempt() // DO NOT DO THIS!
{
unique_ptr<Shape> very_bad_idea(new Square);
return std::move(very_bad_idea); // WRONG!
}
Otomatik nesneleri asla rvalue referansı ile döndürmeyin. Taşınma, sadece bir yapıyı
std::move
bir rvalue referansına bağlayarak değil, yalnızca yapıcı tarafından gerçekleştirilir .
Er ya da geç, böyle bir kod yazacaksınız:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(parameter) // error
{}
};
Temel olarak, derleyici parameter
bir değer olduğunu şikayet edecektir . Türüne bakarsanız, bir değer referansı görürsünüz, ancak bir değer referansı basitçe "bir değere bağlı bir referans" anlamına gelir; o mu değil referans kendisi rvalue olduğu anlamına! Gerçekten de, parameter
sadece adıyla sıradan bir değişkendir. parameter
Yapıcı gövdesinin içinde istediğiniz sıklıkta kullanabilirsiniz ve her zaman aynı nesneyi gösterir. Bundan dolaylı olarak hareket etmek tehlikeli olurdu, bu nedenle dil onu yasaklar.
Adlandırılmış bir değer referansı, diğer tüm değişkenler gibi bir değerdir.
Çözüm, hareketi manuel olarak etkinleştirmektir:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(std::move(parameter)) // note the std::move
{}
};
Bunun parameter
başlangıcından sonra artık kullanılmadığını iddia edebilirsiniz member
. Neden std::move
dönüş değerlerinde olduğu gibi sessizce eklemek için özel bir kural yok ? Muhtemelen derleyici uygulayıcıları üzerinde çok fazla yük olacağı için. Örneğin, yapıcı gövdesi başka bir çeviri birimindeyse ne olur? Buna karşılık, dönüş değeri kuralı, return
anahtar sözcükten sonraki tanımlayıcının otomatik bir nesneyi gösterip göstermediğini belirlemek için sembol tablolarını kontrol etmelidir.
Ayrıca parameter
by değerini de iletebilirsiniz . Gibi sadece hareket türleri unique_ptr
için, henüz yerleşik bir deyim yok gibi görünüyor. Şahsen, arayüzde daha az dağınıklığa neden olduğu için değere göre geçmeyi tercih ederim.
C ++ 98, isteğe bağlı, yani bir yerde ihtiyaç duyulduğunda üç özel üye işlevini dolaylı olarak bildirir: kopya oluşturucu, kopya atama işleci ve yıkıcı.
X::X(const X&); // copy constructor
X& X::operator=(const X&); // copy assignment operator
X::~X(); // destructor
Rvalue referansları çeşitli versiyonlardan geçti. Sürüm 3.0'dan bu yana, C ++ 11 isteğe bağlı iki ek özel üye işlevi bildirir: taşıma yapıcısı ve taşıma atama işleci. Ne VC10 ne de VC11'in henüz 3.0 sürümüyle uyumlu olmadığını unutmayın, bu yüzden bunları kendiniz uygulamanız gerekecektir.
X::X(X&&); // move constructor
X& X::operator=(X&&); // move assignment operator
Bu iki yeni özel üye işlevi, yalnızca özel üye işlevlerinden hiçbiri el ile bildirilmezse örtülü olarak bildirilir. Ayrıca, kendi taşıma kurucunuzu veya taşıma atama işlecinizi bildirirseniz, ne kopya oluşturucu ne de kopya atama işleci dolaylı olarak bildirilmez.
Bu kurallar pratikte ne anlama geliyor?
Yönetilmeyen kaynaklara sahip olmayan bir sınıf yazarsanız, beş özel üye işlevinden herhangi birini kendiniz bildirmeniz gerekmez ve doğru kopya semantiği elde edersiniz ve semantiği ücretsiz olarak taşırsınız. Aksi takdirde, özel üye işlevlerini kendiniz uygulamak zorunda kalacaksınız. Tabii ki, sınıfınız hareket semantiğinden yararlanmıyorsa, özel hareket işlemlerini uygulamaya gerek yoktur.
Kopya atama operatörünün ve taşıma atama operatörünün, bağımsız değişkenini değere göre alarak tek bir birleşik atama operatörüne birleştirilebileceğini unutmayın:
X& X::operator=(X source) // unified assignment operator
{
swap(source); // see my first answer for an explanation
return *this;
}
Bu şekilde, uygulamak için özel üye işlevlerinin sayısı beşten dörde düşer. Burada istisna güvenlik ve verimlilik arasında bir denge vardır, ancak bu konuda uzman değilim.
Aşağıdaki işlev şablonunu düşünün:
template<typename T>
void foo(T&&);
T&&
Sadece değerlere bağlanmayı bekleyebilirsiniz , çünkü ilk bakışta bir değer referansına benziyor. Yine de ortaya çıktığı gibi, T&&
değerlere de bağlanır:
foo(make_triangle()); // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a); // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&
Bağımsız değişken tipte bir rvalue ise X
, T
olduğu sonucuna varılmaktadır X
nedenle, T&&
anlamına gelir X&&
. Herkesin beklediği budur. Argüman türünde bir lvalue Ama eğer X
özel bir kural nedeniyle, T
olması sonucuna varılmaktadır X&
dolayısıyla T&&
gibi bir şey anlamına gelecektir X& &&
. C ++ hala referanslarına referansların bağı yoktur çünkü Ama, tip X& &&
olduğu çöktü içine X&
. Bu başlangıçta kafa karıştırıcı ve işe yaramaz gelebilir, ancak mükemmel çöküş için referans çökmesi şarttır (burada tartışılmayacaktır).
T&& bir rvalue referansı değil, bir yönlendirme referansıdır. Ayrıca hangi durumda SolDeğerler bağlanır
T
veT&&
her ikisi lvalue referanslarıdır.
Bir işlev şablonunu değerlerle sınırlamak istiyorsanız, SFINAE'yi tür özellikleriyle birleştirebilirsiniz :
#include <type_traits>
template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);
Artık referans daralmasını anladığınıza göre, std::move
şu şekilde uygulanır:
template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
Gördüğünüz gibi move
, yönlendirme referansı sayesinde her türlü parametreyi kabul eder T&&
ve bir değer referansı döndürür. std::remove_reference<T>::type
Aksi takdirde Çeşidi SolDeğerler için çünkü meta işlev çağrısı gereklidir X
, dönüş tipi olacağını X& &&
içine çökerdi, hangi X&
. Yana t
daima lvalue (a adlandırılmış rvalue referans bir lvalue olduğunu unutmayın), ama biz bağlama istiyoruz t
bir rvalue referansına, biz açıkça döküm zorunda t
doğru dönüş tipine. Rvalue referansı döndüren bir işlevin çağrılması, kendisi bir xvalue'dur. Artık x değerlerinin nereden geldiğini biliyorsunuz;)
std::move
Rvalue referansı döndüren bir işlevin çağrısı, xvalue'dur.
Bu örnekte, değer referansıyla döndürmenin iyi olduğunu unutmayın, çünkü t
otomatik bir nesneyi değil, arayan tarafından geçirilen bir nesneyi gösterir.
Taşıma semantiği, rvalue referanslarına dayanır .
Bir değer, ifadenin sonunda yok edilecek geçici bir nesnedir. Geçerli C ++ 'da, değerler yalnızca const
referanslara bağlanır . C ++ 1x, bir rvalue nesnelerine başvurular olan const
yazılmamış rvalue dışı başvurulara izin verir T&&
.
Bir ifadenin sonunda bir değer öleceğinden , verilerini çalabilirsiniz . Başka bir nesneye kopyalamak yerine , verilerini nesneye taşırsınız .
class X {
public:
X(X&& rhs) // ctor taking an rvalue reference, so-called move-ctor
: data_()
{
// since 'x' is an rvalue object, we can steal its data
this->swap(std::move(rhs));
// this will leave rhs with the empty data
}
void swap(X&& rhs);
// ...
};
// ...
X f();
X x = f(); // f() returns result as rvalue, so this calls move-ctor
Yukarıdaki kodda, eski derleyiciler ile sonucu f()
olduğu kopyalanan içine x
kullanarak X
'nin kopya kurucu. Derleyiciniz hareket semantiğini destekliyorsa ve X
bir hareket yapıcısı varsa, bunun yerine bu çağrılır. Onun bu yana rhs
bağımsız değişken bir olan rvalue , bunun artık gerekli değil biliyoruz ve değerini çalabilir.
Değer Böylece hareket dönen isimsiz geçici gelen f()
için x
(verilerine ise x
boş için başlatılır, X
, atama sonra imha alacak olan geçici taşınır).
this->swap(std::move(rhs));
adı verilen
rhs
Bir olan lvalue bağlamında X::X(X&& rhs)
. std::move(rhs)
Bir rvalue almak için aramalısın , ama bu biraz cevabı tartışıyor.
Önemli bir nesne döndüren bir işleve sahip olduğunuzu varsayın:
Matrix multiply(const Matrix &a, const Matrix &b);
Böyle bir kod yazdığınızda:
Matrix r = multiply(a, b);
sıradan bir C ++ derleyicisi, sonuç için geçici bir nesne multiply()
oluşturur, başlatmak için kopya yapıcısını çağırır r
ve sonra geçici dönüş değerini yok eder. C ++ 0x'deki semantikleri taşıyın "move yapıcısı" nın r
içeriğini kopyalayarak başlatılmasını sağlar ve sonra geçici değeri yok etmek zorunda kalmadan atar.
Bu, ( Matrix
yukarıdaki örnekte olduğu gibi), kopyalanan nesnenin yığın üzerinde dahili sunumunu saklamak için fazladan bellek ayırması durumunda özellikle önemlidir . Bir kopya oluşturucu, ya iç gösterimin tam bir kopyasını oluşturmak ya da referans sayımı ve yazma üzerine kopya semantiğini birlikte kullanmak zorundadır. Bir hareket yapıcısı yığın belleğini yalnız bırakır ve sadece işaretçiyi Matrix
nesnenin içine kopyalar .
Eğer hareket semantiğinin iyi ve derinlemesine bir açıklamasıyla ilgileniyorsanız, üzerlerindeki orijinal makaleyi okumanızı tavsiye ederim, "C ++ Diline Hareket Semantiği Desteği Ekleme Önerisi."
Çok erişilebilir ve okunması kolay ve sunduğu avantajlar için mükemmel bir durum. WG21 web sitesinde hareket semantiği hakkında daha yeni ve güncel makaleler var , ancak bu, üst düzey bir bakış açısıyla şeylere yaklaştığı ve cesur dil ayrıntılarına çok fazla girmediği için muhtemelen en basit olanıdır.
Semantiği taşıma , artık kaynak değere kimseye ihtiyaç duymadığında kaynakları kopyalamak yerine aktarmakla ilgilidir .
C ++ 03'te, nesneler genellikle kopyalanır, yalnızca herhangi bir kod değeri tekrar kullanmadan önce yok edilir veya atanır. Örneğin, RVO devreye girmedikçe bir işlevden değere göre döndüğünüzde, döndürdüğünüz değer arayanın yığın çerçevesine kopyalanır ve kapsam dışına çıkar ve yok edilir. Bkz pass-by-value kaynak nesne geçici olduğunda, gibi algoritmalar: Bu örneklerden sadece biridir sort
bu sadece yeniden düzenlemek öğeleri içinde yeniden tahsis vector
onun zaman capacity()
aşıldığında, vb
Bu tür kopyalama / yok etme çiftleri pahalı olduğunda, bunun nedeni genellikle nesnenin ağır bir kaynağa sahip olmasıdır. Örneğin , her biri kendi dinamik belleğine sahip vector<string>
bir dizi string
nesne içeren dinamik olarak ayrılmış bir bellek bloğuna sahip olabilir. Böyle bir nesneyi kopyalamak pahalıdır: kaynaktaki dinamik olarak ayrılan her blok için yeni bellek ayırmanız ve tüm değerleri kopyalamanız gerekir. O zaman kopyaladığınız tüm hafızayı yeniden konumlandırmanız gerekir. Bununla birlikte, büyük bir taşımakvector<string>
, sadece birkaç noktayı (dinamik bellek bloğuna atıfta bulunur) hedefe kopyalamak ve bunları kaynakta sıfırlamak anlamına gelir.
Kolay (pratik) terimlerle:
Bir nesnenin kopyalanması, "statik" üyelerini kopyalamak ve new
operatörü dinamik nesneleri için çağırmak anlamına gelir . Sağ?
class A
{
int i, *p;
public:
A(const A& a) : i(a.i), p(new int(*a.p)) {}
~A() { delete p; }
};
Ancak, bir nesneyi taşımak (tekrarlıyorum, pratik bir bakış açısıyla) yenilerini oluşturmak için değil, yalnızca dinamik nesnelerin işaretleyicilerini kopyalamayı gerektirir.
Ama bu tehlikeli değil mi? Tabii ki, dinamik bir nesneyi iki kez yok edebilirsiniz (segmentasyon hatası). Bundan kaçınmak için, kaynak göstergelerini iki kez yok etmemek için "geçersiz kılmalısınız":
class A
{
int i, *p;
public:
// Movement of an object inside a copy constructor.
A(const A& a) : i(a.i), p(a.p)
{
a.p = nullptr; // pointer invalidated.
}
~A() { delete p; }
// Deleting NULL, 0 or nullptr (address 0x0) is safe.
};
Tamam, ama bir nesneyi hareket ettirirsem, kaynak nesne işe yaramaz hale gelir, değil mi? Tabii ki, ancak bazı durumlarda bu çok yararlı. En belirgin olanı, anonim bir nesneyle (geçici, rvalue nesnesi, ..., farklı adlarla çağırabilirsiniz) bir işlevi çağırdığımdadır:
void heavyFunction(HeavyType());
Bu durumda, anonim bir nesne oluşturulur, daha sonra function parametresine kopyalanır ve daha sonra silinir. Bu nedenle, nesneyi taşımak daha iyidir, çünkü anonim nesneye ihtiyacınız yoktur ve zamandan ve hafızadan tasarruf edebilirsiniz.
Bu bir "değer" referansı kavramına yol açar. C ++ 11'de yalnızca alınan nesnenin anonim olup olmadığını tespit etmek için bulunurlar. Ben zaten bir "lvalue" atanabilir bir varlık ( =
operatörün sol kısmı ) olduğunu biliyorum, bu yüzden bir lvalue olarak hareket edebilmek için bir nesneye adlandırılmış bir başvuru gerekir. Bir değer tam tersi, adlandırılmış referansı olmayan bir nesne. Bu nedenle, anonim nesne ve değer eş anlamlılardır. Yani:
class A
{
int i, *p;
public:
// Copy
A(const A& a) : i(a.i), p(new int(*a.p)) {}
// Movement (&& means "rvalue reference to")
A(A&& a) : i(a.i), p(a.p)
{
a.p = nullptr;
}
~A() { delete p; }
};
Bu durumda, bir tür nesnenin A
"kopyalanması" gerektiğinde, derleyici, iletilen nesnenin adlandırılıp adlandırılmadığına göre bir lvalue referansı veya bir rvalue referansı oluşturur. Değilse, hareket oluşturucunuz çağrılır ve nesnenin zamansal olduğunu bilirsiniz ve dinamik nesnelerini kopyalamak yerine hareket ettirebilirsiniz, yer ve bellek tasarrufu yapabilirsiniz.
"Statik" nesnelerin her zaman kopyalandığını hatırlamak önemlidir. Statik bir nesneyi (yığındaki nesne değil, yığındaki nesne) "taşımanın" yolu yoktur. Dolayısıyla, bir nesnenin (doğrudan veya dolaylı olarak) dinamik bir üyesi olmadığında "taşı" / "kopyala" ayrımı önemsizdir.
Nesneniz karmaşıksa ve yıkıcı bir kütüphanenin işlevini çağırmak, diğer küresel işlevleri çağırmak veya her ne olursa olsun, ikincil efektlere sahipse, belki de bayraklı bir hareketi işaret etmek daha iyidir:
class Heavy
{
bool b_moved;
// staff
public:
A(const A& a) { /* definition */ }
A(A&& a) : // initialization list
{
a.b_moved = true;
}
~A() { if (!b_moved) /* destruct object */ }
};
Yani, kodunuz daha kısadır ( nullptr
her dinamik üye için bir atama yapmanız gerekmez ) ve daha geneldir.
Diğer tipik soru: A&&
ve arasındaki fark const A&&
nedir? Tabii ki, ilk durumda, nesneyi değiştirebilir ve ikincisinde pratik anlam değil? İkinci durumda, onu değiştiremezsiniz, bu nedenle nesneyi geçersiz kılmanın hiçbir yolu yoktur (değiştirilebilir bir bayrak veya bunun gibi bir şey hariç) ve bir kopya oluşturucu için pratik bir fark yoktur.
Ve ne mükemmel yönlendirme ? Bir "değer referansı" nın "arayanın kapsamındaki" adlandırılmış bir nesneye başvuru olduğunu bilmek önemlidir. Ancak gerçek kapsamda, bir rvalue referansı bir nesnenin adıdır, bu nedenle adlandırılmış bir nesne gibi davranır. Başka bir işleve bir rvalue başvurusu iletirseniz, adlandırılmış bir nesne iletirsiniz, bu nedenle nesne geçici bir nesne gibi alınmaz.
void some_function(A&& a)
{
other_function(a);
}
Nesne a
, öğesinin gerçek parametresine kopyalanır other_function
. Nesnenin a
geçici bir nesne olarak işlem görmeye devam etmesini istiyorsanız, şu std::move
işlevi kullanmalısınız :
other_function(std::move(a));
Bu hat sayesinde, std::move
döküm olacak a
bir rvalue ve other_function
bir adsız bir nesne olarak nesneyi alacak. Tabii ki, other_function
adsız nesnelerle çalışmak için belirli bir aşırı yüklemesi yoksa , bu ayrım önemli değildir.
Bu mükemmel yönlendirme mi? Değil, ama biz çok yakınız. Mükemmel yönlendirme sadece şablonlarla çalışmak için yararlıdır, amacı: bir nesneyi başka bir işleve geçirmem gerekiyorsa, adlandırılmış bir nesne alırsam, nesnenin adlandırılmış bir nesne olarak geçirilmesi gerekir ve Adsız bir nesne gibi iletmek istiyorum:
template<typename T>
void some_function(T&& a)
{
other_function(std::forward<T>(a));
}
Bu, C ++ 11'de aracılığıyla uygulanan mükemmel iletimi kullanan prototip bir işlevin imzasıdır std::forward
. Bu işlev, şablon örneklemenin bazı kurallarından yararlanır:
`A& && == A&`
`A&& && == A&&`
Yani, eğer ( T = A &) T
için bir değer referansı ise , ( A & && => A &). Eğer bir rvalue referans , ayrıca, (A && && => bir &&). Her iki durumda da, gerçek kapsamda adlandırılmış bir nesnedir, ancak arayan kapsamının bakış açısından "referans türü" bilgisini içerir. Bu bilgi ( ), şablon parametresi olarak iletilir ve 'a', türüne göre taşınır veya taşınır .A
a
T
A
a
a
T
T
forward
T
Bu, kopya semantiği gibidir, ancak "taşınan" nesneden verileri çalmak için elde ettiğiniz tüm verileri çoğaltmak yerine.
Bir kopya semantiğinin ne anlama geldiğini biliyor musunuz? Bu, kopyalanabilen türleriniz olduğu anlamına gelir; bunu tanımladığınız kullanıcı tanımlı türler için, açıkça bir kopya oluşturucu ve atama işleci yazmak veya derleyici bunları dolaylı olarak üretir. Bu bir kopyasını yapar.
Hareket semantiği temel olarak r-değeri referansını (&& (evet iki ve işareti kullanarak yeni referans türü) alan yapıcıya sahip, sabit olmayan bir kullanıcı tanımlı türdür, buna hareket oluşturucu denir, aynı atama operatörü için de geçerlidir. Bu nedenle, bir hareket yapıcısı ne yapar, hafızayı kaynak argümanından kopyalamak yerine hafızayı kaynaktan hedefe 'taşır'.
Ne zaman yapmak istersin? well std :: vector bir örnektir, geçici bir std :: vector oluşturduğunuzu ve bir fonksiyondan geri döndürdüğünüzü söyleyin:
std::vector<foo> get_foos();
İşlev döndüğünde, kopya oluşturucudan ek yüke sahip olacaksınız (ve C ++ 0x'de olacaksa) std :: vector kopyalamak yerine bir hareket yapıcısına sahipse, yalnızca işaretçileri ayarlayabilir ve dinamik olarak ayrılmış 'taşı' yeni örneğe bellek. Std :: auto_ptr ile sahiplik aktarımı semantiği gibi.
Hareket semantiği ihtiyacını göstermek için, hareket semantiği olmadan bu örneği ele alalım:
İşte bir tür T
nesneyi alan ve aynı türden bir nesne döndüren bir işlev T
:
T f(T o) { return o; }
//^^^ new object constructed
Yukarıdaki işlev, değere göre çağrı kullanır ; bu, bu işlev çağrıldığında işlev tarafından kullanılmak üzere bir nesne yapılandırılması gerektiği anlamına gelir .
İşlev de değere göre döndüğünden , dönüş değeri için başka bir yeni nesne oluşturulur:
T b = f(a);
//^ new object constructed
İki yeni nesne inşa edildi, bunlardan biri sadece işlev süresi boyunca kullanılan geçici bir nesne.
Yeni nesne dönüş değerinden oluşturulduğunda, kopya yapıcısı çağrılır kopyalamak yeni nesne b geçici nesnenin içeriği. İşlev tamamlandıktan sonra, işlevde kullanılan geçici nesne kapsam dışına çıkar ve yok edilir.
Şimdi, bir kopya oluşturucunun ne yaptığını düşünelim .
Önce nesneyi başlatmalı, ardından eski nesneden tüm ilgili verileri yenisine kopyalamalıdır.
Sınıfa bağlı olarak, belki de çok fazla veri içeren bir kap, o zaman çok fazla zaman ve bellek kullanımını temsil edebilir
// Copy constructor
T::T(T &old) {
copy_data(m_a, old.m_a);
copy_data(m_b, old.m_b);
copy_data(m_c, old.m_c);
}
Hareket semantiği ile artık verileri kopyalamak yerine hareket ettirerek bu işin çoğunu daha az tatsız hale getirmek mümkün .
// Move constructor
T::T(T &&old) noexcept {
m_a = std::move(old.m_a);
m_b = std::move(old.m_b);
m_c = std::move(old.m_c);
}
Verilerin taşınması, verilerin yeni nesne ile yeniden ilişkilendirilmesini içerir. Ve hiçbir kopya yer almıyor .
Bu bir rvalue
referansla gerçekleştirilir .
Bir rvalue
referans hemen hemen bir gibi çalışır lvalue
önemli bir farkla referans:
Bir rvalue referans hareket ettirilebilir ve bir lvalue olamaz.
Gönderen cppreference.com :
Güçlü istisna garantisini mümkün kılmak için, kullanıcı tanımlı hareket kurucuları istisnalar atmamalıdır. Aslında, standart kaplar, kap öğelerinin yerlerinin değiştirilmesi gerektiğinde taşıma ve kopyalama arasında seçim yapmak için genellikle std :: move_if_noexcept yöntemini kullanır. Hem kopyalama hem de taşıma yapıcıları sağlandıysa, aşırı yük çözünürlüğü, bağımsız değişken bir değerse (adsız geçici gibi bir değer veya std :: move sonucu gibi bir xvalue) ise taşıma yapıcısını seçer ve argüman bir lvalue (adlandırılmış nesne veya lvalue başvurusu döndüren bir işlev / işleç). Yalnızca kopya yapıcısı sağlanmışsa, tüm bağımsız değişken kategorileri onu seçer (const'a bir başvuru gerektirdiği sürece, değerler const referanslarına bağlanabilir), bu da hareket etmek için geri dönüşün kopyalanmasını mümkün olmadığında yapar. Birçok durumda, hareket oluşturucuları gözlemlenebilir yan etkiler üretseler bile optimize edilirler, bkz. Kopya seçimi. Bir kurucu, parametre olarak bir rvalue referansı aldığında 'move yapıcısı' olarak adlandırılır. Hiçbir şeyi taşımak zorunda değildir, sınıfın taşınacak bir kaynağa sahip olması gerekmez ve bir 'taşıma yapıcısı' bir parametrenin izin verilen (ancak belki de mantıklı olmayan) durumda bir kaynağı taşıyamayabilir. sabit değer referansı (cons & T &&).
Bunu doğru anladığımdan emin olmak için yazıyorum.
Büyük nesnelerin gereksiz kopyalanmasını önlemek için taşıma semantiği oluşturuldu. "C ++ Programlama Dili" kitabında Bjarne Stroustrup, varsayılan olarak gereksiz kopyalamanın meydana geldiği iki örnek kullanır: bir, iki büyük nesnenin değiştirilmesi ve iki, büyük bir nesnenin bir yöntemden döndürülmesi.
İki büyük nesnenin değiştirilmesi genellikle ilk nesneyi geçici bir nesneye, ikinci nesneyi ilk nesneye kopyalamayı ve geçici nesneyi ikinci nesneye kopyalamayı içerir. Yerleşik bir tür için bu çok hızlıdır, ancak büyük nesneler için bu üç kopya çok zaman alabilir. Bir "taşıma ataması" programlayıcının varsayılan kopya davranışını geçersiz kılmasına ve bunun yerine nesnelere yapılan başvuruları değiştirmesine olanak tanır, bu da hiçbir kopyalamanın olmadığı ve takas işleminin çok daha hızlı olduğu anlamına gelir. Taşıma ataması std :: move () yöntemi çağrılarak çağrılabilir.
Bir nesneyi bir yöntemden varsayılan olarak döndürmek, yerel nesnenin ve onunla ilişkili verilerin, arayan tarafından erişilebilen bir konumda kopyalanmasını içerir (çünkü yerel nesneye arayan tarafından erişilemez ve yöntem bittiğinde kaybolur). Yerleşik bir tür döndürüldüğünde, bu işlem çok hızlıdır, ancak büyük bir nesne döndürülüyorsa, bu işlem uzun sürebilir. Taşıma yapıcısı programcının bu varsayılan davranışı geçersiz kılmasına ve bunun yerine yerel nesneyle ilişkili verileri yığınlamak için çağırana döndürülen nesneyi işaret ederek yerel nesne ile ilişkili yığın verilerini "yeniden" kullanmasına izin verir. Bu nedenle kopyalamaya gerek yoktur.
Yerel nesnelerin (yani yığın üzerindeki nesnelerin) oluşturulmasına izin vermeyen dillerde, bu tür sorunlar, tüm nesneler öbek üzerinde tahsis edildiğinden ve her zaman referans olarak erişildiğinden oluşmaz.
x
ve y
şunları yapabilirsiniz sadece "nesnelere takas referanslar" ; nesnelerde başka verilere başvuran işaretçiler bulunabilir ve bu işaretçiler değiştirilebilir, ancak hareket işleçlerinin hiçbir şeyi değiştirmeleri gerekmez . Buradaki hedef verileri korumak yerine verileri taşınan nesneden silebilirler.
swap()
Hareket semantiği olmadan yazabilirsiniz . "Taşıma ataması std :: move () yöntemi çağrılarak çağrılabilir." - bazen kullanmak gerekir std::move()
- bu aslında hiçbir şeyi hareket ettirmez - derleyiciye argümanın hareketli olduğunu, bazen std::forward<>()
(yönlendirme referanslarıyla) ve diğer zamanlarda derleyicinin bir değerin taşınabileceğini bilmesini sağlar.
İşte Bjarne Stroustrup'un "C ++ Programlama Dili" kitabından bir cevap . Videoyu görmek istemiyorsanız, aşağıdaki metni görebilirsiniz:
Bu pasajı düşünün. Bir işleçten + dönmek, sonucun yerel değişkenden res
ve arayanın erişebileceği bir yere kopyalanmasını içerir .
Vector operator+(const Vector& a, const Vector& b)
{
if (a.size()!=b.size())
throw Vector_siz e_mismatch{};
Vector res(a.size());
for (int i=0; i!=a.size(); ++i)
res[i]=a[i]+b[i];
return res;
}
Gerçekten bir kopya istemedik; sonucu bir fonksiyondan çıkarmak istedik. Bu yüzden bir Vector öğesini kopyalamak yerine taşımak zorundayız. Move yapıcısını şu şekilde tanımlayabiliriz:
class Vector {
// ...
Vector(const Vector& a); // copy constructor
Vector& operator=(const Vector& a); // copy assignment
Vector(Vector&& a); // move constructor
Vector& operator=(Vector&& a); // move assignment
};
Vector::Vector(Vector&& a)
:elem{a.elem}, // "grab the elements" from a
sz{a.sz}
{
a.elem = nullptr; // now a has no elements
a.sz = 0;
}
&&, "rvalue referansı" anlamına gelir ve bir rvalue'yu bağlayabileceğimiz bir referanstır. "değer", kabaca "ödevin sol tarafında görünebilecek bir şey" anlamına gelen "değer" değerini tamamlamayı amaçlamaktadır. Dolayısıyla bir değer kabaca bir işlev çağrısı tarafından döndürülen bir tamsayı ve res
Vektörler için + () operatörünün yerel değişkeni gibi kabaca "atayamayacağınız bir değer" anlamına gelir .
Şimdi ifade return res;
kopyalanmayacak!