2013-09-05 17 views
19

Trong bài nói chuyện GoingNative 2013 của mình, Scott Meyers đã chỉ ra rằng std::move không đảm bảo rằng mã được tạo sẽ thực sự thực hiện di chuyển.Buộc lỗi thời gian biên dịch nếu std :: di chuyển sẽ dẫn đến bản sao không mong muốn?

Ví dụ:

void foo(std::string x, const std::string y) { 
    std::string x2 = std::move(x); // OK, will be moved 
    std::string y2 = std::move(y); // compiles, but will be copied 
} 

Ở đây, các nhà xây dựng di chuyển không thể áp dụng nhưng vì độ phân giải quá tải, các nhà xây dựng bản sao bình thường sẽ được sử dụng để thay thế. Tùy chọn dự phòng này có thể rất quan trọng đối với khả năng tương thích ngược với mã C++ 98, nhưng trong ví dụ trên, rất có thể đó không phải là ý tưởng của lập trình viên.

Có cách nào để thực thi rằng một hàm tạo di chuyển sẽ được gọi?

Ví dụ: giả sử bạn muốn di chuyển một ma trận lớn. Nếu ứng dụng của bạn thực sự phụ thuộc vào Ma trận để được di chuyển, nó sẽ là tuyệt vời để ngay lập tức nhận được một lỗi biên dịch nếu một động thái là không thể. (Nếu không, vấn đề về hiệu suất có thể trượt dễ dàng thông qua các bài kiểm tra đơn vị và bạn sẽ chỉ tìm ra sau khi một số hồ sơ.)

Cho phép gọi di chuyển được đảm bảo này strict_move. Tôi muốn có thể viết mã như sau:

void bar(Matrix x, const Matrix y) { 
    Matrix x2 = strict_move(x); // OK 
    Matrix y2 = strict_move(y); // compile error 
} 

Có thể không?

Edit:

Cảm ơn câu trả lời tuyệt vời! Có một số yêu cầu hợp pháp để làm rõ câu hỏi của tôi:

  • Nếu strict_move không thành công nếu đầu vào là const?
  • Nếu strict_move không thành công nếu kết quả sẽ không dẫn đến hoạt động di chuyển thực tế (mặc dù bản sao có thể nhanh như di chuyển, ví dụ: const complex<double>)?
  • Cả hai?

Ý tưởng ban đầu của tôi rất mơ hồ: Tôi coi các ví dụ của Scott Meyers khá đáng báo động, vì vậy tôi tự hỏi liệu có thể có trình biên dịch ngăn các bản sao không mong muốn đó không.

Scott Meyers được đề cập trong bài nói chuyện của mình rằng một cảnh báo trình biên dịch chung không phải là một tùy chọn vì nó sẽ dẫn đến một số lượng lớn một sai tích cực. Thay vào đó, tôi muốn giao tiếp với trình biên dịch giống như "Tôi chắc chắn 100% rằng điều này phải luôn dẫn đến hoạt động di chuyển và bản sao quá đắt đối với loại cụ thể này".

Vì vậy, tôi đã tự ý nói rằng strict_move sẽ không thành công trong cả hai trường hợp. Trong khi đó tôi không chắc điều gì sẽ là tốt nhất. Một khía cạnh khác mà tôi không xem xét là noexcept.

Từ phía tôi, ngữ nghĩa chính xác của strict_move đang mở. Tất cả mọi thứ giúp ngăn ngừa một số sai lầm ngu ngốc tại thời gian biên dịch mà không có những hạn chế nghiêm trọng là tốt.

+2

Tạo phiên bản 'di chuyển' của riêng bạn để kiểm tra kiểu trả về? –

+2

Đợi đã, bạn có muốn kiểm tra xem đầu vào không phải là const hay bạn muốn kiểm tra xem di chuyển thực sự có xảy ra hay không. Bởi vì thứ hai là một ý tưởng tồi, và các câu trả lời dưới đây bỏ qua sự giải thích đó. –

+0

@MooingDuck Thú vị, tôi đã không nghĩ về sự khác biệt đó một cách chi tiết, tôi chỉ muốn có một mạng lưới an toàn chống lại hành vi không chủ ý. Tôi đồng ý rằng việc ngăn chặn việc di chuyển đối tượng 'const' là an toàn hơn, nhưng các đối số chống lại việc kiểm tra bổ sung thêm rằng một hàm tạo di chuyển là có sẵn và sẽ được gọi là gì? Tôi có thể hiểu rằng việc sử dụng cấu trúc như vậy trong mã thư viện có thể làm cho mã không linh hoạt vì nó loại trừ tất cả các lớp mà không cần chuyển các hàm tạo và có thể gây ra các vấn đề với các chuyển đổi. Nhưng nhược điểm của việc sử dụng nó chỉ trong phần hiệu năng quan trọng của cơ sở mã của riêng bạn là gì? –

Trả lời

18

Tôi khuyên chống lại viết một vị tướng strict_move được phát hiện const. Tôi nghĩ rằng đó không phải là những gì bạn đang tìm kiếm. Bạn có muốn điều này gắn cờ một số const complex<double> hoặc const pair<int, int> không? Những loại này sẽ sao chép nhanh khi chúng di chuyển. Gắn cờ chúng sẽ chỉ là một chất kích thích.

