Làm cách nào để truyền đối số unique_ptr cho hàm tạo hoặc hàm?


400

Tôi mới chuyển ngữ nghĩa trong C ++ 11 và tôi không biết rõ cách xử lý unique_ptrcác tham số trong hàm tạo hoặc hàm. Hãy xem xét lớp này tham khảo chính 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;

};

Đây có phải là cách tôi nên viết các hàm lấy unique_ptrđối số?

Và tôi có cần sử dụng std::movemã gọi không?

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

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


1
Đây có phải là lỗi phân đoạn khi bạn gọi b1-> setNext trên một con trỏ trống không?
balki

Câu trả lời:


836

Dưới đây là những cách có thể để lấy một con trỏ duy nhất làm đối số, cũng như ý nghĩa liên quan của chúng.

(A) Theo giá trị

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

Để người dùng gọi điều này, họ phải thực hiện một trong các thao tác sau:

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

Để lấy một con trỏ duy nhất theo giá trị có nghĩa là bạn đang chuyển quyền sở hữu của con trỏ sang hàm / object / etc trong câu hỏi. Sau khi newBaseđược xây dựng, nextBaseđược đảm bảo để trống . Bạn không sở hữu đối tượng và thậm chí bạn không còn có một con trỏ đến nó nữa. No mât rôi.

Điều này được đảm bảo bởi vì chúng tôi lấy tham số theo giá trị. std::movekhông thực sự di chuyển bất cứ điều gì; nó chỉ là một dàn diễn viên lạ mắt. std::move(nextBase)trả về một Base&&tham chiếu giá trị r đến nextBase. Đó là tất cả những gì nó làm.

Base::Base(std::unique_ptr<Base> n)lấy đối số của nó theo giá trị thay vì tham chiếu giá trị r, C ++ sẽ tự động xây dựng tạm thời cho chúng tôi. Nó tạo ra một std::unique_ptr<Base>từ Base&&mà chúng ta đã cho hàm thông qua std::move(nextBase). Chính việc xây dựng tạm thời này thực sự chuyển giá trị từ nextBasethành đối số hàm n.

(B) Bằng tham chiếu giá trị không phải là const

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

Điều này phải được gọi trên một giá trị l thực tế (một biến được đặt tên). Nó không thể được gọi với một tạm thời như thế này:

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

Ý nghĩa của điều này giống như ý nghĩa của bất kỳ việc sử dụng các tham chiếu không phải const nào khác: hàm có thể hoặc không thể xác nhận quyền sở hữu của con trỏ. Cho mã này:

Base newBase(nextBase);

Không có gì đảm bảo nextBaselà trống rỗng. Nó thể trống rỗng; nó có thể không Nó thực sự phụ thuộc vào những gìBase::Base(std::unique_ptr<Base> &n) muốn làm. Do đó, nó không rõ ràng chỉ từ chữ ký hàm những gì sẽ xảy ra; bạn phải đọc việc thực hiện (hoặc tài liệu liên quan).

Do đó, tôi sẽ không đề xuất đây là một giao diện.

(C) Bằng tham chiếu giá trị const l

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

Tôi không hiển thị một triển khai, bởi vì bạn không thể di chuyển từ a const&. Bằng cách chuyển a const&, bạn đang nói rằng hàm có thể truy cập Basethông qua con trỏ, nhưng nó không thể lưu trữ nó ở bất cứ đâu. Nó không thể yêu cầu quyền sở hữu của nó.

Điều này có thể hữu ích. Không nhất thiết cho trường hợp cụ thể của bạn, nhưng thật tốt khi có thể đưa cho ai đó một con trỏ và biết rằng họ không thể (không vi phạm các quy tắc của C ++, giống như không bỏ điconst ) yêu cầu quyền sở hữu của nó. Họ không thể lưu trữ nó. Họ có thể truyền nó cho người khác, nhưng những người khác phải tuân theo các quy tắc tương tự.

(D) Theo tham chiếu giá trị r

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

Điều này ít nhiều giống với trường hợp "bởi tham chiếu giá trị không phải là giá trị l". Sự khác biệt là hai điều.

  1. Bạn có thể vượt qua tạm thời:

    Base newBase(std::unique_ptr<Base>(new Base)); //legal now..
  2. Bạn phải sử dụng std::movekhi truyền các đối số không tạm thời.

Sau này thực sự là vấn đề. Nếu bạn thấy dòng này:

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

Bạn có một kỳ vọng hợp lý rằng, sau khi dòng này hoàn thành, nextBasesẽ trống. Nó đã được di chuyển từ. Rốt cuộc, bạn có std::movengồi đó, nói với bạn rằng chuyển động đã xảy ra.

Vấn đề là nó không có. Nó không được đảm bảo đã được di chuyển từ. Nó có thể đã được chuyển từ, nhưng bạn sẽ chỉ biết bằng cách nhìn vào mã nguồn. Bạn không thể chỉ nói từ chữ ký hàm.

