Tại sao chúng ta có thể sử dụng `std :: move` trên đối tượng` const`?


113

Trong C ++ 11, chúng ta có thể viết mã này:

struct Cat {
   Cat(){}
};

const Cat cat;
std::move(cat); //this is valid in C++11

Khi tôi gọi std::move, nó có nghĩa là tôi muốn di chuyển đối tượng, tức là tôi sẽ thay đổi đối tượng. Để di chuyển một constđối tượng là không hợp lý, vậy tại sao std::movekhông hạn chế hành vi này? Nó sẽ là một cái bẫy trong tương lai, phải không?

Ở đây cái bẫy có nghĩa như Brandon đã đề cập trong bình luận:

"Tôi nghĩ ý anh ta là nó" gài bẫy "anh ta một cách lén lút vì nếu anh ta không nhận ra, anh ta sẽ nhận được một bản sao không phải như ý định của anh ta."

Trong cuốn sách 'C ++ hiện đại hiệu quả' của Scott Meyers, ông đưa ra một ví dụ:

class Annotation {
public:
    explicit Annotation(const std::string text)
     : value(std::move(text)) //here we want to call string(string&&),
                              //but because text is const, 
                              //the return type of std::move(text) is const std::string&&
                              //so we actually called string(const string&)
                              //it is a bug which is very hard to find out
private:
    std::string value;
};

Nếu std::movebị cấm hoạt động trên một constđối tượng, chúng ta có thể dễ dàng tìm ra lỗi, phải không?


2
Nhưng hãy cố gắng di chuyển nó. Cố gắng thay đổi trạng thái của nó. std::movetự nó không làm gì đối tượng. Người ta có thể tranh luận std::movelà được đặt tên kém.
juanchopanza

3
Nó không thực sự di chuyển bất cứ điều gì. Tất cả những gì nó làm được chuyển thành tham chiếu giá trị. thử CAT cat2 = std::move(cat);, giả sử CAThỗ trợ chuyển nhượng thường xuyên.
WhozCraig

11
std::movechỉ là một diễn viên, nó không thực sự di chuyển bất cứ điều gì
Red Alert

2
@WhozCraig: Hãy cẩn thận, vì mã bạn đăng sẽ biên dịch và thực thi mà không có cảnh báo, khiến nó có thể gây hiểu lầm.
Mooing Duck

1
@MooingDuck Chưa bao giờ nói rằng nó sẽ không biên dịch. nó chỉ hoạt động vì copy-ctor mặc định được bật. Ngồi xuống và bánh xe rơi ra.
WhozCraig

Câu trả lời:


55
struct strange {
  mutable size_t count = 0;
  strange( strange const&& o ):count(o.count) { o.count = 0; }
};

const strange s;
strange s2 = std::move(s);

ở đây chúng ta thấy việc sử dụng std::movetrên a T const. Nó trả về a T const&&. Chúng tôi có một phương thức khởi tạo di chuyển cho strangechính xác kiểu này.

Và nó được gọi là.

Bây giờ, đúng là loại kỳ lạ này hiếm hơn những lỗi mà đề xuất của bạn sẽ sửa.

Tuy nhiên, mặt khác, mã hiện tại std::movehoạt động tốt hơn trong mã chung, nơi bạn không biết loại bạn đang làm việc là a Thay a T const.


3
+1 vì là câu trả lời đầu tiên thực sự cố gắng giải thích lý do tại sao bạn muốn gọi std::movemột constđối tượng.
Chris Drew

+1 để hiển thị một chức năng đang thực hiện const T&&. Điều này thể hiện một "giao thức API" thuộc loại "Tôi sẽ lấy một rvalue-ref nhưng tôi hứa tôi sẽ không sửa đổi nó". Tôi đoán, ngoài việc sử dụng có thể thay đổi, nó không phổ biến. Có thể một trường hợp sử dụng khác là có thể sử dụng forward_as_tupletrên hầu hết mọi thứ và sau này sử dụng nó.
F Pereira

