Khi nào cần tạo một loại không di chuyển được trong C ++ 11?


126

Tôi đã ngạc nhiên khi điều này không xuất hiện trong kết quả tìm kiếm của tôi, tôi nghĩ rằng ai đó sẽ hỏi điều này trước đây, vì tính hữu ích của ngữ nghĩa di chuyển trong C ++ 11:

Khi nào tôi phải (hoặc đó là một ý tưởng tốt cho tôi) làm cho một lớp không thể di chuyển trong C ++ 11?

(Lý do khác với các vấn đề tương thích với mã hiện có, đó là.)


2
boost luôn đi trước một bước - "đắt tiền để di chuyển các loại" ( boost.org/doc/libs/1_48_0/doc/html/container/move_emplace.html )
SChepurin

1
Tôi nghĩ rằng đây là một câu hỏi rất hay và hữu ích ( +1từ tôi) với câu trả lời rất kỹ lưỡng từ Herb (hoặc người anh em song sinh của anh ấy, vì vậy có vẻ như vậy ), vì vậy tôi đã biến nó thành một mục FAQ. Nếu ai đó đối tượng chỉ ping tôi tại phòng chờ , vì vậy điều này có thể được thảo luận ở đó.
sbi

1
Các lớp có thể di chuyển của AFAIK vẫn có thể bị cắt, do đó, việc cấm di chuyển (và sao chép) đối với tất cả các lớp cơ sở đa hình (nghĩa là tất cả các lớp cơ sở có chức năng ảo).
Phi

1
@Mehrdad: Tôi chỉ nói rằng "T có một nhà xây dựng di chuyển" và " T x = std::move(anotherT);hợp pháp" không tương đương. Cái sau là một yêu cầu di chuyển có thể rơi vào ctor sao chép trong trường hợp T không có ctor di chuyển. Vì vậy, "di chuyển" chính xác có nghĩa là gì?
sellibitze

1
@Mehrdad: Kiểm tra phần thư viện chuẩn C ++ về ý nghĩa của "MoveConstructible". Một số trình vòng lặp có thể không có hàm tạo di chuyển, nhưng nó vẫn là MoveConstructible. Xem ra cho các định nghĩa khác nhau của những người "di chuyển" có trong tâm trí.
sellibitze

Câu trả lời:


110

Câu trả lời của Herb (trước khi nó được chỉnh sửa) thực sự đã đưa ra một ví dụ điển hình về loại không nên di chuyển : std::mutex.

Loại mutex gốc của HĐH (ví dụ: pthread_mutex_ttrên nền tảng POSIX) có thể không phải là "bất biến vị trí" nghĩa là địa chỉ của đối tượng là một phần của giá trị. Ví dụ, HĐH có thể giữ một danh sách các con trỏ tới tất cả các đối tượng mutex được khởi tạo. Nếu std::mutexchứa loại mutex OS gốc là thành viên dữ liệu và địa chỉ của kiểu bản địa phải được cố định (vì HĐH duy trì một danh sách các con trỏ tới mutexes của nó) thì std::mutexsẽ phải lưu trữ kiểu mutex gốc trên heap để nó ở lại cùng một vị trí khi di chuyển giữa std::mutexcác đối tượng hoặc std::mutexkhông được di chuyển. Lưu trữ nó trên heap là không thể, bởi vì một std::mutexngười constexprxây dựng và phải đủ điều kiện để khởi tạo liên tục (nghĩa là khởi tạo tĩnh) để toàn cầustd::mutexđược đảm bảo được xây dựng trước khi bắt đầu thực thi chương trình, vì vậy hàm tạo của nó không thể sử dụng new. Vì vậy, lựa chọn duy nhất còn lại là std::mutexbất động.

Lý do tương tự áp dụng cho các loại khác có chứa một cái gì đó yêu cầu một địa chỉ cố định. Nếu địa chỉ của tài nguyên phải cố định, đừng di chuyển nó!

