Unique_ptr işlevlerinden döndürülüyor


367

unique_ptr<T>kopya oluşturmaya izin vermez, bunun yerine taşıma semantiğini destekler. Yine de, unique_ptr<T>bir işlevden bir geri dönebilir ve döndürülen değeri bir değişkene atayabilirim.

#include <iostream>
#include <memory>

using namespace std;

unique_ptr<int> foo()
{
  unique_ptr<int> p( new int(10) );

  return p;                   // 1
  //return move( p );         // 2
}

int main()
{
  unique_ptr<int> p = foo();

  cout << *p << endl;
  return 0;
}

Yukarıdaki kod istendiği gibi derlenir ve çalışır. Peki bu satır 1kopya oluşturucuyu çağırmıyor ve derleyici hatalarına neden olmuyor? Eğer hat 2kullanmak zorunda olsaydım, bu mantıklı olurdu (hat 2çalışmalarını da kullanmak , ama bunu yapmak zorunda değiliz).

C ++ 0x, bu değer istisna unique_ptr, işlev çıkar çıkmaz yok edilecek geçici bir nesne olduğundan izin verir , böylece döndürülen işaretçinin benzersizliğini garanti eder. Bunun nasıl uygulandığını merak ediyorum, derleyicide özel olarak kaplanmış mı yoksa dil spesifikasyonunda bu istismarın başka bir maddesi var mı?


Varsayımsal olarak, bir fabrika yöntemi uyguluyorsanız , fabrikanın çıktısını iade etmek için 1 veya 2'yi tercih eder misiniz? Bunun 1'in en yaygın kullanımı olacağını düşünüyorum, çünkü uygun bir fabrikada, inşa edilen şeyin sahipliğinin arayan kişiye geçmesini istersiniz.
Xharlie

7
@Xharlie? Her ikisi de unique_ptr. Bütün mesele 1 ve 2 hakkında aynı şeyi başarmanın iki farklı yolu.
Praetorian

bu durumda, RVO c ++ 0x'de de gerçekleşir, unique_ptr nesnesinin imhası mainişlev çıktıktan sonra gerçekleştirilir , ancak çıkıştan sonra gerçekleşmez foo.
ampawd

Yanıtlar:


218

dil belirtiminde bunun istismar ettiği başka bir madde var mı?

Evet, bkz. 12.8 §34 ve §35:

Belirli ölçütler karşılandığında, bir uygulamanın bir nesne nesnesinin kopyala / taşı yapısını atlamasına izin verilir [...] Kopya elizyonu olarak adlandırılan bu kopyala / taşı işlemi seçimine [...] bir dönüş ifadesinde izin verilir. İfade, işlev dönüş türüyle aynı cv-niteliksiz türe sahip geçici olmayan otomatik bir nesnenin adı olduğunda, sınıf dönüş türüne sahip bir işlev [...]

Bir kopyalama işleminin seçilmesi için kriterler karşılandığında ve kopyalanacak nesne bir değer ile belirlenirse, kopya için yapıcıyı seçmek için aşırı yük çözünürlüğü ilk olarak nesne bir değer tarafından belirlenmiş gibi gerçekleştirilir .


En kötü durumda dönüş deyiminde adlandırılmış bir değer, yani C ++ 11, C ++ 14 ve C ++ 17'de elemeler olmadan, değere göre döndürmenin burada varsayılan seçim olması gerektiğine bir nokta daha eklemek istedim bir rvalue olarak. Örneğin, aşağıdaki işlev -fno-elide-constructorsbayrakla derlenir

std::unique_ptr<int> get_unique() {
  auto ptr = std::unique_ptr<int>{new int{2}}; // <- 1
  return ptr; // <- 2, moved into the to be returned unique_ptr
}

...

auto int_uptr = get_unique(); // <- 3

Derlemeye ayarlanan bayrakla bu işlevde iki hamle (1 ve 2) ve daha sonra da (3) bir hamle gerçekleşir.


@juanchopanza foo()Aslında, işlevdeki dönüş değeri gibi, gerçekten de yok edilmek üzere olduğu anlamına mı geliyor unique_ptr<int> p = foo();?
7cows

