Unique_ptr bağımsız değişkenini bir yapıcıya veya işleve nasıl iletirim?


400

C ++ 11'de anlambilimi taşımak için yeniyim ve nasıl işleneceğini çok iyi bilmiyorum unique_ptr yapıcılarda veya işlevlerde parametrelerin . Bu sınıfın kendisine atıfta bulunmayı düşünün:

#include <memory>

class Base
{
  public:

    typedef unique_ptr<Base> UPtr;

    Base(){}
    Base(Base::UPtr n):next(std::move(n)){}

    virtual ~Base(){}

    void setNext(Base::UPtr n)
    {
      next = std::move(n);
    }

  protected :

    Base::UPtr next;

};

Bu şekilde alma fonksiyonları yazmalı mıyım unique_ptr argüman mi?

Ve std::movearama kodunda kullanmam gerekir mi?

Base::UPtr b1;
Base::UPtr b2(new Base());

b1->setNext(b2); //should I write b1->setNext(std::move(b2)); instead?


1
Boş bir işaretçide b1-> setNext'i çağırırken bir segmentasyon hatası değil mi?
balki

Yanıtlar:


836

İşte benzersiz bir işaretçiyi argüman olarak almanın olası yolları ve bunların ilişkili anlamları.

(A) Değere Göre

Base(std::unique_ptr<Base> n)
  : next(std::move(n)) {}

Kullanıcının bunu arayabilmesi için aşağıdakilerden birini yapması gerekir:

