Tự động && cho chúng tôi biết điều gì?


168

Nếu bạn đọc mã như

auto&& var = foo();

trong đó foobất kỳ hàm nào trả về theo giá trị của loại T. Sau đó varlà một giá trị của loại tham chiếu rvalue đến T. Nhưng điều này ngụ ý vargì? Có nghĩa là, chúng tôi được phép đánh cắp tài nguyên của var? Có bất kỳ tình huống hợp lý nào khi bạn nên sử dụng auto&&để nói với người đọc mã của bạn một cái gì đó giống như bạn làm khi bạn trả lại unique_ptr<>để nói rằng bạn có quyền sở hữu độc quyền? Và những gì về ví dụ T&&khi Tlà loại lớp?

Tôi chỉ muốn hiểu, nếu có bất kỳ trường hợp sử dụng nào khác ngoài những trường hợp auto&&trong lập trình mẫu; giống như những người được thảo luận trong các ví dụ trong bài viết này Tài liệu tham khảo phổ quát của Scott Meyers.


1
Tôi đã tự hỏi điều tương tự. Tôi hiểu cách loại trừ hoạt động, nhưng mã của tôi nói gì khi tôi sử dụng auto&&? Tôi đã suy nghĩ về việc xem tại sao một vòng lặp dựa trên phạm vi mở rộng để sử dụng auto&&làm ví dụ, nhưng vẫn chưa hoàn thành nó. Có lẽ bất cứ ai trả lời có thể giải thích nó.
Joseph Mansfield

1
Điều này thậm chí là hợp pháp? Tôi có nghĩa là instnce của T bị phá hủy ngay lập tức sau khi footrả về, lưu trữ một giá trị tham chiếu cho nó nghe giống như UB đến ne.

1
@aleguna Điều này là hoàn toàn hợp pháp. Tôi không muốn trả về một tham chiếu hoặc con trỏ tới một biến cục bộ, nhưng một giá trị. fooVí dụ, hàm có thể trông giống như : int foo(){return 1;}.
MWid

9
@aleguna tham chiếu đến tạm thời thực hiện mở rộng trọn đời, giống như trong C ++ 98.
ecatmur

5
@aleguna mở rộng trọn đời chỉ hoạt động với tạm thời cục bộ, không có chức năng trả về tài liệu tham khảo. Xem stackoverflow.com/a/2784304/567292
ecatmur

Câu trả lời:


232

Bằng cách sử dụng auto&& var = <initializer>bạn đang nói: Tôi sẽ chấp nhận bất kỳ trình khởi tạo nào bất kể đó là biểu thức lvalue hay rvalue và tôi sẽ giữ nguyên hằng số của nó . Điều này thường được sử dụng để chuyển tiếp (thường là với T&&). Lý do điều này hoạt động là vì một "tài liệu tham khảo phổ quát", auto&&hoặc T&&, sẽ liên kết với bất cứ điều gì .

Bạn có thể nói, tại sao không chỉ sử dụng một const auto&vì điều đó cũng sẽ liên kết với bất cứ điều gì? Vấn đề với việc sử dụng một consttài liệu tham khảo là nó const! Sau này, bạn sẽ không thể liên kết nó với bất kỳ tham chiếu không phải nào hoặc gọi bất kỳ hàm thành viên nào không được đánh dấu const.

Ví dụ, hãy tưởng tượng rằng bạn muốn lấy a std::vector, đưa một trình vòng lặp đến phần tử đầu tiên của nó và sửa đổi giá trị được chỉ ra bởi trình vòng lặp đó theo một cách nào đó:

auto&& vec = some_expression_that_may_be_rvalue_or_lvalue;
auto i = std::begin(vec);
(*i)++;

Mã này sẽ biên dịch tốt bất kể biểu thức khởi tạo. Các lựa chọn thay thế auto&&thất bại theo các cách sau:

auto         => will copy the vector, but we wanted a reference
auto&        => will only bind to modifiable lvalues
const auto&  => will bind to anything but make it const, giving us const_iterator
const auto&& => will bind only to rvalues

Vì vậy, đối với điều này, auto&&hoạt động hoàn hảo! Một ví dụ về việc sử dụng auto&&như thế này là trong một forvòng lặp dựa trên phạm vi . Xem câu hỏi khác của tôi để biết thêm chi tiết.

Nếu sau đó bạn sử dụng std::forwardtrên auto&&tài liệu tham khảo của mình để bảo vệ thực tế rằng ban đầu nó là giá trị hoặc giá trị, mã của bạn nói: Bây giờ tôi đã có đối tượng của mình từ biểu thức giá trị hoặc giá trị, tôi muốn giữ nguyên giá trị ban đầu vì vậy tôi có thể sử dụng nó một cách hiệu quả nhất - điều này có thể làm mất hiệu lực của nó. Như trong:

