Trả về unique_ptr từ các hàm


367

unique_ptr<T>không cho phép xây dựng bản sao, thay vào đó nó hỗ trợ di chuyển ngữ nghĩa. Tuy nhiên, tôi có thể trả về a unique_ptr<T>từ một hàm và gán giá trị được trả về cho một biến.

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

Các mã trên biên dịch và hoạt động như dự định. Vậy làm thế nào mà dòng 1đó không gọi được hàm tạo sao chép và dẫn đến lỗi trình biên dịch? Nếu tôi phải sử dụng dòng 2thay vì nó sẽ có ý nghĩa (sử dụng dòng cũng 2hoạt động, nhưng chúng tôi không bắt buộc phải làm như vậy).

Tôi biết C ++ 0x cho phép ngoại lệ này unique_ptrvì giá trị trả về là một đối tượng tạm thời sẽ bị hủy ngay khi hàm thoát, do đó đảm bảo tính duy nhất của con trỏ được trả về. Tôi tò mò về cách thực hiện điều này, nó có đặc biệt trong trình biên dịch hay có một số mệnh đề khác trong đặc tả ngôn ngữ mà điều này khai thác không?


Theo giả thuyết, nếu bạn đang thực hiện một phương pháp của nhà máy , bạn sẽ thích 1 hoặc 2 để trả lại sản lượng của nhà máy? Tôi cho rằng đây sẽ là cách sử dụng phổ biến nhất của 1 bởi vì, với một nhà máy phù hợp, bạn thực sự muốn quyền sở hữu của vật được xây dựng để chuyển cho người gọi.
Xharlie

7
@Xharlie? Cả hai đều vượt qua quyền sở hữu của unique_ptr. Toàn bộ câu hỏi là về 1 và 2 là hai cách khác nhau để đạt được cùng một điều.
Praetorian

trong trường hợp này, RVO cũng diễn ra trong c ++ 0x, việc phá hủy đối tượng unique_ptr sẽ được thực hiện một lần sau khi mainthoát khỏi chức năng, nhưng không xảy ra khi foothoát.
ampawd

Câu trả lời:


218

Có một số điều khoản khác trong đặc tả ngôn ngữ mà điều này khai thác?

Có, xem 12.8 §34 và §35:

Khi một số tiêu chí nhất định được đáp ứng, việc triển khai được phép bỏ qua việc xây dựng bản sao / di chuyển của một đối tượng lớp [...] Việc loại bỏ các hoạt động sao chép / di chuyển này, được gọi là bản sao bản sao , được cho phép [...] trong một tuyên bố trả về trong một hàm có kiểu trả về lớp, khi biểu thức là tên của một đối tượng tự động không bay hơi có cùng loại cv không đủ tiêu chuẩn như kiểu trả về hàm [...]

Khi các tiêu chí loại bỏ hoạt động sao chép được đáp ứng và đối tượng được sao chép được chỉ định bởi một giá trị, độ phân giải quá tải để chọn hàm tạo cho bản sao được thực hiện trước tiên như thể đối tượng được chỉ định bởi một giá trị .


Chỉ muốn thêm một điểm nữa là trả về theo giá trị nên là lựa chọn mặc định ở đây vì một giá trị được đặt tên trong câu lệnh return trong trường hợp xấu nhất, nghĩa là không có sự thay đổi trong C ++ 11, C ++ 14 và C ++ 17 được xử lý như một giá trị. Vì vậy, ví dụ hàm sau sẽ biên dịch với -fno-elide-constructorscờ

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

Với cờ được thiết lập trên quá trình biên dịch, có hai di chuyển (1 và 2) xảy ra trong chức năng này và sau đó một di chuyển sau đó (3).


@juanchopanza Về cơ bản, bạn có nghĩa là nó foo()thực sự cũng sắp bị phá hủy (nếu nó không được gán cho bất cứ thứ gì), giống như giá trị trả về trong hàm, và do đó, có nghĩa là C ++ sử dụng hàm tạo di chuyển khi thực hiện unique_ptr<int> p = foo();?
7cows