107

Có một mẹo ở đây mà bạn đang bỏ qua, đó là std::move(cat) nó không thực sự di chuyển bất cứ điều gì . Nó chỉ nói với trình biên dịch cố gắng di chuyển. Tuy nhiên, vì lớp của bạn không có hàm tạo nào chấp nhận a const CAT&&, thay vào đó nó sẽ sử dụng hàm tạo const CAT&sao chép ngầm và sao chép một cách an toàn. Không có nguy hiểm, không có cạm bẫy. Nếu trình tạo bản sao bị vô hiệu hóa vì bất kỳ lý do gì, bạn sẽ gặp lỗi trình biên dịch.

struct CAT
{
   CAT(){}
   CAT(const CAT&) {std::cout << "COPY";}
   CAT(CAT&&) {std::cout << "MOVE";}
};

int main() {
    const CAT cat;
    CAT cat2 = std::move(cat);
}

bản in COPY, không MOVE.

http://coliru.stacked-crooked.com/a/0dff72133dbf9d1f

Lưu ý rằng lỗi trong mã bạn đề cập là vấn đề hiệu suất , không phải vấn đề ổn định , vì vậy lỗi như vậy sẽ không gây ra sự cố. Nó sẽ chỉ sử dụng một bản sao chậm hơn. Ngoài ra, một lỗi như vậy cũng xảy ra đối với các đối tượng không phải hằng số không có hàm tạo chuyển động, vì vậy chỉ cần thêm một constquá tải sẽ không bắt được tất cả chúng. Chúng tôi có thể kiểm tra khả năng di chuyển cấu trúc hoặc di chuyển gán từ kiểu tham số, nhưng điều đó sẽ gây trở ngại cho mã mẫu chung được cho là rơi trở lại hàm tạo bản sao. Và heck, có lẽ ai đó muốn có thể xây dựng từ const CAT&&, tôi là ai để nói rằng anh ta không thể?


Được nâng cao. Cần lưu ý rằng việc xóa hoàn toàn copy-ctor theo định nghĩa do người dùng định nghĩa về hàm tạo di chuyển thông thường hoặc toán tử gán cũng sẽ chứng minh điều này thông qua biên dịch bị hỏng. Câu trả lời rất hay.
WhozCraig

Cũng đáng nói rằng một copy-constructor cần một constgiá trị không phải là giá trị cũng không giúp được gì. [class.copy] §8: "Nếu không, hàm tạo bản sao được khai báo ngầm sẽ có dạng X::X(X&)"
Deduplicator

9
Tôi không nghĩ ý của anh ấy là "cái bẫy" trong thuật ngữ máy tính / lắp ráp. Tôi nghĩ ý anh ấy là nó "bẫy" anh ấy một cách lén lút vì nếu anh ấy không nhận ra, thì cuối cùng anh ấy sẽ nhận được một bản sao không như những gì anh ấy dự định. Tôi đoán ..
Brandon

bạn có thể nhận được thêm 1 phiếu ủng hộ nữa, và tôi nhận được 4 phiếu khác, cho một huy hiệu vàng. ;)
Yakk - Adam Nevraumont

Năm cao trên mẫu mã đó. Nhận được điểm trên thực sự tốt.
Brent viết mã

20

Một lý do mà phần còn lại của các câu trả lời đã bỏ qua cho đến nay là khả năng mã chung có thể phục hồi khi di chuyển. Ví dụ, giả sử rằng tôi muốn viết một hàm chung di chuyển tất cả các phần tử ra khỏi một loại vùng chứa để tạo một loại vùng chứa khác có cùng các giá trị:

template <class C1, class C2>
C1
move_each(C2&& c2)
{
    return C1(std::make_move_iterator(c2.begin()),
              std::make_move_iterator(c2.end()));
}