Nếu bạn muốn thực hiện điều này, tôi khuyên bạn nên kiểm tra xem loại đó có phải là noexcept MoveConstructible hay không. Điều này sẽ hoạt động hoàn hảo cho std::string. Nếu nhà xây dựng bản sao của string vô tình được gọi, nó không phải là ngoại lệ, và do đó sẽ được gắn cờ. Nhưng nếu nhà xây dựng bản sao của pair<int, int> vô tình được gọi, bạn có thực sự quan tâm không?

Đây là một phác thảo của điều này sẽ như thế nào:

#include <utility> 
#include <type_traits> 

template <class T> 
typename std::remove_reference<T>::type&& 
noexcept_move(T&& t) 
{ 
    typedef typename std::remove_reference<T>::type Tr; 
    static_assert(std::is_nothrow_move_constructible<Tr>::value, 
        "noexcept_move requires T to be noexcept move constructible"); 
    static_assert(std::is_nothrow_move_assignable<Tr>::value, 
        "noexcept_move requires T to be noexcept move assignable"); 
    return std::move(t); 
} 

tôi quyết định kiểm tra chống is_nothrow_move_assignable là tốt, là bạn không biết liệu khách hàng được xây dựng, chuyển nhượng quyền LHS.

Tôi đã chọn nội bộ static_assert thay vì bên ngoài enable_if vì tôi không mong đợi noexcept_move bị quá tải và static_assert sẽ mang lại thông báo lỗi rõ ràng hơn khi được kích hoạt.

+0

OP dường như không quan tâm quá nhiều về việc buộc phải di chuyển đến _happen_ (dù sao cũng có thể là lừa đảo), rất nhiều khi vô tình sử dụng 'di chuyển' theo cách không có ý nghĩa gì cả. –

+0

Có lẽ tôi đã hiểu nhầm câu hỏi. Nhưng 'noexcept_move' của tôi hoạt động như tôi tin rằng OP mong muốn với 'foo' của OP. –

+0

@MooingDuck: Cảm ơn bạn đã sửa lỗi sao chép/dán của tôi! –

5

Bạn chỉ có thể tạo phiên bản move của riêng bạn mà không cho phép các loại trả về không đổi.Ví dụ:

#include <utility> 
#include <string> 
#include <type_traits> 


template <typename T> 
struct enforce_nonconst 
{ 
    static_assert(!std::is_const<T>::value, "Trying an impossible move"); 
    typedef typename std::enable_if<!std::is_const<T>::value, T>::type type; 
}; 

template <typename T> 
constexpr typename enforce_nonconst<typename std::remove_reference<T>::type>::type && 
mymove(T && t) noexcept 
{ 
    return static_cast<typename std::remove_reference<T>::type &&>(t); 
} 

void foo(std::string a, std::string const b) 
{ 
    std::string x = std::move(a); 
    std::string y = std::move(b); 
} 

void bar(std::string a, std::string const b) 
{ 
    std::string x = mymove(a); 
    // std::string y = mymove(b); // Error 
} 

int main() { } 
5

Trước tiên, tôi muốn nói rằng các câu trả lời trước của bạn sẽ không giải quyết được vấn đề của bạn hoàn toàn.

Counterexample nơi các giải pháp trước đó (@Kerrerk và @ 0x499602D2) không thành công: giả sử rằng bạn đã viết lớp ma trận của bạn với một hàm tạo di chuyển ném một ngoại lệ. Bây giờ giả sử rằng bạn muốn di chuyển std::vector<matrix>. This article cho thấy rằng bạn không thể có "bảo đảm ngoại lệ mạnh" nếu các phần tử lớp ma trận mà các khoản giữ std::vector<matrix> đã thực sự được di chuyển (điều gì xảy ra nếu phần tử di chuyển phần tử thứ j ném một ngoại lệ? Bạn sẽ mất dữ liệu vì không có cách nào để khôi phục các yếu tố bạn đã di chuyển!).

Đó là lý do tại sao container STL thực hiện .push_back(), .reserve() và constructor di chuyển của họ sử dụng std::move_if_noexcept để di chuyển các yếu tố mà họ nắm giữ. Dưới đây là một ví dụ thực hiện dự trữ() lấy từ open-std:

void reserve(size_type n) 
{ 
    if (n > this->capacity()) 
    { 
     pointer new_begin = this->allocate(n); 
     size_type s = this->size(), i = 0; 
     try 
     { 
      for (;i < s; ++i) 
       new ((void*)(new_begin + i)) value_type(std::move_if_noexcept((*this)[i]))); 
     } 
     catch(...) 
     { 
      while (i > 0)     // clean up new elements 
       (new_begin + --i)->~value_type(); 

      this->deallocate(new_begin); // release storage 
      throw; 
     } 
     // -------- irreversible mutation starts here ----------- 
     this->deallocate(this->begin_); 
     this->begin_ = new_begin; 
     this->end_ = new_begin + s; 
     this->cap_ = new_begin + n; 
    } 
} 

Vì vậy, nếu bạn không thực hiện đến nỗi di chuyển và mặc định của bạn constructor là noexcept, bạn sẽ không đảm bảo rằng các chức năng như std :: vector. resize() hoặc std :: move (đối với stl container) sẽ không bao giờ sao chép lớp ma trận và đó là hành vi đúng (nếu không bạn có thể mất dữ liệu)

+0

Xác nhận quyền sở hữu: trong khi tôi đang viết/xem xét giải pháp này: @HowardHinnant xuất bản một câu trả lời tương tự –

+1

+1 cho các tham chiếu đến các giấy tờ ủy ban. Đó là nơi để đi cho lý do. –

Các vấn đề liên quan