Có một lập luận khác cho việc không di chuyển std::mutex, đó là sẽ rất khó để làm điều đó một cách an toàn, bởi vì bạn cần biết rằng không ai đang cố gắng khóa mutex tại thời điểm nó được di chuyển. Vì mutexes là một trong những khối xây dựng mà bạn có thể sử dụng để ngăn chặn các cuộc đua dữ liệu, sẽ thật đáng tiếc nếu chúng không an toàn trước chính các cuộc đua! Với một bất động, std::mutexbạn biết những điều duy nhất bất cứ ai có thể làm với nó một khi nó đã được xây dựng và trước khi nó bị phá hủy là khóa nó và mở khóa nó, và những hoạt động đó được đảm bảo rõ ràng là an toàn cho luồng và không giới thiệu các cuộc đua dữ liệu. Lập luận tương tự này áp dụng cho std::atomic<T>các đối tượng: trừ khi chúng có thể được di chuyển nguyên tử, không thể di chuyển chúng một cách an toàn, một luồng khác có thể đang cố gắng gọicompare_exchange_strongtrên đối tượng ngay tại thời điểm nó được di chuyển. Vì vậy, một trường hợp khác mà các loại không nên di chuyển được là ở chỗ chúng là các khối xây dựng mức thấp của mã đồng thời an toàn và phải đảm bảo tính nguyên tử của tất cả các hoạt động trên chúng. Nếu giá trị đối tượng có thể được chuyển sang một đối tượng mới bất cứ lúc nào bạn cần sử dụng biến nguyên tử để bảo vệ mọi biến số nguyên tử để bạn biết liệu có an toàn khi sử dụng nó hay nó đã được di chuyển ... và một biến nguyên tử để bảo vệ biến nguyên tử đó, v.v.

Tôi nghĩ rằng tôi sẽ khái quát để nói rằng khi một đối tượng chỉ là một mảnh bộ nhớ thuần túy, không phải là một loại đóng vai trò là người nắm giữ một giá trị hoặc sự trừu tượng của một giá trị, thì việc di chuyển nó sẽ không có ý nghĩa gì. Các loại cơ bản như intkhông thể di chuyển: di chuyển chúng chỉ là một bản sao. Bạn không thể rip can đảm ra khỏi một int, bạn có thể sao chép giá trị của nó và sau đó đặt nó là không, nhưng nó vẫn là intvới một giá trị, nó chỉ byte của bộ nhớ. Nhưng một intvẫn có thể di chuyểntrong các điều khoản ngôn ngữ vì một bản sao là một hoạt động di chuyển hợp lệ. Tuy nhiên, đối với các loại không thể sao chép, nếu bạn không muốn hoặc không thể di chuyển mảnh bộ nhớ và bạn cũng không thể sao chép giá trị của nó, thì nó không thể di chuyển được. Một mutex hoặc một biến nguyên tử là một vị trí cụ thể của bộ nhớ (được xử lý bằng các thuộc tính đặc biệt) vì vậy không có ý nghĩa để di chuyển, và cũng không thể sao chép, vì vậy nó không thể di chuyển được.


17
+1 một ví dụ ít kỳ lạ hơn về thứ gì đó không thể di chuyển được vì nó có một địa chỉ đặc biệt là một nút trong cấu trúc đồ thị có hướng.
Potatoswatter

3
Nếu mutex không thể sao chép và không di chuyển được, làm thế nào tôi có thể sao chép hoặc di chuyển một đối tượng có chứa mutex? (Giống như một lớp an toàn chủ đề với mutex riêng của nó để đồng bộ hóa ...)
tr3w

4
@ tr3w, bạn không thể, trừ khi bạn tạo mutex trên heap và giữ nó thông qua unique_ptr hoặc tương tự
Jonathan Wakely

2
@ tr3w: Bạn sẽ không di chuyển toàn bộ lớp ngoại trừ phần mutex chứ?
dùng541686