khuyến nghị

  • (A) Theo giá trị: Nếu bạn muốn một chức năng yêu cầu quyền sở hữu của a unique_ptr, hãy lấy nó theo giá trị.
  • (C) Theo tham chiếu giá trị const l: Nếu bạn muốn một hàm chỉ đơn giản là sử dụng unique_ptrthời lượng thực hiện của hàm đó, hãy thực hiện theo const&. Ngoài ra, chuyển một &hoặc const&đến loại thực tế được trỏ đến, thay vì sử dụng a unique_ptr.
  • (D) Theo tham chiếu giá trị r: Nếu một hàm có thể hoặc không thể yêu cầu quyền sở hữu (tùy thuộc vào đường dẫn mã nội bộ), thì hãy thực hiện theo &&. Nhưng tôi mạnh mẽ khuyên không nên làm điều này bất cứ khi nào có thể.

Cách thao tác unique_ptr

Bạn không thể sao chép a unique_ptr. Bạn chỉ có thể di chuyển nó. Cách thích hợp để làm điều này là với std::movechức năng thư viện tiêu chuẩn.

Nếu bạn lấy một unique_ptrgiá trị, bạn có thể di chuyển từ nó một cách tự do. Nhưng phong trào không thực sự xảy ra vì std::move. Lấy tuyên bố sau:

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

Đây thực sự là hai tuyên bố:

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

(lưu ý: Đoạn mã trên không biên dịch về mặt kỹ thuật, vì các tham chiếu giá trị r không tạm thời không thực sự là giá trị r. Nó chỉ ở đây cho mục đích demo).

Đây temporarychỉ là một tài liệu tham khảo giá trị r oldPtr. Đó là trong các nhà xây dựng của newPtrnơi phong trào xảy ra. unique_ptrPhương thức di chuyển của hàm tạo (một hàm tạo có một &&chính nó) là chuyển động thực sự.

Nếu bạn có một unique_ptrgiá trị và bạn muốn lưu trữ nó ở đâu đó, bạn phải sử dụng std::moveđể lưu trữ.


5
@Nicol: nhưng std::movekhông đặt tên giá trị trả về của nó. Hãy nhớ rằng các tham chiếu rvalue được đặt tên là giá trị. ideone.com/VlEM3
R. Martinho Fernandes

31
Tôi cơ bản đồng ý với câu trả lời này, nhưng có một số nhận xét. (1) Tôi không nghĩ có trường hợp sử dụng hợp lệ để chuyển tham chiếu đến const lvalue: mọi thứ mà callee có thể làm với điều đó, nó cũng có thể làm với tham chiếu đến con trỏ const (trần) hoặc thậm chí tốt hơn chính con trỏ [và Không ai biết việc sở hữu được tổ chức thông qua a unique_ptr; có thể một số người gọi khác cần chức năng tương tự nhưng đang giữ một shared_ptrcuộc gọi thay thế] (2) bởi tham chiếu lvalue có thể hữu ích nếu hàm được gọi sửa đổi con trỏ, ví dụ: thêm hoặc xóa các nút (thuộc sở hữu danh sách) khỏi danh sách được liên kết.
Marc van Leeuwen

8
... (3) Mặc dù đối số của bạn ủng hộ chuyển qua giá trị hơn chuyển qua tham chiếu rvalue có ý nghĩa, tôi nghĩ rằng chính tiêu chuẩn luôn vượt qua unique_ptrcác giá trị bằng tham chiếu giá trị (ví dụ: khi chuyển đổi chúng thành shared_ptr). Lý do cho điều đó có thể là nó hiệu quả hơn một chút (không thực hiện chuyển sang con trỏ tạm thời) trong khi nó cung cấp các quyền chính xác tương tự cho người gọi (có thể chuyển các giá trị, hoặc giá trị được bọc std::move, nhưng không phải là giá trị trần).
Marc van Leeuwen

19
Chỉ cần lặp lại những gì Marc đã nói và trích dẫn Sutter : "Đừng sử dụng const unique_ptr & làm tham số; thay vào đó hãy sử dụng widget *"
Jon

17
Chúng tôi đã phát hiện ra một vấn đề với giá trị phụ - việc di chuyển diễn ra trong quá trình khởi tạo đối số, không liên quan đến các đánh giá đối số khác (tất nhiên ngoại trừ trong initizer_list). Trong khi đó, việc chấp nhận một tham chiếu giá trị mạnh mẽ ra lệnh di chuyển xảy ra sau lệnh gọi hàm, và do đó sau khi đánh giá các đối số khác. Vì vậy, chấp nhận tham chiếu giá trị nên được ưu tiên bất cứ khi nào quyền sở hữu sẽ được thực hiện.
Ben Voigt

57

Hãy để tôi thử nêu các chế độ khả thi khác nhau của việc truyền con trỏ xung quanh tới các đối tượng có bộ nhớ được quản lý bởi một thể hiện của std::unique_ptrmẫu lớp; nó cũng áp dụng cho std::auto_ptrkhuôn mẫu lớp cũ hơn (mà tôi tin là cho phép tất cả sử dụng con trỏ duy nhất đó, nhưng ngoài ra, giá trị có thể sửa đổi sẽ được chấp nhận khi giá trị được dự kiến, mà không phải gọi std::move), và trong một chừng mực nào đó std::shared_ptr.