auto&& var = some_expression_that_may_be_rvalue_or_lvalue;
// var was initialized with either an lvalue or rvalue, but var itself
// is an lvalue because named rvalues are lvalues
use_it_elsewhere(std::forward<decltype(var)>(var));

Điều này cho phép use_it_elsewheretách ruột của nó ra vì mục đích hiệu suất (tránh các bản sao) khi trình khởi tạo ban đầu là một giá trị có thể sửa đổi.

Điều này có nghĩa là gì nếu chúng ta có thể hoặc khi chúng ta có thể đánh cắp tài nguyên từ varđâu? Vì ý auto&&chí sẽ liên kết với bất cứ điều gì, chúng ta không thể cố gắng tự xé varruột mình - nó rất có thể là một giá trị hoặc thậm chí là const. Tuy nhiên, chúng ta có thể làm std::forwardđiều đó với các chức năng khác có thể tàn phá hoàn toàn bên trong của nó. Ngay khi chúng tôi làm điều này, chúng tôi nên xem xét varở trạng thái không hợp lệ.

Bây giờ, hãy áp dụng điều này cho trường hợp auto&& var = foo();, như được đưa ra trong câu hỏi của bạn, nơi foo trả về một Tgiá trị. Trong trường hợp này, chúng tôi biết chắc chắn rằng loại varsẽ được suy ra là T&&. Vì chúng tôi biết chắc chắn rằng đó là một giá trị, chúng tôi không cần std::forwardsự cho phép để đánh cắp tài nguyên của nó. Trong trường hợp cụ thể này, biết rằng footrả về theo giá trị , người đọc chỉ nên đọc nó như sau: Tôi đang lấy một tham chiếu giá trị cho trả về tạm thời từ đó foo, vì vậy tôi có thể vui vẻ di chuyển từ nó.


Là một phụ lục, tôi nghĩ rằng đáng để đề cập đến khi một biểu thức như some_expression_that_may_be_rvalue_or_lvaluecó thể xuất hiện, ngoài trường hợp "mã của bạn có thể thay đổi". Vì vậy, đây là một ví dụ giả định:

std::vector<int> global_vec{1, 2, 3, 4};

template <typename T>
T get_vector()
{
  return global_vec;
}

template <typename T>
void foo()
{
  auto&& vec = get_vector<T>();
  auto i = std::begin(vec);
  (*i)++;
  std::cout << vec[0] << std::endl;
}

Ở đây, get_vector<T>()biểu hiện đáng yêu đó có thể là giá trị hoặc giá trị tùy thuộc vào loại chung T. Về cơ bản chúng tôi thay đổi kiểu trả về get_vectorthông qua mẫu tham số của foo.

Khi chúng ta gọi foo<std::vector<int>>, get_vectorsẽ trả về global_vectheo giá trị, đưa ra biểu thức giá trị. Ngoài ra, khi chúng tôi gọi foo<std::vector<int>&>, get_vectorsẽ trả về global_vecbằng tham chiếu, dẫn đến biểu thức giá trị.

Nếu chúng ta làm:

foo<std::vector<int>>();
std::cout << global_vec[0] << std::endl;
foo<std::vector<int>&>();
std::cout << global_vec[0] << std::endl;

Chúng tôi nhận được đầu ra như mong đợi:

2
1
2
2

Nếu bạn đã thay đổi auto&&trong mã bất kỳ auto, auto&, const auto&, hoặc const auto&&sau đó chúng tôi sẽ không có được kết quả chúng ta muốn.


Một cách khác để thay đổi logic chương trình dựa trên việc auto&&tham chiếu của bạn được khởi tạo với biểu thức giá trị hay giá trị là sử dụng các đặc điểm loại:

if (std::is_lvalue_reference<decltype(var)>::value) {
  // var was initialised with an lvalue expression
} else if (std::is_rvalue_reference<decltype(var)>::value) {
  // var was initialised with an rvalue expression
}

2
Chúng ta có thể nói đơn giản là T vec = get_vector<T>();bên trong chức năng không? Hoặc tôi đang đơn giản hóa nó đến một mức độ vô lý :)
Asterisk

@Asterisk Không có bcoz T vec chỉ có thể được gán cho lvalue trong trường hợp std :: vector <int &> và nếu T là std :: vector <int> thì chúng tôi sẽ sử dụng lệnh gọi theo giá trị không hiệu quả
Kapil