3
@BenVoigt, nhưng đối tượng mới sẽ có mutex riêng. Tôi nghĩ rằng anh ta có nghĩa là có các hoạt động di chuyển do người dùng xác định, di chuyển tất cả các thành viên ngoại trừ thành viên mutex. Vậy nếu đối tượng cũ sắp hết hạn thì sao? Mutex của nó hết hạn với nó.
Jonathan Wakely

57

Câu trả lời ngắn: Nếu một loại có thể sao chép, nó cũng có thể được di chuyển. Tuy nhiên, điều ngược lại là không đúng: một số loại như std::unique_ptrcó thể di chuyển được nhưng không hợp lý khi sao chép chúng; Đây là những loại chỉ di chuyển tự nhiên.

Câu trả lời dài hơn một chút sau ...

Có hai loại chính (trong số các loại có mục đích đặc biệt khác như đặc điểm):

  1. Các loại giống như giá trị, chẳng hạn như inthoặc vector<widget>. Chúng đại diện cho các giá trị, và đương nhiên có thể sao chép. Trong C ++ 11, nói chung, bạn nên nghĩ về việc di chuyển như là một tối ưu hóa của bản sao và vì vậy tất cả các loại có thể sao chép nên có thể di chuyển một cách tự nhiên ... di chuyển chỉ là một cách hiệu quả để thực hiện một bản sao trong trường hợp thường gặp mà bạn không ' Không cần đối tượng ban đầu nữa và dù sao cũng sẽ phá hủy nó.

  2. Các kiểu giống như tham chiếu tồn tại trong hệ thống phân cấp thừa kế, chẳng hạn như các lớp cơ sở và các lớp có các hàm thành viên ảo hoặc được bảo vệ. Chúng thường được giữ bởi con trỏ hoặc tham chiếu, thường là một base*hoặc base&, và do đó không cung cấp cấu trúc sao chép để tránh cắt lát; nếu bạn muốn lấy một đối tượng khác giống như một đối tượng hiện có, bạn thường gọi một hàm ảo như thế nào clone. Chúng không cần di chuyển xây dựng hoặc gán vì hai lý do: Chúng không thể sao chép và chúng đã có thao tác "di chuyển" tự nhiên thậm chí hiệu quả hơn - bạn chỉ cần sao chép / di chuyển con trỏ đến đối tượng và bản thân đối tượng không phải di chuyển đến một vị trí bộ nhớ mới.

Hầu hết các loại thuộc một trong hai loại đó, nhưng cũng có những loại khác cũng hữu ích, hiếm hơn. Cụ thể ở đây, các loại thể hiện quyền sở hữu duy nhất của một tài nguyên, chẳng hạn như std::unique_ptr, là các loại chỉ di chuyển tự nhiên, vì chúng không giống như giá trị (không có nghĩa là sao chép chúng) nhưng bạn không sử dụng chúng trực tiếp (không phải lúc nào cũng bằng con trỏ hoặc tham chiếu) và vì vậy muốn di chuyển các đối tượng thuộc loại này từ nơi này sang nơi khác.


61
Sẽ là thực Herb Sutter xin vui lòng đứng lên? :)
dòng chảy

6
Vâng, tôi đã chuyển từ sử dụng một tài khoản Google OAuth sang tài khoản khác và không thể bận tâm tìm cách hợp nhất hai thông tin đăng nhập mang lại cho tôi ở đây. (Tuy nhiên, một cuộc tranh luận khác chống lại OAuth trong số những cuộc tranh luận hấp dẫn hơn nhiều.) Tôi có thể sẽ không sử dụng lại cuộc tranh luận khác, vì vậy đây là những gì tôi sẽ sử dụng cho bài viết SO thỉnh thoảng.
Herb Sutter

7
Tôi nghĩ rằng đó std::mutexlà bất động, vì các biến thể POSIX được sử dụng theo địa chỉ.
Cún con