Để làm ví dụ cụ thể cho cuộc thảo luận, tôi sẽ xem xét loại danh sách đơn giản sau đây

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

Các trường hợp của danh sách đó (không thể được phép chia sẻ các phần với các thể hiện khác hoặc là hình tròn) hoàn toàn thuộc sở hữu của bất kỳ ai giữ listcon trỏ ban đầu . Nếu mã khách hàng biết rằng danh sách mà nó lưu trữ sẽ không bao giờ trống, nó cũng có thể chọn lưu trữ nodetrực tiếp đầu tiên thay vì a list. Không có hàm hủy nào nodecần được xác định: vì các hàm hủy cho các trường của nó được gọi tự động, toàn bộ danh sách sẽ bị xóa theo cách đệ quy bởi hàm hủy con trỏ thông minh sau khi vòng đời của con trỏ hoặc nút ban đầu kết thúc.

Kiểu đệ quy này tạo cơ hội để thảo luận về một số trường hợp ít nhìn thấy hơn trong trường hợp con trỏ thông minh đến dữ liệu đơn giản. Ngoài ra, các hàm đôi khi cũng cung cấp (đệ quy) một ví dụ về mã máy khách. Typedef cho listtất nhiên là thiên về hướng unique_ptr, nhưng định nghĩa có thể được thay đổi để sử dụng auto_ptrhoặc shared_ptrthay vào đó mà không cần phải thay đổi nhiều so với những gì được nói dưới đây (đáng chú ý là an toàn ngoại lệ được đảm bảo mà không cần phải viết hàm hủy).

Các chế độ chuyển con trỏ thông minh xung quanh

Chế độ 0: truyền con trỏ hoặc đối số tham chiếu thay vì con trỏ thông minh

Nếu chức năng của bạn không liên quan đến quyền sở hữu, thì đây là phương pháp ưa thích: đừng biến nó thành một con trỏ thông minh. Trong trường hợp này, chức năng của bạn không cần phải lo lắng ai sở hữu đối tượng được chỉ đến, hoặc điều đó có nghĩa là quyền sở hữu được quản lý, do đó, việc chuyển một con trỏ thô vừa an toàn, vừa là hình thức linh hoạt nhất, vì bất kể khách hàng luôn có thể sở hữu tạo ra một con trỏ thô (bằng cách gọi getphương thức hoặc từ địa chỉ của toán tử &).

Chẳng hạn, hàm tính toán độ dài của danh sách đó, không nên đưa ra một listđối số, mà là một con trỏ thô:

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

Một máy khách chứa một biến list headcó thể gọi hàm này là length(head.get()), trong khi một máy khách được chọn thay thế để lưu trữ một node ndanh sách đại diện không trống có thể gọi length(&n).

Nếu con trỏ được đảm bảo là không null (không phải là trường hợp ở đây vì danh sách có thể trống), người ta có thể thích truyền tham chiếu hơn là con trỏ. Nó có thể là một con trỏ / tham chiếu đến non- constnếu hàm cần cập nhật nội dung của (các) nút, mà không cần thêm hoặc xóa bất kỳ cái nào trong số chúng (cái sau sẽ liên quan đến quyền sở hữu).

Một trường hợp thú vị nằm trong danh mục chế độ 0 là tạo một bản sao (sâu) của danh sách; trong khi một chức năng làm điều này tất nhiên phải chuyển quyền sở hữu bản sao mà nó tạo ra, nó không liên quan đến quyền sở hữu của danh sách mà nó đang sao chép. Vì vậy, nó có thể được định nghĩa như sau:

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

Mã này đáng để xem xét kỹ, cả về câu hỏi tại sao nó biên dịch hoàn toàn (kết quả của lệnh gọi đệ quy copytrong danh sách trình khởi tạo liên kết với đối số tham chiếu rvalue trong hàm tạo di chuyển của unique_ptr<node>, hay list, khi khởi tạo nexttrường của được tạo ra node) và cho câu hỏi tại sao nó an toàn ngoại lệ (nếu trong quá trình phân bổ đệ quy hết bộ nhớ và một số lệnh newném std::bad_alloc, thì tại thời điểm đó, một con trỏ tới danh sách được xây dựng một phần được ẩn danh theo kiểu tạm thời listđược tạo cho danh sách khởi tạo và hàm hủy của nó sẽ dọn sạch danh sách một phần đó). Bằng cách này, người ta nên chống lại sự cám dỗ để thay thế (như tôi ban đầu đã làm) lần thứ hainullptr bằngp, mà sau tất cả được biết là null tại thời điểm đó: người ta không thể xây dựng một con trỏ thông minh từ một con trỏ (thô) thành hằng số , ngay cả khi nó được biết là null.

Chế độ 1: vượt qua một con trỏ thông minh theo giá trị

