如何將unique_ptr參數傳遞給構造函數或函數?

我是C ++ 11中移動語義的新手,我不太清楚如何在構造函數或函數中處理unique_ptr參數。 考慮此類自己的引用: html

#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;

};

這是我應該如何編寫unique_ptr參數的函數嗎? node

我是否須要在調用代碼中使用std::move安全

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

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

#1樓

讓我嘗試說明將指針傳遞給對象的不一樣可行模式,這些對象的內存由std::unique_ptr類模板的實例管理; 它也適用於較舊的std::auto_ptr類模板(我相信容許全部使用該惟一指針的模板,但爲此,在須要rvalue的狀況下,還能夠接受可修改的lvalue,而沒必要調用std::move ),並在某種程度上也爲std::shared_ptrapp

做爲討論的具體示例,我將考慮如下簡單列表類型 ide

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

此類列表的實例(不容許與其餘實例共享零件或爲圓形)徹底由持有初始list指針的人全部。 若是客戶端代碼知道其存儲的列表永遠不會爲空,則還能夠選擇直接存儲第一個node ,而不是list 。 無需爲node定義析構函數:因爲會自動調用其字段的析構函數,所以一旦初始指針或節點的生命週期結束,整個列表將被智能指針析構函數遞歸刪除。 函數

這種遞歸類型使您有機會討論在智能指針指向普通數據的狀況下不可見的某些狀況。 一樣,函數自己有時也(遞歸地)提供客戶端代碼的示例。 list的typedef固然偏向unique_ptr ,可是能夠將定義更改成使用auto_ptrshared_ptr而無需過多更改如下內容(特別是在確保異常安全性而無需編寫析構函數的狀況下)。 ui

傳遞智能指針的方式

模式0:傳遞指針或引用參數而不是智能指針

若是您的函數與全部權無關,那麼這是首選方法:不要使其徹底採用智能指針。 在這種狀況下,您的函數無需擔憂擁有所指向的對象,或擔憂全部權的管理方式,所以傳遞原始指針既安全又是最靈活的形式,由於不管全部權如何,客戶均可以始終產生原始指針(經過調用get方法或從運算符&的地址)。 spa

例如,用於計算此類列表長度的函數不該該提供list參數,而應使用原始指針: 指針

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

擁有變量list head客戶端能夠將此函數稱爲length(head.get()) ,而選擇存儲表明非空列表的node n的客戶端能夠調用length(&n)code

若是保證指針爲非null(此處不是這種狀況,由於列表可能爲空),則可能但願傳遞引用而不是指針。 若是函數須要更新節點的內容而不添加或刪除它們的任何內容(後者將涉及全部權),則它多是指向非const的指針/引用。

屬於模式0類別的一個有趣狀況是製做列表的(深層)副本。 雖然執行此功能的功能固然必須轉移其建立的副本的全部權,但它與正在複製的列表的全部權無關。 所以能夠定義以下:

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

這段代碼值得仔細一看,以解決爲何要編譯的問題(初始化程序列表中對copy進行遞歸調用的結果copy綁定到unique_ptr<node>的移動構造函數aka list的rvalue引用參數中)。初始化生成的nodenext字段時),並詢問爲何它是異常安全的(若是在遞歸分配過程當中內存用完而且對new throw的某些調用std::bad_alloc ,則在那時是一個指針)部分構造列表的名稱被匿名保存在爲初始化列表建立的類型list的臨時列表中,其析構函數將清理該部分列表)。 順便說一下,一我的應該抵制用p替換第二個nullptr的誘惑(就像我最初所作的那樣),畢竟在那一點上它被認爲是null:一我的不能從(raw)指針構造一個常量常量 ,甚至已知爲null時。

模式1:按值傳遞智能指針

以智能指針值做爲參數的函數當即擁有指向的對象:調用方持有的智能指針(不管是在命名變量中仍是匿名臨時變量中)都將被複制到函數入口處的參數值中,而調用方的指針已變爲空(在臨時狀況下,副本可能已被刪除,但在任何狀況下,調用者都沒法訪問指向的對象)。 我想經過現金呼叫此模式:呼叫者爲所調用的服務付費,而且對呼叫後的全部權沒有任何幻想。 爲了明確起見,語言規則要求調用者將智能參數包裝在std::move若是智能指針保存在變量中)(從技術上講,若是參數是左值); 在這種狀況下(但不適用於下面的模式3),此函數將執行其名稱所建議的操做,即將值從變量移到臨時變量,而使變量爲null。