Tuyệt vời, bây giờ tôi có thể tạo một cách tương đối hiệu quả vector<string>từ a deque<string>và mỗi cá nhân stringsẽ được di chuyển trong quá trình này.

Nhưng nếu tôi muốn chuyển từ a map?

int
main()
{
    std::map<int, std::string> m{{1, "one"}, {2, "two"}, {3, "three"}};
    auto v = move_each<std::vector<std::pair<int, std::string>>>(m);
    for (auto const& p : v)
        std::cout << "{" << p.first << ", " << p.second << "} ";
    std::cout << '\n';
}

Nếu được std::movenhấn mạnh vào một không constđối số, phần khởi tạo ở trên của move_eachsẽ không được biên dịch bởi vì nó đang cố gắng di chuyển a const int( key_typethe map). Nhưng mã này không quan tâm nếu nó không thể di chuyển key_type. Nó muốn di chuyển mapped_type(std::string ) vì lý do hiệu suất.

Đó là ví dụ này và vô số ví dụ khác giống như nó trong mã hóa chung std::movelà một yêu cầu di chuyển chứ không phải yêu cầu di chuyển.


2

Tôi có cùng mối quan tâm như OP.

std :: move không di chuyển đối tượng, cũng không đảm bảo đối tượng có thể di chuyển được. Vậy tại sao nó được gọi là di chuyển?

Tôi nghĩ rằng không thể di chuyển có thể là một trong hai trường hợp sau:

1. Kiểu chuyển động là const.

Lý do chúng ta có từ khóa const trong ngôn ngữ là chúng ta muốn trình biên dịch ngăn chặn bất kỳ thay đổi nào đối với đối tượng được định nghĩa là const. Lấy ví dụ trong cuốn sách của Scott Meyers:

    class Annotation {
    public:
     explicit Annotation(const std::string text)
     : value(std::move(text)) // "move" text into value; this code
     {  } // doesn't do what it seems to!    
     
    private:
     std::string value;
    };

Nghĩa đen của nó là gì? Di chuyển một chuỗi const sang thành viên giá trị - ít nhất, đó là hiểu biết của tôi trước khi tôi đọc giải thích.

Nếu ngôn ngữ có ý định không di chuyển hoặc không đảm bảo di chuyển được áp dụng khi std :: move () được gọi, thì nó thực sự gây hiểu nhầm khi sử dụng từ move.

Nếu ngôn ngữ đang khuyến khích mọi người sử dụng std :: move để có hiệu quả tốt hơn, thì nó phải ngăn chặn những cái bẫy như thế này càng sớm càng tốt, đặc biệt là đối với kiểu mâu thuẫn nghĩa đen rõ ràng này.

Tôi đồng ý rằng mọi người nên biết việc di chuyển một hằng số là không thể, nhưng nghĩa vụ này không có nghĩa là trình biên dịch có thể im lặng khi xảy ra mâu thuẫn rõ ràng.

2. Đối tượng không có hàm tạo chuyển động

Cá nhân tôi nghĩ đây là một câu chuyện tách biệt khỏi mối quan tâm của OP, như Chris Drew đã nói

@hvd Điều đó có vẻ như là một chút tranh cãi đối với tôi. Chỉ vì gợi ý của OP không sửa được tất cả các lỗi trên thế giới không nhất thiết có nghĩa là đó là một ý kiến ​​tồi (có thể là vậy, nhưng không phải vì lý do bạn đưa ra). - Chris Drew


1

Tôi ngạc nhiên là không ai đề cập đến khía cạnh tương thích ngược của điều này. Tôi tin rằng, std::moveđược thiết kế có chủ đích để làm điều này trong C ++ 11. Hãy tưởng tượng bạn đang làm việc với cơ sở mã kế thừa, cơ sở này phụ thuộc nhiều vào các thư viện C ++ 98, vì vậy nếu không có dự phòng về việc gán bản sao, việc di chuyển sẽ phá vỡ mọi thứ.


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.