Hàm lấy giá trị con trỏ thông minh làm đối số chiếm hữu đối tượng được trỏ ngay lập tức: con trỏ thông minh mà người gọi giữ (cho dù là biến có tên hoặc tạm thời ẩn danh) được sao chép vào giá trị đối số ở lối vào hàm và trình gọi con trỏ đã trở thành null (trong trường hợp tạm thời, bản sao có thể đã bị xóa, nhưng trong mọi trường hợp, người gọi đã mất quyền truy cập vào đối tượng được trỏ đến). Tôi muốn gọi chế độ này bằng tiền mặt : người gọi trả trước cho dịch vụ được gọi và không thể ảo tưởng về quyền sở hữu sau cuộc gọi. Để làm rõ điều này, các quy tắc ngôn ngữ yêu cầu người gọi bọc đối số trongstd::movenếu con trỏ thông minh được giữ trong một biến (về mặt kỹ thuật, nếu đối số là giá trị); trong trường hợp này (nhưng không phải cho chế độ 3 bên dưới), hàm này thực hiện những gì tên của nó gợi ý, cụ thể là chuyển giá trị từ biến sang tạm thời, để lại biến null.

Đối với các trường hợp hàm được gọi vô điều kiện sở hữu (điều khiển) đối tượng trỏ tới, chế độ này được sử dụng với std::unique_ptrhoặc std::auto_ptrlà một cách tốt để chuyển một con trỏ cùng với quyền sở hữu của nó, tránh mọi nguy cơ rò rỉ bộ nhớ. Tuy nhiên, tôi nghĩ rằng chỉ có rất ít tình huống trong đó chế độ 3 dưới đây không được ưu tiên (bao giờ hơi quá) so với chế độ 1. Vì lý do này, tôi sẽ không cung cấp ví dụ sử dụng nào cho chế độ này. (Nhưng hãy xem reversedví dụ về chế độ 3 bên dưới, trong đó nhận xét rằng chế độ 1 cũng sẽ làm ít nhất.) Nếu hàm có nhiều đối số hơn chỉ con trỏ này, có thể có thêm lý do kỹ thuật để tránh chế độ 1 (với std::unique_ptrhoặc std::auto_ptr): do một hoạt động di chuyển thực tế diễn ra trong khi truyền một biến con trỏpbằng biểu thức std::move(p), không thể giả định rằngpgiữ một giá trị hữu ích trong khi đánh giá các đối số khác (thứ tự đánh giá không được chỉ định), điều này có thể dẫn đến các lỗi tinh vi; ngược lại, sử dụng chế độ 3 đảm bảo rằng không có sự di chuyển nào pdiễn ra trước khi gọi hàm, vì vậy các đối số khác có thể truy cập một cách an toàn một giá trị thông qua p.

Khi được sử dụng std::shared_ptr, chế độ này thú vị ở chỗ với một định nghĩa hàm duy nhất, nó cho phép người gọi chọn giữ bản sao chia sẻ của con trỏ trong khi tạo một bản sao chia sẻ mới được sử dụng bởi hàm (điều này xảy ra khi một giá trị đối số được cung cấp, hàm tạo sao chép cho các con trỏ dùng chung được sử dụng trong cuộc gọi làm tăng số tham chiếu) hoặc chỉ cung cấp cho hàm một bản sao của con trỏ mà không giữ lại một hoặc chạm vào số tham chiếu (điều này xảy ra khi có thể cung cấp đối số giá trị một giá trị được bọc trong một cuộc gọi của std::move). Ví dụ

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
}

Điều tương tự có thể đạt được bằng cách định nghĩa riêng void f(const std::shared_ptr<X>& x)(đối với trường hợp giá trị) và void f(std::shared_ptr<X>&& x)(đối với trường hợp giá trị ), với các cơ quan chức năng chỉ khác nhau ở phiên bản đầu tiên gọi ngữ nghĩa sao chép (sử dụng xây dựng / gán sao chép khi sử dụng x) nhưng phiên bản thứ hai di chuyển ngữ nghĩa (viết std::move(x)thay thế, như trong mã ví dụ). Vì vậy, đối với các con trỏ được chia sẻ, chế độ 1 có thể hữu ích để tránh một số sao chép mã.

Chế độ 2: vượt qua một con trỏ thông minh bằng cách tham chiếu giá trị (có thể sửa đổi)