對於被調用函數無條件獲取指向對象的全部權(盜用)的狀況,此模式與std::unique_ptrstd::auto_ptr一塊兒使用是將指針及其全部權傳遞到一塊兒的好方法,這避免了任何風險內存泄漏。 儘管如此,我認爲在不多的狀況下,下面的模式3不會比模式1更受青睞(出於某種緣由)。所以,我將不提供該模式的使用示例。 (可是請參見下面的模式3的reversed示例,其中說明了模式1至少也能夠作到。)若是該函數接受的參數比該指針更多,則可能會發生其餘技術緣由,以免模式1 (使用std::unique_ptrstd::auto_ptr ):因爲實際移動操做是在經過表達式std::move(p)傳遞指針變量p的,所以不能假定p保持有用的值,而評估其餘參數(評估順序未指定),這可能致使細微的錯誤; 相比之下,使用模式3能夠確保在調用函數以前不會發生從p移動的狀況,所以其餘參數能夠安全地經過p訪問值。

std::shared_ptr ,此模式頗有趣,由於它具備單個函數定義,它容許調用者選擇是否爲其自身保留指針的共享副本,同時建立要由該函數使用的新共享副本(此方法發生在提供左值參數時;調用時使用的共享指針的副本構造函數增長了引用計數),或者只是給函數提供了一個指針副本而不保留一個或觸及引用計數(這種狀況在右值參數時發生)提供,多是包裝在std::move調用中的左值。 例如

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
}

經過分別定義void f(const std::shared_ptr<X>& x) (對於左值狀況)和void f(std::shared_ptr<X>&& x) (對於右值狀況),能夠實現相同的效果,函數體的不一樣之處僅在於,第一個版本調用複製語義(使用x時使用複製構造/賦值),而第二個版本移動語義(如示例代碼中那樣,編寫std::move(x) )。 所以,對於共享指針,模式1有助於避免某些代碼重複。

模式2:經過(可修改的)左值引用傳遞智能指針

在這裏,該功能僅須要對智能指針進行可修改的引用,但沒有提供對其功能的指示。 我想經過卡調用此方法:調用者經過提供信用卡號來確保付款。 引用用於獲取指向對象的全部權,但沒必要如此。 此模式須要提供一個可修改的左值參數,這與如下事實有關:函數的指望效果可能包括在參數變量中保留有用的值。 但願傳遞給該函數的帶有右值表達式的調用方將被迫將其存儲在命名變量中,以便可以進行調用,由於該語言僅提供對常量左值引用的隱式轉換(指的是臨時值) )。 (與std::move處理的相反狀況不一樣,從Y&&Y& (使用Y爲智能指針類型是不可能的;儘管如此,若是確實須要此轉換能夠經過簡單的模板函數得到;請參見https:// stackoverflow.com/a/24868376/1436796 )。 對於被調用函數打算無條件地獲取對象全部權(從參數中竊取)的狀況,提供左值參數的義務給出了錯誤的信號:變量在調用後將沒有任何有用的值。 所以,對於這種用法,應該首選模式3,該模式在咱們的函數內部具備相同的可能性,但要求調用者提供一個右值。

可是,模式2有一個有效的用例,便可以修改指針或以涉及全部權的方式指向的對象的函數。 例如,將節點添加到list前綴的函數提供了此類用法的示例:

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

顯然,強制調用者使用std::move是不但願的,由於它們的智能指針在調用以後仍然擁有一個定義良好且非空的列表,儘管與以前的列表不一樣。

一樣有趣的是,觀察因爲缺乏可用內存而致使prepend調用失敗的狀況。 而後new調用將拋出std::bad_alloc ; 在此時間點上,因爲沒法分配node ,所以能夠肯定還沒有對從std::move(l)傳遞的右值引用(模式3)進行竊取,由於這樣作是爲了構造的next字段分配失敗的node 。 所以,當引起錯誤時,原始智能指針l仍然保留原始列表; 該列表將被智能指針析構函數適當地銷燬,或者若是l因爲足夠早的catch子句而得以倖存,它仍將保留原始列表。

那是一個建設性的例子。 對此問題眨眼,您還能夠給出更具破壞性的示例,刪除包含給定值(若是有)的第一個節點:

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); 
}