1
Câu trả lời này nói rằng việc triển khai được phép làm một cái gì đó ... nó không nói là phải, vì vậy nếu đây là phần duy nhất có liên quan, điều đó có nghĩa là dựa vào hành vi này không phải là di động. Nhưng tôi không nghĩ điều đó đúng. Tôi có khuynh hướng nghĩ rằng câu trả lời đúng có liên quan nhiều hơn đến nhà xây dựng di chuyển, như được mô tả trong câu trả lời của Nikola Smiljanic và Bartosz Milewski.
Don nở

6
@DonHatch Nó nói rằng "được phép" thực hiện sao chép / di chuyển cuộc bầu cử trong những trường hợp đó, nhưng chúng tôi không nói về cuộc bầu cử sao chép ở đây. Đây là đoạn trích dẫn thứ hai áp dụng ở đây, đoạn trích này dựa trên các quy tắc bầu cử sao chép, nhưng không phải là bản sao cuộc bầu cử. Không có sự không chắc chắn trong đoạn thứ hai - nó hoàn toàn di động.
Joseph Mansfield

@juanchopanza Tôi nhận ra điều này bây giờ là 2 năm sau, nhưng bạn vẫn cảm thấy điều này là sai? Như tôi đã đề cập trong các bình luận trước đây, đây không phải là về cuộc bầu cử sao chép. Nó chỉ xảy ra rằng trong các trường hợp có thể áp dụng phương pháp sao chép (ngay cả khi không thể áp dụng std::unique_ptr), có một quy tắc đặc biệt để trước tiên coi các đối tượng là giá trị. Tôi nghĩ điều này hoàn toàn đồng ý với những gì Nikola đã trả lời.
Joseph Mansfield

1
Vậy tại sao tôi vẫn gặp lỗi "cố gắng tham chiếu hàm bị xóa" cho loại chỉ di chuyển (hàm tạo sao chép bị loại bỏ) khi trả lại chính xác theo cách tương tự như ví dụ này?
DrumM

104

Điều này là không có cách cụ thể std::unique_ptr, nhưng áp dụng cho bất kỳ lớp nào có thể di chuyển. Nó được đảm bảo bởi các quy tắc ngôn ngữ vì bạn đang trả về theo giá trị. Trình biên dịch cố gắng tách các bản sao, gọi một hàm tạo di chuyển nếu nó không thể xóa các bản sao, gọi một hàm tạo sao chép nếu nó không thể di chuyển và không biên dịch nếu nó không thể sao chép.

Nếu bạn có một hàm chấp nhận std::unique_ptrlàm đối số, bạn sẽ không thể truyền p cho nó. Bạn sẽ phải gọi hàm tạo di chuyển một cách rõ ràng, nhưng trong trường hợp này bạn không nên sử dụng biến p sau khi gọi đến 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 - tốt, không thực sự. Mặc dù pkhông phải là tạm thời, kết quả của foo(), những gì được trả lại, là; do đó, nó là một giá trị và có thể được di chuyển, điều này làm cho việc chuyển nhượng maincó thể thực hiện được. Tôi muốn nói rằng bạn đã sai ngoại trừ Nikola sau đó dường như áp dụng quy tắc này cho pchính IS có lỗi.
Edward Strange

Chính xác những gì tôi muốn nói, nhưng không thể tìm thấy các từ. Tôi đã xóa phần đó của câu trả lời vì nó không rõ ràng lắm.
Nikola Smilćć

Tôi có một câu hỏi: trong câu hỏi ban đầu, có sự khác biệt đáng kể nào giữa Line 1và Line 2không? Theo quan điểm của tôi nó giống kể từ khi xây dựng ptrong main, nó chỉ quan tâm về loại kiểu trả về của foo, phải không?
Hongxu Chen

1
@HongxuChen Trong ví dụ đó hoàn toàn không có sự khác biệt, hãy xem trích dẫn từ tiêu chuẩn trong câu trả lời được chấp nhận.
Nikola Smil camerać

Trên thực tế, bạn có thể sử dụng p sau đó, miễn là bạn gán cho nó. Cho đến lúc đó, bạn không thể cố gắng tham khảo nội dung.
Alan

38