1
tự động & cho tôi kết quả tương tự. Tôi đang sử dụng MSVC 2015. Và GCC tạo ra lỗi.
Serge Podobry

Ở đây tôi đang sử dụng MSVC 2015, tự động & cho kết quả tương tự như tự động &&.
Kehe CAI

Tại sao là int i; tự động && j = i; được phép nhưng int i; int && j = i; không phải ?
SeventhSon84

14

Đầu tiên, tôi khuyên bạn nên đọc câu trả lời này của tôi dưới dạng đọc phụ để giải thích từng bước về cách khấu trừ đối số khuôn mẫu cho các tham chiếu phổ quát hoạt động.

Có nghĩa là, chúng tôi được phép đánh cắp tài nguyên của var?

Không cần thiết. Điều gì sẽ xảy ra nếu foo()đột nhiên trả lại một tham chiếu hoặc bạn đã thay đổi cuộc gọi mà quên cập nhật việc sử dụng var? Hoặc nếu bạn đang ở trong mã chung và loại trả về foo()có thể thay đổi tùy thuộc vào tham số của bạn?

Hãy nghĩ về auto&&việc giống hệt như T&&trong template<class T> void f(T&& v);, bởi vì nó (gần ) chính xác là như vậy. Bạn làm gì với các tham chiếu phổ quát trong các hàm, khi bạn cần chuyển chúng theo hoặc sử dụng chúng theo bất kỳ cách nào? Bạn sử dụng std::forward<T>(v)để lấy lại danh mục giá trị ban đầu. Nếu đó là một giá trị trước khi được chuyển đến chức năng của bạn, thì nó vẫn là một giá trị sau khi được chuyển qua std::forward. Nếu nó là một giá trị, nó sẽ trở thành một giá trị một lần nữa (hãy nhớ rằng, một tham chiếu rvalue có tên là một giá trị).

Vì vậy, làm thế nào để bạn sử dụng varchính xác trong một thời trang chung? Sử dụng std::forward<decltype(var)>(var). Điều này sẽ hoạt động chính xác giống như std::forward<T>(v)trong mẫu chức năng ở trên. Nếu varlà một T&&, bạn sẽ nhận lại T&được giá trị và nếu có , bạn sẽ nhận lại được giá trị.

Vì vậy, trở lại chủ đề: Làm gì auto&& v = f();std::forward<decltype(v)>(v)trong một cơ sở mã cho chúng tôi biết? Họ nói với chúng tôi rằng vsẽ được mua lại và truyền lại một cách hiệu quả nhất. Tuy nhiên, hãy nhớ rằng sau khi đã chuyển tiếp một biến như vậy, có thể nó đã được chuyển từ đó, vì vậy sẽ không chính xác khi sử dụng nó thêm mà không đặt lại nó.

Cá nhân, tôi sử dụng auto&&mã chung khi tôi cần một biến có thể sửa đổi. Chuyển tiếp hoàn hảo một giá trị đang sửa đổi, vì hoạt động di chuyển có khả năng đánh cắp can đảm của nó. Nếu tôi chỉ muốn lười biếng (nghĩa là không đánh vần tên loại ngay cả khi tôi biết) và không cần sửa đổi (ví dụ: khi chỉ in các phần tử của một phạm vi), tôi sẽ giữ nguyên auto const&.


autolà trong chừng mực khác nhau mà auto v = {1,2,3};sẽ làm vmột std::initializer_list, trong khi f({1,2,3})sẽ là một thất bại khấu trừ.


Trong phần đầu tiên của câu trả lời của bạn: Tôi có nghĩa là nếu foo()trả về một loại giá trị T, thì var(biểu thức này) sẽ là một giá trị và loại của nó (của biểu thức này) sẽ là một tham chiếu giá trị cho T(nghĩa là T&&).
MWid

@MWid: Làm cho ý nghĩa, loại bỏ phần đầu tiên.
Xèo

3

Hãy xem xét một số loại Tcó hàm tạo di chuyển và giả sử

T t( foo() );

sử dụng constructor di chuyển đó.

Bây giờ, hãy sử dụng một tham chiếu trung gian để nắm bắt lợi nhuận từ foo:

auto const &ref = foo();

quy tắc này không sử dụng hàm tạo di chuyển, vì vậy giá trị trả về sẽ phải được sao chép thay vì di chuyển (ngay cả khi chúng tôi sử dụng std::moveở đây, chúng tôi thực sự không thể di chuyển qua một const ref)

T t(std::move(ref));   // invokes T::T(T const&)

Tuy nhiên, nếu chúng ta sử dụng

auto &&rvref = foo();
// ...
T t(std::move(rvref)); // invokes T::T(T &&)