一樣,這裏的正確性很是微妙。 值得注意的是,在最後一條語句中,要reset節點(保留時)在取消(隱式)破壞以前 ,未連接的指針(*p)->next保持未連接狀態(經過release ,該連接返回指針,但使原始值爲null)。 p )保留的舊值,以確保此時破壞一個節點。 (在註釋中提到的另外一種形式中,此時間將留給std::unique_ptr實例list的move-assignment運算符實現的內部執行;該標準表示20.7.1.2.3; 2該運算符應表現爲「就像經過調用reset(u.release()) 」,所以此處的時間也應該是安全的。)

請注意,存儲始終爲非空列表的本地node變量的客戶端不能調用prependremove_first ,這是正確的,由於給定的實現不適用於此類狀況。

模式3:經過(可修改的)右值引用傳遞智能指針

當簡單地獲取指針全部權時,這是首選的模式。 我想經過支票來調用此方法:調用者必須經過簽署支票來接受放棄全部權,就像提供現金同樣,可是實際提款被推遲到被調用函數實際竊取指針以前(與使用模式2時徹底同樣)。 )。 具體來講,「籤支票」意味着調用者必須在std::move包裝一個參數(如模式1中同樣),若是它是左值(若是是右值),則「放棄全部權」部分是顯而易見的,不須要單獨的代碼)。

請注意,從技術上講,模式3的行爲與模式2的行爲徹底相同,所以被調用的函數沒必要具備全部權。 可是,我堅持認爲,若是在全部權轉移方面存在任何不肯定性(在正常使用狀況下),模式2應優先於模式3,這樣使用模式3隱含地向呼叫者代表他們正在放棄全部權。 有人可能反駁說,只有模式1參數傳遞才真正向調用者發出強制喪失全部權的信號。 可是,若是客戶對被調用函數的意圖有任何疑問,則應該知道她知道被調用函數的規格,這將消除任何疑問。

使人驚訝地很難找到涉及使用模式3參數傳遞的list類型的典型示例。 一個典型的例子是將列表b移到另外一個列表a的末尾。 可是,使用模式2能夠更好地傳遞a (能夠保留並保留操做結果):

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
}

如下是模式3參數傳遞的一個純示例,該示例接受一個列表(及其全部權),並以相反的順序返回包含相同節點的列表。

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;
}

能夠像在l = reversed(std::move(l));那樣調用該函數l = reversed(std::move(l)); 將列表自己反轉,可是反向列表也能夠不一樣地使用。

在這裏,該參數當即移至局部變量以提升效率(能夠直接在p處使用參數l ,可是每次訪問該參數都會涉及一個間接的額外級別); 所以,與模式1參數傳遞的差別很小。 實際上,使用該模式,該參數能夠直接用做局部變量,從而避免了該初始操做。 這只是通常原理的一個實例,即若是經過引用傳遞的參數僅用於初始化局部變量,則也能夠按值傳遞參數並將該參數用做局部變量。

該標準彷佛提倡使用模式3,事實是全部提供的庫函數都使用模式3轉移了智能指針的全部權。一個特別使人信服的例子是構造函數std::shared_ptr<T>(auto_ptr<T>&& p) 。 該構造函數使用(在std::tr1 )獲取可修改的左值引用(就像auto_ptr<T>& copy構造函數同樣),所以能夠像在std::shared_ptr<T> q(p)那樣使用auto_ptr<T>左值p進行調用。 std::shared_ptr<T> q(p) ,以後將p重置爲null。 因爲參數傳遞從模式2更改成3,所以如今必須將此舊代碼重寫爲std::shared_ptr<T> q(std::move(p)) ,而後才能繼續工做。 我知道委員會不喜歡這裏的模式2,可是他們能夠經過定義std::shared_ptr<T>(auto_ptr<T> p)來更改成模式1,他們能夠確保舊代碼有效無需修改,由於(與惟一指針不一樣)自動指針能夠無提示地取消引用值(在過程當中將指針對象自己重置爲null)。 顯然,委員會比模式1更偏心提倡模式3,以致於他們選擇主動破壞現有代碼,而不是即便已經棄用的模式也使用模式1。

何時比模式1更喜歡模式3