Ở đây, hàm chỉ yêu cầu có một tham chiếu có thể sửa đổi đối với con trỏ thông minh, nhưng không đưa ra dấu hiệu nào về việc nó sẽ làm gì với nó. Tôi muốn gọi phương thức này bằng thẻ : người gọi đảm bảo thanh toán bằng cách cho số thẻ tín dụng. Tham chiếu có thể được sử dụng để sở hữu đối tượng trỏ tới, nhưng nó không phải. Chế độ này yêu cầu cung cấp một đối số giá trị có thể sửa đổi, tương ứng với thực tế là hiệu ứng mong muốn của hàm có thể bao gồm việc để lại một giá trị hữu ích trong biến đối số. Một người gọi với biểu thức giá trị mà nó muốn chuyển đến một hàm như vậy sẽ bị buộc phải lưu nó trong một biến được đặt tên để có thể thực hiện cuộc gọi, vì ngôn ngữ chỉ cung cấp chuyển đổi ngầm định thành hằng sốtham chiếu lvalue (đề cập đến tạm thời) từ một giá trị. (Không giống như trường hợp ngược lại được xử lý bởi std::move, việc truyền từ Y&&sang Y&, với Yloại con trỏ thông minh, là không thể; dù sao, chuyển đổi này có thể có được bằng một hàm mẫu đơn giản nếu thực sự mong muốn; xem https://stackoverflow.com/a/24868376 / 1436796 ). Đối với trường hợp hàm được gọi có ý định vô điều kiện sở hữu đối tượng, đánh cắp đối số, nghĩa vụ cung cấp đối số lvalue đưa ra tín hiệu sai: biến sẽ không có giá trị hữu ích sau cuộc gọi. Do đó, chế độ 3, cung cấp các khả năng giống hệt nhau bên trong chức năng của chúng tôi nhưng yêu cầu người gọi cung cấp một giá trị, nên được ưu tiên cho việc sử dụng đó.

Tuy nhiên, có một trường hợp sử dụng hợp lệ cho chế độ 2, cụ thể là các hàm có thể sửa đổi con trỏ hoặc đối tượng được trỏ theo cách liên quan đến quyền sở hữu . Chẳng hạn, một hàm có tiền tố một nút listcung cấp một ví dụ về việc sử dụng đó:

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

Rõ ràng ở đây sẽ là điều không mong muốn khi buộc người gọi sử dụng std::move, vì con trỏ thông minh của họ vẫn sở hữu một danh sách được xác định rõ và không trống sau cuộc gọi, mặc dù khác với trước.

Một lần nữa thật thú vị khi quan sát những gì xảy ra nếu prependcuộc gọi thất bại vì thiếu bộ nhớ trống. Rồi newcuộc gọi sẽ ném std::bad_alloc; tại thời điểm này, vì không nodethể được phân bổ, nên chắc chắn rằng tham chiếu giá trị đã qua (chế độ 3) từ std::move(l)chưa thể được điều chỉnh, vì điều đó sẽ được thực hiện để xây dựng nexttrường của trường nodekhông được phân bổ. Vì vậy, con trỏ thông minh ban đầu lvẫn giữ danh sách ban đầu khi lỗi được ném; danh sách đó sẽ bị hủy bởi bộ hủy con trỏ thông minh hoặc trong trường hợp lnên tồn tại nhờ vào một catchmệnh đề đủ sớm , nó vẫn sẽ giữ danh sách ban đầu.

Đó là một ví dụ mang tính xây dựng; với một cái nháy mắt cho câu hỏi này, người ta cũng có thể đưa ra ví dụ phá hủy hơn về việc loại bỏ nút đầu tiên có chứa một giá trị nhất định, nếu có:

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

Một lần nữa sự đúng đắn là khá tinh tế ở đây. Đáng chú ý, trong câu lệnh cuối cùng, con trỏ (*p)->nextđược giữ bên trong nút cần loại bỏ sẽ không được liên kết (bởi release, nó trả về con trỏ nhưng tạo null ban đầu) trước khi reset (ngầm) phá hủy nút đó (khi nó phá hủy giá trị cũ được giữ bởi p), đảm bảo rằng một và chỉ một nút bị phá hủy tại thời điểm đó. (Trong hình thức thay thế được đề cập trong bình luận, thời gian này sẽ được để lại cho bên trong việc thực hiện toán tử gán chuyển động của std::unique_ptrthể hiện list; tiêu chuẩn nói 20.7.1.2.3; 2 rằng toán tử này sẽ hành động "như thể bởi đang gọi reset(u.release())", từ đó thời gian cũng sẽ an toàn.)

Lưu ý rằng prependremove_firstkhông thể được gọi bởi các khách hàng lưu trữ một nodebiến cục bộ cho một danh sách luôn trống và đúng vì vậy các triển khai đưa ra không thể hoạt động cho các trường hợp như vậy.

Chế độ 3: vượt qua một con trỏ thông minh bằng cách tham chiếu giá trị (có thể sửa đổi)

Đây là chế độ ưa thích để sử dụng khi chỉ cần sở hữu con trỏ. Tôi muốn gọi phương thức này bằng séc : người gọi phải chấp nhận từ bỏ quyền sở hữu, như thể cung cấp tiền mặt, bằng cách ký séc, nhưng việc rút tiền thực tế bị hoãn cho đến khi chức năng được gọi thực sự điều khiển con trỏ (chính xác như khi sử dụng chế độ 2 ). Việc "ký séc" một cách cụ thể có nghĩa là người gọi phải bọc một đối số trong std::move(như trong chế độ 1) nếu đó là một giá trị (nếu đó là một giá trị, phần "từ bỏ quyền sở hữu" là rõ ràng và không yêu cầu mã riêng).

Lưu ý rằng về mặt kỹ thuật chế độ 3 hoạt động chính xác như chế độ 2, do đó, chức năng được gọi không phải đảm nhận quyền sở hữu; tuy nhiên tôi sẽ nhấn mạnh rằng nếu có bất kỳ sự không chắc chắn nào về chuyển quyền sở hữu (trong sử dụng bình thường), chế độ 2 nên được ưu tiên hơn cho chế độ 3, do đó, việc sử dụng chế độ 3 là một tín hiệu cho người gọi rằng họ đang từ bỏ quyền sở hữu. Người ta có thể vặn lại rằng chỉ có đối số chế độ 1 chuyển qua thực sự báo hiệu việc mất quyền sở hữu đối với người gọi. Nhưng nếu một khách hàng có bất kỳ nghi ngờ nào về ý định của hàm được gọi, thì cô ấy có nghĩa vụ phải biết các thông số kỹ thuật của hàm được gọi, điều này sẽ loại bỏ mọi nghi ngờ.

Thật đáng ngạc nhiên khi tìm thấy một ví dụ điển hình liên quan đến listloại của chúng tôi sử dụng đối số chế độ 3 đi qua. Di chuyển một danh sách bđến cuối danh sách khác alà một ví dụ điển hình; tuy nhiên a(tồn tại và giữ kết quả của hoạt động) sẽ tốt hơn khi sử dụng chế độ 2:

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
}