các constructor di chuyển vẫn có sẵn.


Và để giải quyết các câu hỏi khác của bạn:

... Có bất kỳ tình huống hợp lý nào khi bạn nên sử dụng tự động && để cho người đọc biết mã của bạn một cái gì đó ...

Điều đầu tiên, như Xeo nói, về cơ bản là tôi đang vượt qua X một cách hiệu quả nhất có thể , bất kể loại X là gì. Vì vậy, nhìn thấy mã sử dụng auto&&nội bộ nên thông báo rằng nó sẽ sử dụng ngữ nghĩa di chuyển trong nội bộ khi thích hợp.

... Giống như bạn làm khi bạn trả lại unique_ptr <> để nói rằng bạn có quyền sở hữu độc quyền ...

Khi một mẫu hàm có một đối số kiểu T&&, nó nói rằng nó có thể di chuyển đối tượng bạn truyền vào. Trả lại unique_ptrrõ ràng mang lại quyền sở hữu cho người gọi; chấp nhận T&&có thể xóa quyền sở hữu khỏi người gọi (nếu ctor di chuyển tồn tại, v.v.).


2
Tôi không chắc chắn ví dụ thứ hai của bạn là hợp lệ. Bạn không cần phải chuyển tiếp hoàn hảo để gọi hàm tạo di chuyển?

3
Cái này sai. Trong cả hai trường hợp, hàm tạo sao chép được gọi, vì refrvrefđều là giá trị. Nếu bạn muốn xây dựng di chuyển, sau đó bạn phải viết T t(std::move(rvref)).
MWid

Ý của bạn là const ref trong ví dụ đầu tiên của bạn : auto const &?
PiotrNycz

@aleguna - bạn và MWid nói đúng, cảm ơn. Tôi đã sửa câu trả lời của mình.
Vô dụng

1
@ Vô dụng Bạn đúng. Nhưng điều này không trả lời câu hỏi của tôi. Khi nào bạn sử dụng auto&&và bạn nói gì với người đọc mã của bạn bằng cách sử dụng auto&&?
MWid

-3

Các auto &&cú pháp sử dụng hai tính năng mới của C ++ 11:

  1. Phần autocho phép trình biên dịch suy ra loại dựa trên ngữ cảnh (giá trị trả về trong trường hợp này). Điều này là không có bất kỳ trình độ tham chiếu nào (cho phép bạn chỉ định xem bạn muốn T, T &hoặc T &&cho một loại suy diễn T).

  2. Đây &&là ngữ nghĩa di chuyển mới. Một loại hỗ trợ ngữ nghĩa di chuyển thực hiện một hàm tạo T(T && other)giúp di chuyển tối ưu nội dung trong loại mới. Điều này cho phép một đối tượng hoán đổi biểu diễn bên trong thay vì thực hiện một bản sao sâu.

Điều này cho phép bạn có một cái gì đó như:

std::vector<std::string> foo();

Vì thế:

auto var = foo();

sẽ thực hiện một bản sao của vectơ trả về (đắt tiền), nhưng:

auto &&var = foo();

sẽ hoán đổi biểu diễn bên trong của vectơ (vectơ từ foovà vectơ trống từ var), do đó sẽ nhanh hơn.

Điều này được sử dụng trong cú pháp for-loop mới:

for (auto &item : foo())
    std::cout << item << std::endl;

Trong đó vòng lặp for đang giữ một auto &&giá trị trả về từ fooitem là một tham chiếu đến từng giá trị trong foo.


Điều này là không đúng. auto&&sẽ không di chuyển bất cứ điều gì, nó sẽ chỉ làm một tài liệu tham khảo. Cho dù đó là tham chiếu lvalue hay rvalue tùy thuộc vào biểu thức được sử dụng để khởi tạo nó.
Joseph Mansfield

Trong cả hai trường hợp, hàm tạo di chuyển sẽ được gọi, vì std::vectorstd::stringlà cấu trúc di động. Điều này không có gì để làm với các loại var.
MWid

1
@MWid: Trên thực tế, cuộc gọi đến nhà xây dựng sao chép / di chuyển cũng có thể được thực hiện hoàn toàn với RVO.
Matthieu M.

@MatthieuM. Bạn đúng rồi. Nhưng tôi nghĩ rằng trong ví dụ trên, cnstructor sao chép sẽ không bao giờ được gọi, vì mọi thứ đều có thể di chuyển được.
MWid

1
@MWid: quan điểm của tôi là ngay cả các nhà xây dựng di chuyển cũng có thể bị loại bỏ. Elision trumps di chuyển (nó rẻ hơn).
Matthieu M.
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.