模式1在許多狀況下均可以完美使用,而且在假設全部權的狀況下,如上述reversed示例中那樣,將全部權假定爲將智能指針移動到局部變量的形式,則模式1可能比模式3更可取。 可是,在更通常的狀況下,我能夠看到兩個緣由偏心模式3的緣由:

  • 傳遞引用比建立臨時指針和廢除舊指針要有效得多(處理現金有些費力)。 在某些狀況下,在實際竊取指針以前,可能會將指針屢次屢次傳遞給另外一個函數。 這種傳遞一般須要編寫std::move (除非使用模式2),可是請注意,這只是一個強制轉換,實際上不執行任何操做(特別是不進行取消引用),所以其附加成本爲零。

  • 能夠想象,在函數調用的開始和它(或某些包含的調用)的位置之間實際會指向對象的任何對象之間引起任何異常(而且該異常還沒有在函數自己內部捕獲) ),則在使用模式1時,智能指針所引用的對象將在catch子句能夠處理異常以前被銷燬(由於函數參數在堆棧展開時被破壞了),但在使用模式3時則不會被破壞。在這種狀況下,調用方能夠選擇恢復對象的數據(經過捕獲異常)。 請注意,此處的模式1 不會致使內存泄漏 ,但可能致使程序數據沒法恢復,這也是不但願的。

返回智能指針:始終按值

總結一下有關返回智能指針的信息,大概是指向建立供調用者使用的對象。 這與將指針傳遞到函數中的狀況實際上不具備可比性,可是出於完整性考慮,我想堅持認爲在這種狀況下始終按值返回 (而且不要return語句中使用 std::move )。 沒有人但願得到可能剛剛被刪除的指針的引用


#2樓

到最高投票答案。 我更喜歡經過右值引用傳遞。

我瞭解傳遞右值引用可能會致使什麼問題。 但讓咱們將此問題分爲兩個方面:

  • 對於來電者:

我必須編寫代碼Base newBase(std::move(<lvalue>))Base newBase(<rvalue>)

  • 對於被叫方:

庫做者應保證,若是要擁有全部權,它將實際上移動unique_ptr來初始化成員。

就這樣。

若是經過右值引用傳遞,它將僅調用一個「移動」指令,但若是經過值傳遞,則爲兩個。

是的,若是庫做者不是這方面的專家,那麼他可能不會移動unique_ptr來初始化成員,但這是做者的問題,而不是您。 不管經過值或右值引用傳遞什麼,您的代碼都是同樣的!

若是您正在編寫一個庫,如今您知道應該保證它,因此就這樣作,經過右值引用傳遞比值傳遞是更好的選擇。 使用您的庫的客戶端將只編寫相同的代碼。

如今,您的問題。 如何將unique_ptr參數傳遞給構造函數或函數?

您知道什麼是最佳選擇。

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


#3樓

是的,若是您在構造函數中按值獲取unique_ptr ,則必須這樣作。 明確是一件好事。 因爲unique_ptr是不可複製的(私有副本ctor),所以您編寫的內容應該會給您帶來編譯器錯誤。


#4樓

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

應該更好

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

void setNext(Base::UPtr n)

應該

void setNext(Base::UPtr&& n)

具備相同的身體。

還有... handle() evt是什麼?


#5樓

編輯:這個答案是錯誤的,即便嚴格來說,代碼也能夠。 我僅將其留在此處,由於其下的討論太有用了。 這個其餘答案是我上次編輯此內容時給出的最佳答案: 如何將unique_ptr參數傳遞給構造函數或函數?

::std::move的基本思想是,向您傳遞unique_ptr應該使用它來表達這樣的知識,即他們知道所傳遞的unique_ptr將會失去全部權。

這意味着您應該在方法中使用對unique_ptr的右值引用,而不是unique_ptr自己。 不管如何,這將不起做用,由於傳入一個普通的舊的unique_ptr將須要進行復制,而且在unique_ptr的接口中明確禁止這樣作。 有趣的是,使用命名的右值引用會再次將其返回爲左值,所以您還須要方法內部使用::std::move

這意味着您的兩個方法應以下所示:

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

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

而後,使用這些方法的人會這樣作:

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

如您所見, ::std::move表示指針將在最相關且最有幫助的地方失去全部權。 若是發生這種狀況無形中,它會使用你的類對人很是困惑objptr對於沒有顯而易見的緣由忽然失去全部權。

相關文章
相關標籤/搜索