Một ví dụ thuần túy về việc truyền đối số chế độ 3 là sau đây lấy một danh sách (và quyền sở hữu của nó) và trả về một danh sách chứa các nút giống hệt nhau theo thứ tự ngược lại.

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

Hàm này có thể được gọi là trong l = reversed(std::move(l));để đảo ngược danh sách thành chính nó, nhưng danh sách đảo ngược cũng có thể được sử dụng khác nhau.

Ở đây, đối số ngay lập tức được chuyển đến một biến cục bộ để đạt hiệu quả (người ta có thể đã sử dụng tham số ltrực tiếp ở vị trí của nó p, nhưng sau đó truy cập vào nó mỗi lần sẽ liên quan đến một mức độ gián tiếp bổ sung); do đó sự khác biệt với việc truyền đối số mode 1 là tối thiểu. Trong thực tế sử dụng chế độ đó, đối số có thể đã phục vụ trực tiếp dưới dạng biến cục bộ, do đó tránh được động thái ban đầu đó; đây chỉ là một ví dụ của nguyên tắc chung rằng nếu một đối số được truyền bởi tham chiếu chỉ phục vụ để khởi tạo một biến cục bộ, thì người ta cũng có thể chuyển nó theo giá trị thay vào đó và sử dụng tham số làm biến cục bộ.

Sử dụng chế độ 3 dường như được ủng hộ bởi tiêu chuẩn, như được chứng kiến ​​bởi thực tế là tất cả các chức năng thư viện được cung cấp chuyển quyền sở hữu con trỏ thông minh bằng chế độ 3. Một trường hợp thuyết phục cụ thể là nhà xây dựng std::shared_ptr<T>(auto_ptr<T>&& p). Hàm tạo đó đã sử dụng (in std::tr1) để lấy tham chiếu giá trị có thể sửa đổi (giống như hàm tạo auto_ptr<T>&sao chép), và do đó có thể được gọi với một auto_ptr<T>giá trị pnhư std::shared_ptr<T> q(p)sau, sau đó pđã được đặt lại thành null. Do sự thay đổi từ chế độ 2 thành 3 trong việc truyền đối số, mã cũ này hiện phải được viết lại std::shared_ptr<T> q(std::move(p))và sau đó sẽ tiếp tục hoạt động. Tôi hiểu rằng ủy ban không thích chế độ 2 ở đây, nhưng họ có tùy chọn thay đổi sang chế độ 1, bằng cách xác địnhstd::shared_ptr<T>(auto_ptr<T> p)thay vào đó, họ có thể đảm bảo rằng mã cũ hoạt động mà không cần sửa đổi, bởi vì (không giống như các con trỏ duy nhất) có thể được âm thầm quy định thành một giá trị (chính đối tượng con trỏ được đặt lại thành null trong quá trình). Rõ ràng ủy ban rất thích ủng hộ chế độ 3 hơn chế độ 1, đến mức họ chọn chủ động phá vỡ mã hiện có thay vì sử dụng chế độ 1 ngay cả đối với việc sử dụng đã bị phản đối.

Khi nào thích chế độ 3 hơn chế độ 1