9
@SChepurin: Thật ra, cái đó được gọi là HerbOverflow.
sbi

26
Điều này đang nhận được rất nhiều sự ủng hộ, không ai nhận thấy nó nói khi nào một loại chỉ nên di chuyển, đó không phải là câu hỏi? :)
Jonathan Wakely

18

Thực tế khi tôi tìm kiếm xung quanh, tôi thấy khá nhiều loại trong C ++ 11 không thể di chuyển được:

  • tất cả các mutexloại ( recursive_mutex, timed_mutex, recursive_timed_mutex,
  • condition_variable
  • type_info
  • error_category
  • locale::facet
  • random_device
  • seed_seq
  • ios_base
  • basic_istream<charT,traits>::sentry
  • basic_ostream<charT,traits>::sentry
  • tất cả các atomicloại
  • once_flag

Rõ ràng có một cuộc thảo luận về Clang: https://groups.google.com/forum/?fromgroups=#!topic/comp.std.c++/pCO1Qqb3Xa4


1
... các vòng lặp không nên di chuyển?! Cái gì ... tại sao?
dùng541686

vâng, tôi nghĩ iterators / iterator adaptorsnên chỉnh sửa vì C ++ 11 có move_iterator?
billz

Được rồi bây giờ tôi chỉ bối rối. Bạn đang nói về vòng lặp mà di chuyển của họ mục tiêu , tương đương khoảng cách di chuyển lặp mình ?
dùng541686

1
Thế là xong std::reference_wrapper. Ok, những người khác thực sự dường như không thể di chuyển.
Christian Rau

1
Đây dường như rơi vào ba loại: 1. ở mức độ thấp loại đồng thời liên quan đến (Atomics, mutexes), 2. các lớp cơ sở đa hình ( ios_base, type_info, facet), 3. loại thứ lạ ( sentry). Có lẽ các lớp duy nhất không thể di chuyển mà một lập trình viên trung bình sẽ viết là trong thể loại thứ hai.
Philipp

0

Một lý do khác mà tôi đã tìm thấy - hiệu suất. Giả sử bạn có một lớp 'a' chứa giá trị. Bạn muốn xuất giao diện cho phép người dùng thay đổi giá trị trong một thời gian giới hạn (đối với phạm vi).

Một cách để đạt được điều này là bằng cách trả về một đối tượng 'bảo vệ phạm vi' từ 'a', đặt lại giá trị trong hàm hủy của nó, như vậy:

class a 
{ 
    int value = 0;

  public:

    struct change_value_guard 
    { 
        friend a;
      private:
        change_value_guard(a& owner, int value) 
            : owner{ owner } 
        { 
            owner.value = value;
        }
        change_value_guard(change_value_guard&&) = delete;
        change_value_guard(const change_value_guard&) = delete;
      public:
        ~change_value_guard()
        {
            owner.value = 0;
        }
      private:
        a& owner;
    };

    change_value_guard changeValue(int newValue)
    { 
        return{ *this, newValue };
    }
};

int main()
{
    a a;
    {
        auto guard = a.changeValue(2);
    }
}

Nếu tôi thực hiện thay đổi_value_guard có thể di chuyển, tôi sẽ phải thêm 'nếu' vào hàm hủy của nó để kiểm tra xem bộ bảo vệ đã được di chuyển từ đó chưa - đó là một bổ sung nếu và tác động hiệu suất.

Vâng, chắc chắn, nó có thể được tối ưu hóa bởi bất kỳ trình tối ưu hóa lành mạnh nào, nhưng ngôn ngữ vẫn rất tốt (điều này đòi hỏi C ++ 17, để có thể trả về một loại không thể di chuyển được yêu cầu xóa bản sao được bảo đảm) không yêu cầu chúng tôi để trả tiền nếu chúng ta sẽ không di chuyển người bảo vệ nào khác ngoài việc trả lại nó từ chức năng tạo (nguyên tắc không trả tiền cho những gì bạn không sử dụng).

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.