unique_ptr không có hàm tạo sao chép truyền thống. Thay vào đó, nó có một "hàm tạo di chuyển" sử dụng các tham chiếu rvalue:

unique_ptr::unique_ptr(unique_ptr && src);

Một tham chiếu rvalue (ampersand kép) sẽ chỉ liên kết với một giá trị. Đó là lý do tại sao bạn gặp lỗi khi bạn cố gắng truyền một giá trị duy nhất_ptr cho một hàm. Mặt khác, một giá trị được trả về từ một hàm được coi là một giá trị, do đó, hàm tạo di chuyển được gọi tự động.

Nhân tiện, điều này sẽ hoạt động chính xác:

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

Unique_ptr tạm thời ở đây là một giá trị.


8
Tôi nghĩ vấn đề là nhiều, tại sao có thể p- "rõ ràng" một vế trái - được coi là một rvalue trong câu lệnh return return p;trong định nghĩa của foo. Tôi không nghĩ có bất kỳ vấn đề nào với thực tế là giá trị trả về của chính hàm có thể bị "dịch chuyển".
CB Bailey

Liệu gói giá trị được trả về từ hàm trong std :: move có nghĩa là nó sẽ được di chuyển hai lần?

3
@RodrigoSalazar std :: move chỉ là một kiểu đúc lạ mắt từ tham chiếu giá trị (&) sang tham chiếu giá trị (&&). Việc sử dụng std :: di chuyển trên một tham chiếu giá trị đơn giản sẽ chỉ là một noop
TiMoch

13

Tôi nghĩ rằng nó được giải thích hoàn hảo trong mục 25 của C ++ Modern Modern C ++ hiệu quả của Scott Meyers . Đây là một đoạn trích:

Một phần của Tiêu chuẩn ban phước cho RVO tiếp tục nói rằng nếu các điều kiện cho RVO được đáp ứng, nhưng các trình biên dịch chọn không thực hiện việc bỏ phiếu sao chép, đối tượng được trả về phải được coi là một giá trị. Trên thực tế, Tiêu chuẩn yêu cầu rằng khi RVO được cho phép, thì cuộc bầu chọn sao chép sẽ diễn ra hoặc std::moveđược áp dụng ngầm cho các đối tượng cục bộ được trả về.

Ở đây, RVO đề cập đến tối ưu hóa giá trị trả vềnếu đáp ứng các điều kiện cho RVO có nghĩa là trả về đối tượng cục bộ được khai báo bên trong hàm mà bạn mong đợi để thực hiện RVO , điều này cũng được giải thích độc đáo trong mục 25 của cuốn sách bằng cách tham khảo tiêu chuẩn (ở đây đối tượng cục bộ bao gồm các đối tượng tạm thời được tạo bởi câu lệnh return). Điểm trừ lớn nhất từ ​​đoạn trích là cuộc bầu cử sao chép diễn ra hoặc std::moveđược áp dụng ngầm cho các đối tượng địa phương được trả lại . Scott đề cập đến mục 25 std::moveđược áp dụng ngầm khi trình biên dịch chọn không bỏ qua bản sao và lập trình viên không nên làm như vậy một cách rõ ràng.

Trong trường hợp của bạn, mã rõ ràng là một ứng cử viên cho RVO vì nó trả về đối tượng cục bộ pvà loại pgiống như kiểu trả về, dẫn đến việc sao chép bản sao. Và nếu trình biên dịch chọn không bỏ qua bản sao, vì bất kỳ lý do gì, std::movesẽ bị đá vào dòng 1.


5

Một điều mà tôi đã không thấy trong các câu trả lời khác làĐể làm rõ một câu trả lời khác rằng có một sự khác biệt giữa trả về std :: unique_ptr đã được tạo trong một hàm và một câu trả lời đã được trao cho hàm đó.

Ví dụ có thể như thế này:

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

Nó được đề cập trong câu trả lời bởi fredoverflow - " đối tượng tự động " được tô sáng rõ ràng . Một tham chiếu (bao gồm một tham chiếu rvalue) không phải là một đối tượng tự động.
Toby Speight

@TobySpeight Ok, xin lỗi. Tôi đoán mã của tôi chỉ là một sự làm rõ sau đó.
v010dya
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.