Chế độ 1 hoàn toàn có thể sử dụng được trong nhiều trường hợp và có thể được ưu tiên hơn chế độ 3 trong trường hợp giả sử quyền sở hữu sẽ có hình thức di chuyển con trỏ thông minh sang một biến cục bộ như trong reversedví dụ trên. Tuy nhiên, tôi có thể thấy hai lý do để thích chế độ 3 trong trường hợp tổng quát hơn:

  • Việc chuyển một tham chiếu hiệu quả hơn một chút so với việc tạo một con trỏ cũ và tạm thời con trỏ cũ (xử lý tiền mặt có phần tốn công sức); trong một số trường hợp, con trỏ có thể được chuyển không thay đổi nhiều lần sang hàm khác trước khi nó thực sự được điều khiển. Việc vượt qua như vậy thường sẽ yêu cầu viết std::move(trừ khi sử dụng chế độ 2), nhưng lưu ý rằng đây chỉ là một diễn viên không thực sự làm gì cả (đặc biệt là không có hội thảo), vì vậy nó không có chi phí kèm theo.

  • Có thể hiểu được rằng bất cứ điều gì ném một ngoại lệ giữa lúc bắt đầu cuộc gọi hàm và điểm mà nó (hoặc một số cuộc gọi có chứa) thực sự di chuyển đối tượng trỏ vào một cấu trúc dữ liệu khác (và ngoại lệ này chưa bị bắt trong chính hàm đó ), sau đó khi sử dụng chế độ 1, đối tượng được gọi bởi con trỏ thông minh sẽ bị hủy trước khi catchmệnh đề có thể xử lý ngoại lệ (vì tham số hàm bị hủy trong quá trình hủy bỏ ngăn xếp), nhưng không phải vậy khi sử dụng chế độ 3. Cái sau cho người gọi có tùy chọn khôi phục dữ liệu của đối tượng trong các trường hợp đó (bằng cách bắt ngoại lệ). Lưu ý rằng chế độ 1 ở đây không gây rò rỉ bộ nhớ , nhưng có thể dẫn đến việc mất dữ liệu không thể phục hồi cho chương trình, điều này cũng có thể không mong muốn.

Trả về một con trỏ thông minh: luôn luôn theo giá trị

Để kết luận một từ về việc trả về một con trỏ thông minh, có lẽ chỉ vào một đối tượng được tạo bởi người gọi. Đây thực sự không phải là một trường hợp có thể so sánh với việc chuyển con trỏ vào các hàm, nhưng để hoàn chỉnh tôi muốn nhấn mạnh rằng trong những trường hợp như vậy luôn trả về giá trị (và không sử dụng std::move trong returncâu lệnh). Không ai muốn có được một tham chiếu đến một con trỏ có lẽ vừa được trộn.


1
+1 cho Chế độ 0 - chuyển con trỏ bên dưới thay vì unique_ptr. Hơi lạc đề (vì câu hỏi là về việc chuyển một unique_ptr) nhưng nó đơn giản và tránh được các vấn đề.
Machta

" Chế độ 1 ở đây không gây rò rỉ bộ nhớ " - ngụ ý rằng chế độ 3 không gây rò rỉ bộ nhớ, điều này không đúng. Bất kể unique_ptrcó được di chuyển từ hay không, nó vẫn sẽ xóa giá trị một cách độc đáo nếu nó vẫn giữ nó bất cứ khi nào bị phá hủy hoặc tái sử dụng.
rustyx

@RustyX: Tôi không thể thấy cách bạn hiểu hàm ý đó và tôi không bao giờ có ý định nói những gì bạn nghĩ tôi ngụ ý. Tất cả những gì tôi muốn nói là ở nơi khác, việc sử dụng unique_ptrngăn chặn rò rỉ bộ nhớ (và do đó theo nghĩa hoàn thành hợp đồng của nó), nhưng ở đây (tức là sử dụng chế độ 1), nó có thể gây ra (trong những trường hợp cụ thể) một thứ có thể được coi là có hại hơn , cụ thể là mất dữ liệu (phá hủy giá trị được chỉ ra) có thể tránh được bằng cách sử dụng chế độ 3.
Marc van Leeuwen

4

Có bạn phải làm nếu bạn lấy unique_ptrgiá trị trong hàm tạo. Tự do là một điều tốt đẹp. Vì không thể unique_ptrquét được (ctor sao chép riêng), những gì bạn đã viết sẽ cung cấp cho bạn một lỗi biên dịch.


3

Chỉnh sửa: Câu trả lời này là sai, mặc dù, nói đúng ra, mã hoạt động. Tôi chỉ để nó ở đây vì cuộc thảo luận dưới đây quá hữu ích. Câu trả lời khác này là câu trả lời tốt nhất được đưa ra tại thời điểm tôi chỉnh sửa lần cuối: Tôi làm cách nào để chuyển một đối số unique_ptr cho hàm tạo hoặc hàm?

Ý tưởng cơ bản ::std::movelà những người đi ngang qua bạn unique_ptrnên sử dụng nó để thể hiện kiến ​​thức mà họ biết rằng unique_ptrhọ đang đi vào sẽ mất quyền sở hữu.

Điều này có nghĩa là bạn nên sử dụng một tham chiếu giá trị cho một unique_ptrtrong các phương thức của bạn chứ không phải unique_ptrchính nó. Điều này sẽ không hoạt động vì dù sao đi qua một bản cũ unique_ptrsẽ yêu cầu tạo một bản sao và điều đó rõ ràng bị cấm trong giao diện unique_ptr. Thật thú vị, bằng cách sử dụng một tham chiếu rvalue có tên sẽ biến nó trở lại thành một giá trị một lần nữa, vì vậy bạn cũng cần sử dụng ::std::move bên trong các phương thức của mình.