1
Bu cevap, bir uygulamanın bir şey yapmasına izin verildiğini söylüyor ... olması gerektiği anlamına gelmiyor, bu yüzden bu ilgili tek bölüm olsaydı, bu davranışa güvenmek anlamına gelir taşınabilir değil. Ama bunun doğru olduğunu düşünmüyorum. Nikola Smiljanic'in ve Bartosz Milewski'nin cevabında açıklandığı gibi, doğru cevabın hareket oluşturucu ile daha fazla ilgisi olduğunu düşünmeye meyilliyim.
Don Hatch

6
@DonHatch Bu gibi durumlarda kopyalama / taşıma seçimleri yapmanın "izinli" olduğunu söylüyor, ancak burada kopyalama seçiminden bahsetmiyoruz. Burada geçerli olan ikinci kopya paragraf, kopya elizyon kurallarına domuzcuk destek veriyor, ancak kopya elizyonunun kendisi değil. İkinci paragrafta belirsizlik yok - tamamen taşınabilir.
Joseph Mansfield

@juanchopanza Bunun 2 yıl sonra olduğunun farkındayım, ama bunun hala yanlış olduğunu düşünüyor musun? Önceki yorumda belirttiğim gibi, bu kopya seçimi ile ilgili değil. Öyle ki, kopya seçiminin uygulanabileceği durumlarda (uygulanamazsa bile std::unique_ptr), nesneleri ilk önce değer olarak ele almak için özel bir kural vardır. Bence bu tamamen Nikola'ın cevapladığı şeye katılıyor.
Joseph Mansfield

1
Öyleyse neden yine de yalnızca bu örnekle aynı şekilde döndürülürken salt hareket türüm (kaldırılan kopya oluşturucu) için "silinmiş bir işleve başvurmaya çalışıyor" hatasını alıyorum?
DrumM

104

Bu hiçbir şekilde spesifik değildir std::unique_ptr, ancak taşınabilir olan herhangi bir sınıfa uygulanır. Değerle döndüğünüz için dil kuralları tarafından garanti edilir. Derleyici kopyaları kaldırmaya çalışır, kopyaları kaldıramıyorsa bir taşıma yapıcısını çağırır, hareket edemiyorsa bir kopya yapıcısını çağırır ve kopyalayamazsa derleyemez.

Eğer std::unique_ptrargüman olarak kabul eden bir fonksiyonunuz olsaydı, ona p'yi geçemezdiniz. Move yapıcısını açıkça çağırmanız gerekir, ancak bu durumda çağrıdan sonra p değişkenini kullanmamalısınız bar().

void bar(std::unique_ptr<int> p)
{
    // ...
}

int main()
{
    unique_ptr<int> p = foo();
    bar(p); // error, can't implicitly invoke move constructor on lvalue
    bar(std::move(p)); // OK but don't use p afterwards
    return 0;
}

3
@Fred - pek değil. pGeçici olmasa da , foo()iade edilenin sonucu ; bu nedenle bu bir değerdir ve ödevi mainmümkün kılan taşınabilir . Hatalı olduğunu söyleyebilirim, ancak Nikola daha sonra bu kuralı phatalı olan kendisine uygular .
Edward Strange

Tam olarak söylemek istediklerim, ama kelimeleri bulamadım. Çok net olmadığı için cevabın o kısmını kaldırdım.
Nikola Smiljanić

Bir sorum var: orijinal soruda, Çizgi 1ve Çizgi arasında önemli bir fark var 2mı? Bence aynı şey inşa pederken main, sadece dönüş tipini önemsiyor foo, değil mi?
Hongxu Chen

1
@HongxuChen Bu örnekte kesinlikle hiçbir fark yoktur, kabul edilen cevaptaki standarttan alıntıya bakın.
Nikola Smiljanić

Aslında, daha sonra p'yi atadığınız sürece kullanabilirsiniz. O zamana kadar, içeriğe başvurmaya çalışamazsınız.
Alan

38