Base newBase(std::move(nextBase));
Base fromTemp(std::unique_ptr<Base>(new Base(...));

Eğer ki değer vasıtasıyla benzersiz işaretçi çekmek için transfer söz konusu fonksiyon / nesne / vb pointer sahipliğini. newBaseİnşa edildikten sonra boşnextBase olduğu garanti edilir . Nesneye sahip değilsiniz ve artık ona bir işaretçi bile yok. Gitti.

Parametreyi değere göre aldığımız için bu sağlanır. std::moveaslında vermez hareket şey; sadece süslü bir oyuncu. , bir r-değeri başvurusu olan a değerini std::move(nextBase)döndürür . Tüm yaptığı bu.Base&&nextBase

Çünkü Base::Base(std::unique_ptr<Base> n)değeri yerine r-değeri referans olarak argüman alır, C ++ bizim için otomatik olarak geçici bir yapı olacaktır. Bir yaratır std::unique_ptr<Base>dan Base&&biz yoluyla işlevini verdi std::move(nextBase). Aslında bu geçici bir yapıdır hamle değeri nextBasefonksiyon argüman içine n.

(B) Sabit olmayan l değeri referansı ile

Base(std::unique_ptr<Base> &n)
  : next(std::move(n)) {}

Bu, gerçek bir l değerinde (adlandırılmış değişken) çağrılmalıdır. Böyle bir geçici ile çağrılamaz:

Base newBase(std::unique_ptr<Base>(new Base)); //Illegal in this case.

Bunun anlamı, sabit olmayan referansların başka herhangi bir şekilde kullanılmasının anlamı ile aynıdır: işlev , işaretçinin sahipliğini talep edebilir veya etmeyebilir . Bu kod verildiğinde:

Base newBase(nextBase);

nextBaseBoş olduğuna dair bir garanti yoktur . Bu olabilir boş; olmayabilir. Gerçekten ne Base::Base(std::unique_ptr<Base> &n)yapmak istediğine bağlı . Bu nedenle, ne olacağı sadece işlev imzasından pek belli değil; uygulamayı (veya ilişkili belgeleri) okumalısınız.

Bu nedenle, bunu bir arayüz olarak önermem.

(C) Sabit l-değeri referansı ile

Base(std::unique_ptr<Base> const &n);

Eğer, çünkü bir uygulama görünmüyor olamaz bir hareket const&. A'yı geçerek const&, işlevin Baseimleç üzerinden erişebileceğini söylüyorsunuz , ancak hiçbir yerde saklayamazsınız . Sahipliğini iddia edemez.

Bu yararlı olabilir. Özel durumunuz için değil, ancak birisine bir işaretçi verebilmeniz ve (C ++ kurallarını ihlal etmeden, döküm yapmadan ) iddia edemeyeceğini bilmek her zaman iyidir const. Saklayamazlar. Bunu başkalarına aktarabilirler, ancak diğerleri aynı kurallara uymak zorundadır.

(D) r-değeri referansı ile

Base(std::unique_ptr<Base> &&n)
  : next(std::move(n)) {}

Bu, "sabit olmayan l değeri referansı ile" durumu ile aşağı yukarı aynıdır. Farklar iki şeydir.

  1. Sen edebilirsiniz geçici pass:

    Base newBase(std::unique_ptr<Base>(new Base)); //legal now..
  2. Sen gerekir kullanmak std::moveolmayan geçici argümanlar geçerken.

İkincisi gerçekten sorun. Bu satırı görürseniz:

Base newBase(std::move(nextBase));

Bu çizgi tamamlandıktan sonra nextBaseboş olması gerektiği konusunda makul bir beklentiniz var . Konumundan taşınmış olmalıydı. Sonuçta, std::moveo hareketin gerçekleştiğini söyleyen orada oturuyorsunuz.

Sorun şu ki, öyle değil. Taşındığı garanti edilmez . Buradan taşınmış olabilir , ancak yalnızca kaynak koduna bakarak bilirsiniz. Sadece fonksiyon imzasından anlatamazsınız.

öneriler

  • Değeri (A): Eğer iddia bir işlev için demek durumunda mülkiyet a unique_ptr, değeri tarafından götürün.
  • (C) const l-değer referansı ile: Eğer bir fonksiyonun sadece unique_ptro fonksiyonun yürütülmesi süresince kullanılmasını kastediyorsanız , onu kullanın const&. Alternatif olarak, a yerine a &veya const&yerine işaret edilen gerçek türe geçin unique_ptr.
  • (D) r-değeri referansı ile: Bir işlev sahipliğini talep edebilir veya etmeyebilirse (dahili kod yollarına bağlı olarak), bunu yapın &&. Ancak bunu mümkün olduğunca yapmamanızı şiddetle tavsiye ederim.

Unique_ptr nasıl değiştirilir

A kopyalayamazsınız unique_ptr. Sadece taşıyabilirsiniz. Bunu yapmanın uygun yolu std::movestandart kütüphane fonksiyonudur.

Bir unique_ptrby değeri alırsanız, ondan serbestçe hareket edebilirsiniz. Fakat hareket aslında yüzünden olmaz std::move. Aşağıdaki ifadeyi alın:

std::unique_ptr<Base> newPtr(std::move(oldPtr));

Bu gerçekten iki ifadedir:

std::unique_ptr<Base> &&temporary = std::move(oldPtr);
std::unique_ptr<Base> newPtr(temporary);

(not: Yukarıdaki kod teknik olarak derlenmez, çünkü geçici olmayan r-değeri referansları aslında r-değerleri değildir. Yalnızca demo amaçlıdır).

Bu temporarysadece bir r-değeri referansıdır oldPtr. Bu ise yapıcı bir newPtrhareket olur nerede. unique_ptr'nin hareket yapıcısı ( &&kendi kendine a alan bir yapıcı ) gerçek hareketi yapan şeydir.

Bir unique_ptrdeğeriniz varsa ve bunu bir yerde saklamak istiyorsanız , depolama alanını kullanmak için kullanmanız gerekirstd::move .


5
@Nicol: ancak std::movedönüş değerini adlandırmaz. Adlandırılmış rvalue referanslarının lvalue olduğunu unutmayın. ideone.com/VlEM3
R. Martinho Fernandes

31
Temel olarak bu yanıta katılıyorum, ancak bazı görüşlerim var. (1) Sabit değere referansı geçmek için geçerli bir kullanım durumu olduğunu düşünmüyorum: callee'nin bununla yapabileceği her şey, const (çıplak) işaretçisine de başvurabilir, hatta işaretçinin kendisini daha iyi yapabilir [ve mülkiyetin a unique_ptr. belki diğer bazı arayanlar da aynı işlevselliğe ihtiyaç duyarlar, ancak shared_ptrbunun yerine [=) lvalue referansı ile çağrı, işlev çağrıldığında değiştiriciyi değiştirirse , örneğin, bağlantılı listeye (listeye ait) düğümler ekleme veya kaldırma yararlı olabilir .
Marc van Leeuwen

8
... (3) Değer referansı ile geçerken değerden geçmeyi savunan argümanınız mantıklı olsa da, bence standardın kendisinin her zaman unique_ptrdeğerleri değer referansı ile geçirdiğini düşünüyorum (örneğin, bunları dönüştürürken shared_ptr). Bunun mantığı, arayan kişiye tam olarak aynı hakları verirken (daha fazla değer taşıyabilir veya sarılmış değerlere sahip olabilir std::move, ancak çıplak değerlere sahip olmayabilir) biraz daha verimli olması olabilir (geçici işaretlere geçiş yapılmaz).
Marc van Leeuwen

19
Sadece Marc söylediklerini tekrarlamak ve alıntı için Sutter : "unique_ptr & parametre olarak bir const kullanmayın; kullanımı yerine * Widget"
Jon

17
By-value ile ilgili bir sorun keşfettik - hareket, diğer argüman değerlendirmelerine göre (tabii ki bir initializer_list hariç) argüman başlatma sırasında gerçekleşir. Oysa bir rvalue referansını kabul etmek, hareket çağrısının fonksiyon çağrısından sonra ve dolayısıyla diğer argümanların değerlendirilmesinden sonra gerçekleşmesini şiddetle emreder. Dolayısıyla, sahiplik ne zaman alınacaksa rvalue referansını kabul etmek tercih edilmelidir.
Ben Voigt

57

İşaretçileri, hafızayı std::unique_ptrsınıf şablonunun bir örneği tarafından yönetilen nesnelere geçirmenin farklı uygulanabilir modlarını belirtmeye çalışayım ; aynı zamanda eski std::auto_ptrsınıf şablonu (ki benzersiz işaretçinin yaptığı tüm kullanımlara izin verdiğine inanıyorum, ancak ek olarak, değerlerin beklendiği yerlerde, çağrılmak zorunda kalmadan değiştirilebilen değerlerin kabul edileceğine inanıyorum std::move) ve bir ölçüde de geçerlidir std::shared_ptr.

Tartışmanın somut bir örneği olarak aşağıdaki basit liste türünü ele alacağım.

struct node;
typedef std::unique_ptr<node> list;
struct node { int entry; list next; }

Bu listenin örnekleri (parçaları başka örneklerle paylaşmasına veya dairesel olmasına izin verilemez) tamamen ilkini elinde bulunduran kişiye aittir. list işaretçiyi . İstemci kodu depoladığı listenin hiçbir zaman boş olmayacağını biliyorsa, ilk önce nodea yerine doğrudan depolamayı da seçebilir list. Herhangi bir yıkıcı nodetanımlanmasına gerek yoktur: alanları için yıkıcılar otomatik olarak çağrıldığından, ilk işaretçi veya düğümün ömrü sona erdiğinde tüm liste akıllı işaretçi yıkıcı tarafından yinelemeli olarak silinecektir.

Bu özyinelemeli tür, düz verilere akıllı bir işaretçi durumunda daha az görünür olan bazı durumları tartışma fırsatı verir. Ayrıca işlevlerin kendileri de zaman zaman (yinelemeli olarak) bir istemci kodu örneği sağlarlar. İçin typedef listelbette önyargılıdır unique_ptr, ancak tanım kullanılmak üzere auto_ptrveya shared_ptrbunun yerine aşağıda söylenenlere çok fazla ihtiyaç duyulmaksızın değiştirilebilir (özellikle yıkıcılar yazmaya gerek kalmadan istisna güvenliği ile ilgilidir).

Akıllı işaretçileri geçirme modları

Mod 0: Akıllı işaretçi yerine işaretçi veya başvuru bağımsız değişkenini iletme

İşleviniz sahiplikle ilgili değilse, bu tercih edilen yöntemdir: hiç akıllı bir işaretçi almayın. Bu durumda, işlevin işaret edilen nesneye kimin sahip olduğu veya sahipliğin yönetildiği ile endişelenmenize gerek yoktur , bu nedenle ham bir işaretçiyi geçmek hem tamamen güvenli hem de en esnek formdur, çünkü sahiplik ne olursa olsun bir müşteri her zaman ham bir işaretçi oluşturun (get yöntemi veya operatörün adresinden &).

Örneğin, bu listenin uzunluğunu hesaplama işlevi bir listargüman değil, ham bir işaretçi olmalıdır:

size_t length(const node* p)
{ size_t l=0; for ( ; p!=nullptr; p=p->next.get()) ++l; return l; }

Değişken içeren bir istemci, list headbu işlevi şu şekilde çağırabilir length(head.get()); bunun yerine,node n boş olmayan bir listeyi temsil eden bir çağırabilir length(&n).

İşaretçinin boş olmadığı garanti edilirse (listeler boş olabileceğinden burada durum böyle değildir), işaretçi yerine referans geçmeyi tercih edebilir. Olmayan bir işaretçi / referans olabilirconstFonksiyonun herhangi bir düğüm eklemeden veya çıkarmadan düğümün içeriğini güncellemesi gerekip gerekmediğine (ikincisi sahipliği içerecektir).

0 modu kategorisine giren ilginç bir durum, listenin (derin) bir kopyasını oluşturmaktır; bunu yapan bir işlev elbette oluşturduğu kopyanın sahipliğini devretmekle birlikte, kopyaladığı listenin sahipliğiyle ilgilenmez. Böylece şu şekilde tanımlanabilir:

list copy(const node* p)
{ return list( p==nullptr ? nullptr : new node{p->entry,copy(p->next.get())} ); }

Bunun için (hiç özyinelemeli aramanın sonucunu derler neden olarak soru için hem bu kod yararları yakından bakmak, copybir hareket kurucusundaki rvalue referans tartışmaya Başlatıcı liste bağlar içinde unique_ptr<node>aka list, başlatılıyor zaman nextalanını üretilen node) ve özel durum korumalı (özyinelemeli tahsis sürecinin bellek biterse ve bazı çağrı sırasında bir eğer neden olarak soru için newatar std::bad_alloc, sonra o zaman kısmen inşa listesine bir işaretçi türü geçici anonim tutulurlist başlatıcı listesi için oluşturulur ve yıkıcısı bu kısmi listeyi temizler). Bu arada bir (Başlangıçta yaptığı gibi) ikinci değiştirmek için günaha karşı gerektiği nullptrtarafındanpve sonuçta bu noktada null olduğu bilinir: null olduğu bilinse bile (ham) bir işaretçiden sabite bir akıllı işaretçi yapılamaz.

Mod 1: Akıllı işaretçiyi değere göre geçirin

Akıllı işaretçi değerini bağımsız değişken olarak alan bir işlev hemen işaret edilen nesneye sahip olur: arayanın tuttuğu akıllı işaretçi (adlandırılmış bir değişken veya anonim geçici olarak) işlev girişindeki bağımsız değişken değerine ve arayanın imleç geçersiz hale geldi (geçici bir durumda kopya kaldırılmış olabilir, ancak her durumda arayan, işaretlenen nesneye erişimi kaybetmiştir). Bu mod çağrısını nakit olarak aramak istiyorum : arayan, çağrılan hizmet için ön ödeme yapar ve çağrıdan sonra sahiplik hakkında hiçbir yanılsama yaşayamaz. Bunu açıklığa kavuşturmak için, dil kuralları arayanın argümanı içine almasını gerektirirstd::moveakıllı işaretçi bir değişkende tutulursa (teknik olarak, bağımsız değişken bir değerse); bu durumda (ancak aşağıdaki mod 3 için değil), bu işlev adından da anlaşılacağı gibi, değeri değişkenten geçici hale getirip değişkeni null bırakarak yapar.

Aranan işlevin koşulsuz olarak sivri uçlu nesnenin sahipliğini aldığı (pilfers), bu mod, sahip olduğu std::unique_ptrveya std::auto_ptrişaretçi ile birlikte geçmesi için iyi bir yoldur ve bu da bellek sızıntısı riskini önler. Bununla birlikte, aşağıdaki mod 3'ün mod 1'de tercih edilmeyeceği (çok az) sadece çok az durum olduğunu düşünüyorum. Bu nedenle, bu modun hiçbir kullanım örneğini vermeyeceğim. (Ancak reversedaşağıdaki mod 3 örneğine bakın, burada mod 1'in de en azından yapacağı belirtilmiştir.) İşlev yalnızca bu işaretçiden daha fazla argüman alırsa, moddan kaçınmanın teknik bir nedeni de olabilir. 1 (std::unique_ptr veyastd::auto_ptr ): bir işaretçi değişkeni iletilirken gerçek bir hareket işlemi gerçekleştiğindenpifade ile diğer argümanları değerlendirirken faydalı bir değer tutar (değerlendirme sırası belirtilmemiş), bu da ince hatalara yol açabilir; tersine, mod 3 kullanıldığında , işlev çağrısından önce herhangi bir hareket yapılmamasını sağlar , böylece diğer argümanlar bir değere güvenli bir şekilde erişebilir .std::move(p) , olduğu varsayılamazppp

std::shared_ptrBununla birlikte kullanıldığında , bu mod, tek bir işlev tanımı ile, arayanın işlev tarafından kullanılacak yeni bir paylaşım kopyası oluştururken işaretçinin paylaşım kopyasını kendisi için tutup tutmayacağını seçmesine izin vermesi ilginçtir (bu, bir değer değeri bağımsız değişken sağlanır; çağrıda kullanılan paylaşılan işaretçiler için kopya oluşturucu referans sayısını artırır) veya işleve bir tanesini tutmadan veya referans sayısına dokunmadan işaretçinin bir kopyasını vermek için (bu, muhtemelen bir rvalue bağımsız değişkeni sağlandığında gerçekleşir çağrısına sarılmış bir lvalue std::move). Örneğin

void f(std::shared_ptr<X> x) // call by shared cash
{ container.insert(std::move(x)); } // store shared pointer in container

void client()
{ std::shared_ptr<X> p = std::make_shared<X>(args);
  f(p); // lvalue argument; store pointer in container but keep a copy
  f(std::make_shared<X>(args)); // prvalue argument; fresh pointer is just stored away
  f(std::move(p)); // xvalue argument; p is transferred to container and left null
}

Aynı şey ayrı ayrı tanımlanarak void f(const std::shared_ptr<X>& x)(lvalue durumu için) ve void f(std::shared_ptr<X>&& x)(rvalue durumu için), işlev gövdelerinin yalnızca ilk sürümün kopya semantiğini (kullanırken kopya oluşturma / atama kullanarak x) ancak ikinci sürüm taşıma semantiğini kullanmasıyla farklılık göstermesi ile elde edilebilir. ( std::move(x)bunun yerine örnek kodda olduğu gibi yazma ). Bu nedenle, paylaşılan işaretçiler için, mod 1 bazı kod çoğaltmalarını önlemek için yararlı olabilir.

Mod 2: Akıllı bir işaretçiyi (değiştirilebilir) değer referansıyla geçirin

Burada fonksiyon sadece akıllı işaretçiye değiştirilebilir bir referansa sahip olmayı gerektirir, ancak onunla ne yapacağına dair hiçbir belirti vermez. Bu yöntemi kart ile aramak istiyorum : arayan kredi kartı numarası vererek ödeme sağlar. Referans olabilir sivri-nesneye ancak zorunda değildir sahipliğini almak için kullanılabilir. Bu mod, işlevin istenen etkisinin argüman değişkeninde yararlı bir değer bırakmayı içerebileceği gerçeğine karşılık gelen değiştirilebilir bir değer bağımsız değişkeni sağlamayı gerektirir. Bu tür bir işleve geçmek istediği rvalue ifadesine sahip bir arayan, çağrıyı yapabilmek için adlandırılmış bir değişkende saklamak zorunda kalacaktır, çünkü dil yalnızca bir sabite dolaylı dönüştürme sağlarbir değerden referans değeri (geçici olarak atıfta bulunarak). (Ele alınan zıt durumdan farklı olarak , akıllı işaretçi türüyle std::movebir oyuncudan yayın Y&&yapmak mümkün değildir; yine de bu dönüşüm gerçekten istenirse basit bir şablon işlevi ile elde edilebilir; bkz. Https://stackoverflow.com/a/24868376 / 1436796 ). Aranan işlevin koşulsuz olarak nesnenin sahipliğini almayı amaçladığı ve argümandan çaldığı durumlarda, bir değer değeri argümanı sağlama yükümlülüğü yanlış sinyal verir: değişkenin çağrıdan sonra yararlı bir değeri olmayacaktır. Bu nedenle, fonksiyonumuz içinde özdeş olasılıklar sunan ancak arayanlardan bir değer sağlamasını isteyen mod 3, bu kullanım için tercih edilmelidir.Y&Y

Bununla birlikte, mod 2 için geçerli bir kullanım durumu vardır, yani işaretçiyi değiştirebilecek işlevler veya sahiplik içeren bir şekilde işaret edilen nesne . Örneğin, bir düğüme önek ekleyen bir işlev list, bu tür kullanımlara bir örnek sağlar:

void prepend (int x, list& l) { l = list( new node{ x, std::move(l)} ); }

Açıkça burada, arayanları kullanmaya zorlamak istenmeyen bir durum olacaktır std::move, çünkü akıllı işaretçileri hala çağrıdan sonra iyi tanımlanmış ve boş olmayan bir listeye sahiptir, ancak öncekinden farklı bir liste.

Yine prepend, boş hafıza eksikliği nedeniyle çağrı başarısız olursa ne olduğunu gözlemlemek ilginçtir . Sonra newçağrı atacak std::bad_alloc; bu noktada, nodetahsis edilemediğinden, atanan rvalue referansının (mod 3) std::move(l)henüz tahsis edilemediği nextalanın inşası için yapılacağı için henüz çalınamayacağı kesindir node. Böylece lhata atıldığında orijinal akıllı işaretçi hala orijinal listeyi tutar; bu liste ya akıllı işaretçi yıkıcı tarafından düzgün bir şekilde imha edilecek ya da lyeterince erken bir catchfıkra sayesinde hayatta kalması durumunda orijinal listeyi koruyacaktır.

Bu yapıcı bir örnekti; bir göz kırpma ile bu sorunun bir de, eğer varsa, belli bir değeri içeren birinci düğüm kaldırma daha yıkıcı örnek verebiliriz:

void remove_first(int x, list& l)
{ list* p = &l;
  while ((*p).get()!=nullptr and (*p)->entry!=x)
    p = &(*p)->next;
  if ((*p).get()!=nullptr)
    (*p).reset((*p)->next.release()); // or equivalent: *p = std::move((*p)->next); 
}

Yine burada doğruluk oldukça incedir. Özellikle, son ifadede, (*p)->nextkaldırılacak düğümün içinde tutulan işaretçinin bağlantısı kaldırılır (ile release, işaretçiyi döndürür, ancak orijinali null yapar), bu düğümü (örtülü olarak) yok etmeden önce reset (tarafından tutulan eski değeri yok ettiğinde p) bir ve sadece bir düğüm o anda yok edilir. ( Yorumda adı geçen alternatif formda, bu zamanlama, std::unique_ptrörneğin taşıma-atama operatörünün uygulanmasının listiçlerine bırakılacaktır; Standart, bu operatörün "20.7.1.2.3; 2 çağıran reset(u.release())zamanlama burada da güvenli olmalıdır nereden,".)

O Not prependve remove_firstyerel saklamak müşteriler tarafından çağrılamaz nodebir zaman boş olmayan liste için değişken ve haklı olarak çok uygulamaları gibi durumlarda için yapamadı iş verilir beri.

Mod 3: Akıllı bir işaretçiyi (değiştirilebilir) değer referansıyla geçirin

İşaretçinin sahipliğini alırken kullanmak için tercih edilen mod budur. Bu yöntem çağrıyı çekle çağırmak istiyorum : arayan nakit çekiyormuş gibi çek bırakma sahipliğini kabul etmelidir, ancak çek imzalanarak gerçek çekilme, çağrılan işlev aslında işaretçiyi çalıncaya kadar ertelenir (mod 2'yi kullanırken olduğu gibi) ). "Çekin imzalanması" somut olarak, arayanların std::movebir değerse (bir değer ise, "sahiplikten vazgeçme" kısmı açıktır ve ayrı bir kod gerektirmezse) ( argümanı 1 modunda olduğu gibi) bir argümanı sarmaları gerektiği anlamına gelir .

Teknik olarak mod 3'ün tam olarak mod 2 gibi davrandığını, bu nedenle çağrılan işlevin sahiplik üstlenmesine gerek olmadığını unutmayın ; ancak (normal kullanımda) sahiplik transferi hakkında herhangi bir belirsizlik varsa, mod 2 moduna 3'ü kullanarak onlar arayanlar için bir sinyal örtülü olduğu böylece, mod 3'e tercih edilmelidir ısrar edeceğini edilir sahipliğini vazgeçerek. Sadece mod 1 argümanının geçmesinin gerçekten arayanlara zorla sahiplik kaybına işaret ettiğini doğrulayabilir. Ancak bir istemcinin çağrılan işlevin niyetleri hakkında herhangi bir şüphesi varsa, çağrılan işlevin özelliklerini bilmesi gerekir, bu da herhangi bir şüphe ortadan kaldırmalıdır.

listMod 3 argümanını kullanan tipimizi içeren tipik bir örnek bulmak şaşırtıcı derecede zordur . Bir listeyi bbaşka bir listenin sonuna taşımak atipik bir örnektir; ancak a(operasyonun sonucunu koruyan ve tutan) mod 2 kullanılarak daha iyi geçirilir:

void append (list& a, list&& b)
{ list* p=&a;
  while ((*p).get()!=nullptr) // find end of list a
    p=&(*p)->next;
  *p = std::move(b); // attach b; the variable b relinquishes ownership here
}

Mod 3 bağımsız değişken geçişinin saf bir örneği, bir listeyi (ve sahipliğini) alan ve aynı düğümleri içeren bir listeyi ters sırada döndüren şudur.

list reversed (list&& l) noexcept // pilfering reversal of list
{ list p(l.release()); // move list into temporary for traversal
  list result(nullptr);
  while (p.get()!=nullptr)
  { // permute: result --> p->next --> p --> (cycle to result)
    result.swap(p->next);
    result.swap(p);
  }
  return result;
}

Bu işlev l = reversed(std::move(l));, listeyi kendisine ters çevirmek için olduğu gibi çağrılabilir , ancak tersine çevrilmiş liste farklı şekilde kullanılabilir.

Burada argüman verimlilik için derhal yerel bir değişkene taşınır (biri parametreyi ldoğrudan yerinde kullanabilir p, ancak daha sonra her seferinde erişmek ekstra bir dolaylı seviye içerir); dolayısıyla mod 1 argüman geçişi arasındaki fark minimumdur. Aslında, bu modu kullanarak, argüman doğrudan yerel değişken olarak işlev görebilir ve böylece ilk hareketten kaçınabilir; bu sadece referansla iletilen bir argüman sadece bir yerel değişkeni başlatmaya hizmet ediyorsa, bunun yerine değere göre de geçebilir ve parametreyi yerel değişken olarak kullanabilir.

Mod 3'ün kullanılması, mod 3'ü kullanarak akıllı işaretçilerin sahipliğini aktaran tüm sağlanan kütüphane işlevlerinin tanık olduğu gibi standart tarafından savunulduğu anlaşılmaktadır std::shared_ptr<T>(auto_ptr<T>&& p). (Kullanılan Yani yapıcı std::tr1) bir değiştirilebilir almaya lvalue (tıpkı referans auto_ptr<T>&kopya kurucu) ve bu nedenle bir ile denebilecek auto_ptr<T>lvalue polduğu gibi std::shared_ptr<T> q(p)bundan sonra, pboş olarak sıfırlanmış. Bağımsız değişken geçerken mod 2'den 3'e değişiklik nedeniyle, bu eski kodun üzerine yeniden yazılması gerekir std::shared_ptr<T> q(std::move(p))ve daha sonra çalışmaya devam eder. Komitenin burada mod 2'yi beğenmediğini anlıyorum, ancak tanımlayarak, mod 1'e geçme seçeneği vardıstd::shared_ptr<T>(auto_ptr<T> p)bunun yerine, eski kodun değiştirilmeden çalışmasını sağlayabilirlerdi, çünkü (benzersiz işaretçilerden farklı olarak) otomatik işaretçiler sessizce bir değere silinebilir (işaretçi nesnesi işlemde null değerine sıfırlanır). Görünüşe göre komite, mod 1 yerine mod 3'ü savunmayı tercih etti , zaten kullanımdan kaldırılmış bir kullanım için bile mod 1'i kullanmak yerine mevcut kodu aktif olarak kırmayı seçtiler .

Mod 3 yerine mod 3 ne zaman tercih edilir

Mod 1 birçok durumda mükemmel bir şekilde kullanılabilir ve mülkiyetin aksi halde akıllı işaretçiyi reversedyukarıdaki örnekte olduğu gibi yerel bir değişkene taşıma şeklini alacağı varsayıldığında mod 3'e göre tercih edilebilir . Bununla birlikte, daha genel bir durumda mod 3'ü tercih etmek için iki neden görebilirim:

  • Bir referansı geçmek, geçici bir işaret oluşturmaktan ve eski işaretçiyi birleştirmekten biraz daha etkilidir (nakit kullanımı biraz zahmetlidir); bazı senaryolarda işaretçi, gerçekte çalınmadan önce başka bir işleve birkaç kez değişmeden geçirilebilir. Bu tür geçişler genellikle yazmayı gerektirir std::move(mod 2 kullanılmadığı sürece), ancak bunun sadece hiçbir şey yapmayan (özellikle de kayıt silme yok) bir döküm olduğunu unutmayın, bu nedenle sıfır maliyete sahiptir.

  • Herhangi bir şeyin fonksiyon çağrısının başlaması ile onun (ya da içerdiği bir çağrının) gerçekte sivri uçlu nesneyi başka bir veri yapısına taşıdığı nokta arasında bir istisna fırlatması düşünülebilirse (ve bu istisna zaten fonksiyonun içinde yakalanmamıştır) ), mod 1 kullanıldığında, akıllı işaretçi tarafından atıfta bulunulan nesne, bir catchcümlenin istisnayı işleyebilmesi için imha edilir (çünkü fonksiyon parametresi yığın çözme sırasında tahrip edildiğinden), ancak mod 3 kullanılırken böyle olmaz. arayan, bu gibi durumlarda (istisna yakalayarak) nesnenin verilerini kurtarma seçeneğine sahiptir. Buradaki mod 1'in bellek sızıntısına neden olmadığını , ancak program için geri alınamayan veri kaybına neden olabileceğini ve bu da istenmeyen olabilir.

Akıllı işaretçiyi döndürme: her zaman değere göre

Akıllı bir işaretçiyi döndürmeyle ilgili bir kelimeyi sonuçlandırmak için , muhtemelen arayan tarafından kullanılmak üzere oluşturulmuş bir nesneyi işaret etmek. Bu gerçekten fonksiyonları içine işaretçileri geçirmeden ile kıyaslanabilir bir durum değildir, ancak şeyiyle ben böyle durumlarda ısrar etmek istiyorum her zaman değerleriyle dönmek (ve kullanmayan std::move içinde returndeyimi). Hiç kimse muhtemelen yeni kurulmuş olan bir işaretçiye referans almak istemez .


1
Mod 0 için +1 - unique_ptr yerine temel işaretçiyi iletme. Biraz konu dışı (soru unique_ptr'i geçirmeyle ilgili olduğu için), ancak basit ve sorunlardan kaçınıyor.
Machta

" mod 1 burada bellek sızıntısına neden olmaz " - bu, mod 3'ün bellek sızıntısına neden olduğunu ima eder, bu doğru değildir. Taşınıp unique_ptrtaşınmadığına bakılmaksızın, yok edildiğinde veya yeniden kullanıldığında hala tutarsa ​​değeri yine de güzel bir şekilde siler.
rustyx

@RustyX: Bu imaları nasıl yorumladığınızı göremiyorum ve asla ima ettiğimi düşündüğünüzü söylemek istemedim. Demek istediğim, başka yerlerde olduğu gibi, unique_ptrbellek sızıntısının önlenmesi (ve dolayısıyla bir anlamda sözleşmesini yerine getirmesi), ancak burada (yani mod 1'i kullanarak) (belirli koşullar altında) daha zararlı olarak kabul edilebilecek bir şeye neden olabilir. , mod 3 kullanılarak önlenebilecek veri kaybı (işaret edilen değerin bozulması)
Marc van Leeuwen

4

Yapıcıdaki unique_ptrby değerini alırsanız evet gerekir . Açıklık güzel bir şeydir. Yana unique_ptr(özel kopya ctor) kopyalanamaz, sen yazdıklarını sana bir derleyici hatası vermelidir.


3

Düzenleme: Bu cevap yanlış olsa da, kesinlikle konuşursak, kod çalışıyor. Sadece burada bırakıyorum çünkü altındaki tartışma çok faydalı. Bu diğer cevap, bunu en son düzenlediğim zaman verilen en iyi yanıttır: Bir yapıcıya veya işleve nasıl unique_ptr argümanını iletirim?

Temel fikir, ::std::movesizi geçen insanların unique_ptr, geçtiklerini bildikleri bilgisini ifade etmek için kullanmaları gerektiğidir unique_ptr.

Bu unique_ptr, yöntemlerinizde a değerine bir rvalue referansı kullanmanız gerektiği anlamına gelir unique_ptr. Bu zaten işe yaramaz çünkü bir düz eski geçmek unique_ptrbir kopya yapmak gerektirir ve bu arayüz için açıkça yasaktır unique_ptr. İlginç bir şekilde, adlandırılmış bir rvalue referansı kullanmak onu tekrar bir lvalue'ya dönüştürür, bu nedenle yöntemlerinizin ::std::move içinde de kullanmanız gerekir .

Bu, iki yönteminizin şöyle görünmesi gerektiği anlamına gelir:

Base(Base::UPtr &&n) : next(::std::move(n)) {} // Spaces for readability

void setNext(Base::UPtr &&n) { next = ::std::move(n); }

Sonra yöntemleri kullanan insanlar bunu yapar:

Base::UPtr objptr{ new Base; }
Base::UPtr objptr2{ new Base; }
Base fred(::std::move(objptr)); // objptr now loses ownership
fred.setNext(::std::move(objptr2)); // objptr2 now loses ownership

Gördüğünüz gibi ::std::move, işaretçinin en alakalı ve bilmek yararlı olduğu noktada sahipliğini kaybedeceğini ifade eder. Bu görünmez bir şekilde gerçekleşmiş olsaydı, sınıfınızı kullanan kişilerin objptraniden görünürde bir sebep olmadan aniden sahipliğini kaybetmeleri çok kafa karıştırıcı olurdu .


2
Adlandırılmış değer referansları değerlerdir.
R. Martinho Fernandes

emin misin Base fred(::std::move(objptr));değil Base::UPtr fred(::std::move(objptr));mi?
codablank1

1
Önceki yorumuma eklemek için: bu kod derlenmeyecek. std::moveHem kurucu hem de yöntemin uygulanmasında hala kullanmanız gerekir . Ve değere göre geçtiğinizde bile, arayan yine de std::movedeğerlerini iletmek için kullanmalıdır . Aralarındaki en önemli fark, arabirim-değeri ile arabirimin sahipliğini açık hale getirmesidir. Başka bir cevap için Nicol Bolas yorumuna bakın.
R. Martinho Fernandes

@ codablank1: Evet. Oluşturucu ve yöntemleri bazda değer referansları almak nasıl kullanılacağını gösteriyorum.
şeye kadir

@ R.MartinhoFernandes: Oh, ilginç. Sanırım bu mantıklı. Sizden yanlış olmanızı bekliyordum, ama gerçek testler sizi doğruladı. Şimdi düzeltildi.
şeye kadir

0
Base(Base::UPtr n):next(std::move(n)) {}

kadar daha iyi olmalı

Base(Base::UPtr&& n):next(std::forward<Base::UPtr>(n)) {}

ve

void setNext(Base::UPtr n)

olmalı

void setNext(Base::UPtr&& n)

aynı vücut ile.

Ve ... ne evtde handle()??


3
Burada kullanmanın bir kazancı yoktur std::forward: Base::UPtr&&her zaman bir rvalue referans türüdür ve std::movebir rvalue olarak geçirir. Zaten doğru yönlendirilmiş.
R. Martinho Fernandes

7
Kesinlikle katılmıyorum. Bir işlev bir sürerse unique_ptrdeğeriyle, o zaman edilir garantili bir hareket yapıcı yeni değeri denilen o (veya geçici verildi sadece o). Bu olmasını sağlar o unique_ptrkullanıcının sahip değişken artık boş . &&Bunun yerine alırsanız , yalnızca kodunuz bir taşıma işlemi çağırırsa boşaltılır. İstediğiniz şekilde, kullanıcının taşınmamış olması gereken değişken mümkündür. Bu da kullanıcınınstd::move şüpheli ve kafa karıştırıcı kullanımını sağlar. Kullanmak std::moveher zaman bir şeyin taşınmasını sağlamalıdır .
Nicol Bolas

@NicolBolas: Haklısın. Cevabımı sileceğim, çünkü çalışırken, gözleminiz kesinlikle doğru.
şeye kadir

0

En çok oylanan cevap. Rvalue referansı ile geçmeyi tercih ederim.

Rvalue referansından geçmeyle ilgili sorunun ne olabileceğini anlıyorum. Ama bu sorunu iki tarafa ayıralım:

  • arayan için:

Kod yazmalıyım Base newBase(std::move(<lvalue>))veya Base newBase(<rvalue>).

  • callee için:

Kütüphane yazarı, sahibine sahip olmak istiyorsa, üyeyi başlatmak için unique_ptr öğesini taşıyacağını garanti etmelidir.

Bu kadar.

Rvalue referansı ile geçerseniz, yalnızca bir "hareket" talimatını çağırır, ancak değere göre geçiş yaparsanız, bu iki olur.

Evet, kütüphane yazarı bu konuda uzman değilse, üyeyi başlatmak için unique_ptr öğesini taşımayabilir, ancak bu sizin değil yazarın sorunudur. Değer veya değer referansı ile ne olursa olsun, kodunuz aynıdır!

Bir kitaplık yazıyorsanız, artık bunu garanti etmeniz gerektiğini biliyorsunuz, sadece bunu yapın, değer referansından geçmek değerden daha iyi bir seçimdir. Size kitaplığı kullanan istemci sadece aynı kodu yazacaktır.

Şimdi, sorunuz için. Unique_ptr bağımsız değişkenini bir yapıcıya veya işleve nasıl iletirim?

En iyi seçimin ne olduğunu biliyorsun.

http://scottmeyers.blogspot.com/2014/07/should-move-only-types-ever-be-passed.html

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.