Điều này có nghĩa là hai phương thức của bạn sẽ trông như thế này:

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

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

Sau đó, mọi người sử dụng các phương thức sẽ làm điều này:

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

Như bạn thấy, ::std::movecon trỏ biểu thị rằng con trỏ sẽ mất quyền sở hữu tại thời điểm mà nó phù hợp và hữu ích nhất để biết. Nếu điều này xảy ra vô hình, sẽ rất khó hiểu khi những người sử dụng lớp của bạn objptrđột nhiên mất quyền sở hữu mà không có lý do rõ ràng.


2
Tài liệu tham khảo rvalue được đặt tên là giá trị.
R. Martinho Fernandes

bạn có chắc chắn Base fred(::std::move(objptr));và không Base::UPtr fred(::std::move(objptr));?
codableank1

1
Để thêm vào nhận xét trước đây của tôi: mã này sẽ không được biên dịch. Bạn vẫn cần sử dụng std::movetrong việc thực hiện cả hàm tạo và phương thức. Và ngay cả khi bạn vượt qua giá trị, người gọi vẫn phải sử dụng std::moveđể vượt qua giá trị. Sự khác biệt chính là với giao diện truyền qua giá trị đó làm cho quyền sở hữu rõ ràng sẽ bị mất. Xem bình luận của Nicol Bolas về một câu trả lời khác.
R. Martinho Fernandes

@ codableank1: Có. Tôi đang trình bày cách sử dụng hàm tạo và các phương thức trong cơ sở lấy tham chiếu giá trị.
Omnifarious

@ R.MartinhoFernandes: Ồ, thật thú vị. Tôi cho rằng nó có ý nghĩa. Tôi đã mong bạn sai, nhưng thử nghiệm thực tế đã chứng minh bạn đúng. Đã sửa bây giờ.
Omnifarious

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

nên tốt hơn nhiều

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

void setNext(Base::UPtr n)

nên là

void setNext(Base::UPtr&& n)

với cùng một cơ thể.

Và ... có gì evttrong handle()??


3
Không có lợi ích trong việc sử dụng std::forwardở đây: Base::UPtr&&luôn luôn một loại tài liệu tham khảo rvalue, và std::movevượt qua nó như một rvalue. Nó đã được chuyển tiếp chính xác.
R. Martinho Fernandes

7
Tôi rất không đồng ý. Nếu một hàm lấy một unique_ptrgiá trị, thì bạn được đảm bảo rằng hàm tạo di chuyển được gọi trên giá trị mới (hoặc đơn giản là bạn đã được cung cấp tạm thời). Điều này đảm bảo rằng unique_ptrbiến người dùng hiện đang trống . Nếu bạn &&thay thế nó, nó sẽ chỉ bị xóa nếu mã của bạn gọi một thao tác di chuyển. Theo cách của bạn, có thể là biến mà người dùng không được di chuyển từ đó. Điều này làm cho người dùng sử dụng std::movenghi ngờ và khó hiểu. Sử dụng std::movephải luôn đảm bảo rằng một cái gì đó đã được di chuyển .
Nicol Bolas

@NicolBolas: Bạn nói đúng. Tôi sẽ xóa câu trả lời của mình vì trong khi nó hoạt động, quan sát của bạn là hoàn toàn chính xác.
Omnifarious

0

Để câu trả lời bình chọn hàng đầu. Tôi thích đi qua tham chiếu rvalue.

Tôi hiểu những gì vấn đề về việc chuyển qua tham chiếu giá trị có thể gây ra. Nhưng hãy chia vấn đề này ra hai bên:

  • cho người gọi:

Tôi phải viết mã Base newBase(std::move(<lvalue>))hoặc Base newBase(<rvalue>).

  • cho callee:

Tác giả thư viện cần đảm bảo rằng nó thực sự sẽ di chuyển unique_ptr để khởi tạo thành viên nếu muốn sở hữu quyền sở hữu.

Đó là tất cả.

Nếu bạn chuyển qua tham chiếu giá trị, nó sẽ chỉ gọi một lệnh "di chuyển", nhưng nếu vượt qua giá trị, thì đó là hai.

Đúng, nếu tác giả thư viện không phải là chuyên gia về vấn đề này, anh ta có thể không di chuyển unique_ptr để khởi tạo thành viên, nhưng đó là vấn đề của tác giả, không phải bạn. Bất cứ điều gì nó vượt qua bởi giá trị hoặc tham chiếu giá trị, mã của bạn đều giống nhau!

Nếu bạn đang viết một thư viện, bây giờ bạn biết bạn nên đảm bảo nó, vì vậy chỉ cần làm điều đó, đi qua tham chiếu rvalue là một lựa chọn tốt hơn so với giá trị. Khách hàng sử dụng thư viện của bạn sẽ chỉ viết cùng một mã.

Bây giờ, cho câu hỏi của bạn. Làm cách nào để truyền đối số unique_ptr cho hàm tạo hoặc hàm?

Bạn biết đâu là sự lựa chọn tốt nhất.

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

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.