unique_ptr geleneksel kopya oluşturucuya sahip değil. Bunun yerine, değer referanslarını kullanan bir "hareket yapıcısı" vardır:

unique_ptr::unique_ptr(unique_ptr && src);

Bir rvalue referansı (çift ve işareti) yalnızca bir rvalue'ya bağlanır. Bu nedenle bir işleve lvalue unique_ptr iletmeye çalıştığınızda hata alıyorsunuz. Öte yandan, bir işlevden döndürülen bir değer bir değer olarak kabul edilir, bu nedenle taşıma yapıcısı otomatik olarak çağrılır.

Bu arada, bu doğru bir şekilde çalışacaktır:

bar(unique_ptr<int>(new int(44));

Buradaki geçici unique_ptr bir değerdir.


8
Bence nokta daha fazla, neden p- "belli ki" bir lvalue - tanımında dönüş ifadesinde bir rvalue olarak ele alınabilir . Ben fonksiyonu kendisi dönüş değeri "hareket" olabilir herhangi bir sorun olduğunu sanmıyorum. return p;foo
CB Bailey

Döndürülen değeri std :: move işlevinden kaydırmak, değerin iki kez taşınacağı anlamına mı geliyor?

3
@RodrigoSalazar std :: move bir lvalue referansından (&) bir rvalue referansına (&&) süslü bir dökümdür.
Std'nin harici

13

Bence Scott Meyers'in Etkili Modern C ++ 'ın 25. maddesinde mükemmel bir şekilde açıklandı . İşte bir alıntı:

Standardın RVO'yu kutsama kısmı, RVO'nun koşulları yerine getirilirse, ancak derleyiciler kopya elizyonu yapmayı seçmezse, döndürülen nesneye bir rvalue olarak davranılması gerektiğini söyler. Gerçekte, Standart, RVO'ya izin verildiğinde, ya kopya seçiminin yapılmasını ya std::moveda döndürülen yerel nesnelere dolaylı olarak uygulanmasını gerektirir.

Burada, RVO atıfta değer optimizasyonu dönmek ve RVT için şartlar yerine getirildiği takdirde yerel nesne dönen araçlar yapmanız beklenir o fonksiyonun içinde tanımlanması RVO güzel atıfta bulunarak kitabının öğesi 25'de açıklandığı da, standart (burada yerel nesne , return ifadesi tarafından oluşturulan geçici nesneleri içerir). Alıntıdanstd::move en büyük çekim ya kopya eleme ya da iade edilen yerel nesnelere dolaylı olarak uygulanmasıdır . Scott std::move, derleyici kopyayı elememeyi seçtiğinde ve programcının açıkça yapmaması gerektiğinde örtük olarak uygulanan madde 25'ten bahseder .

Senin durumunda, kod açıkça aday olduğunu RVT yerel nesnesi döndüren olarak pve tipi pdönüş türü, kopya elision sonuçları aynıdır. Ve eğer derleyici kopyayı elememeyi seçerse, ne sebeple olursa olsun, std::movesıraya girerdi 1.


5

Diğer cevaplarda görmediğim bir şeyBaşka bir cevabı açıklığa kavuşturmak için , bir işlev içinde yaratılan std :: unique_ptr ile bu işleve verilen yanıt arasında bir fark olduğunu açıklığa kavuşturmak .

Örnek şöyle olabilir:

class Test
{int i;};
std::unique_ptr<Test> foo1()
{
    std::unique_ptr<Test> res(new Test);
    return res;
}
std::unique_ptr<Test> foo2(std::unique_ptr<Test>&& t)
{
    // return t;  // this will produce an error!
    return std::move(t);
}

//...
auto test1=foo1();
auto test2=foo2(std::unique_ptr<Test>(new Test));

Yanıtta fredoverflow - açıkça vurgulanan " otomatik nesne" ile bahsedilmiştir . Bir referans (bir rvalue referansı dahil) otomatik bir nesne değildir.
Toby Speight

@TobySpeight Tamam, üzgünüm. Kodum o zaman sadece bir açıklama sanırım.
v010dya
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.