Chà, có quá nhiều thứ để dọn dẹp ở đây ...
Đầu tiên, Sao chép và Hoán đổi không phải lúc nào cũng là cách chính xác để thực hiện Chỉ định Sao chép. Gần như chắc chắn trong trường hợp dumb_array
, đây là một giải pháp dưới mức tối ưu.
Việc sử dụng Copy and Swap là dumb_array
một ví dụ điển hình về việc đặt hoạt động đắt tiền nhất với đầy đủ các tính năng nhất ở lớp dưới cùng. Nó hoàn hảo cho những khách hàng muốn có đầy đủ tính năng nhất và sẵn sàng trả tiền phạt cho hiệu suất. Họ nhận được chính xác những gì họ muốn.
Nhưng thật tai hại cho những khách hàng không cần tính năng đầy đủ nhất và thay vào đó họ đang tìm kiếm hiệu suất cao nhất. Đối với họ dumb_array
chỉ là một phần mềm khác mà họ phải viết lại vì nó quá chậm. Đã dumb_array
được thiết kế khác biệt, nó có thể làm hài lòng cả hai khách hàng mà không có bất kỳ sự thỏa hiệp nào đối với một trong hai khách hàng.
Chìa khóa để làm hài lòng cả hai khách hàng là xây dựng các hoạt động nhanh nhất ở cấp thấp nhất và sau đó thêm API lên trên để có các tính năng đầy đủ hơn với chi phí cao hơn. Tức là bạn cần đảm bảo ngoại lệ mạnh mẽ, tốt, bạn trả tiền cho nó. Bạn không cần nó? Đây là một giải pháp nhanh hơn.
Hãy tìm hiểu cụ thể: Đây là toán tử Gán sao chép cơ bản, nhanh chóng, đảm bảo ngoại lệ cho dumb_array
:
dumb_array& operator=(const dumb_array& other)
{
if (this != &other)
{
if (mSize != other.mSize)
{
delete [] mArray;
mArray = nullptr;
mArray = other.mSize ? new int[other.mSize] : nullptr;
mSize = other.mSize;
}
std::copy(other.mArray, other.mArray + mSize, mArray);
}
return *this;
}
Giải trình:
Một trong những điều đắt tiền hơn bạn có thể làm trên phần cứng hiện đại là thực hiện một chuyến đi đến đống. Bất cứ điều gì bạn có thể làm để tránh bị mất thời gian và công sức. Khách hàng của dumb_array
có thể muốn thường xuyên gán các mảng có cùng kích thước. Và khi họ làm vậy, tất cả những gì bạn cần làm là memcpy
(ẩn bên dưới std::copy
). Bạn không muốn phân bổ một mảng mới có cùng kích thước và sau đó phân bổ mảng cũ có cùng kích thước!
Bây giờ dành cho khách hàng của bạn, những người thực sự muốn an toàn ngoại lệ mạnh mẽ:
template <class C>
C&
strong_assign(C& lhs, C rhs)
{
swap(lhs, rhs);
return lhs;
}
Hoặc có thể nếu bạn muốn tận dụng lợi thế của việc chuyển nhượng trong C ++ 11 thì nên:
template <class C>
C&
strong_assign(C& lhs, C rhs)
{
lhs = std::move(rhs);
return lhs;
}
Nếu dumb_array
khách hàng coi trọng tốc độ, họ nên gọi operator=
. Nếu họ cần an toàn ngoại lệ mạnh mẽ, có những thuật toán chung mà họ có thể gọi sẽ hoạt động trên nhiều đối tượng và chỉ cần thực hiện một lần.
Bây giờ trở lại câu hỏi ban đầu (có kiểu chữ o tại thời điểm này):
Class&
Class::operator=(Class&& rhs)
{
if (this == &rhs) // is this check needed?
{
// ...
}
return *this;
}
Đây thực sự là một câu hỏi gây tranh cãi. Một số sẽ nói có, hoàn toàn, một số sẽ nói không.
Ý kiến cá nhân của tôi là không, bạn không cần kiểm tra này.
Cơ sở lý luận:
Khi một đối tượng liên kết với một tham chiếu rvalue thì đó là một trong hai điều:
- Tạm thời.
- Đối tượng mà người gọi muốn bạn tin tưởng chỉ là tạm thời.
Nếu bạn có một tham chiếu đến một đối tượng thực sự là tạm thời, thì theo định nghĩa, bạn có một tham chiếu duy nhất đến đối tượng đó. Nó không thể được tham chiếu bởi bất kỳ nơi nào khác trong toàn bộ chương trình của bạn. Tức this == &temporary
là không thể .
Bây giờ, nếu khách hàng của bạn đã nói dối bạn và hứa với bạn rằng bạn sẽ nhận được tạm thời khi không có, thì khách hàng có trách nhiệm đảm bảo rằng bạn không phải quan tâm. Nếu bạn muốn thực sự cẩn thận, tôi tin rằng đây sẽ là cách triển khai tốt hơn:
Class&
Class::operator=(Class&& other)
{
assert(this != &other);
// ...
return *this;
}
Tức là Nếu bạn được thông qua tự tham chiếu, đây là một lỗi ở phần máy khách cần được sửa.
Để hoàn thiện, đây là một toán tử gán di chuyển cho dumb_array
:
dumb_array& operator=(dumb_array&& other)
{
assert(this != &other);
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
Trong trường hợp sử dụng điển hình của phép chuyển nhượng, *this
sẽ là một đối tượng được chuyển đến và do đó, delete [] mArray;
nên là một không chọn. Điều quan trọng là việc triển khai thực hiện xóa trên nullptr càng nhanh càng tốt.
Cảnh báo:
Một số người sẽ cho rằng đó swap(x, x)
là một ý tưởng tốt, hay chỉ là một điều ác cần thiết. Và điều này, nếu hoán đổi chuyển sang hoán đổi mặc định, có thể gây ra sự tự chuyển nhượng.
Tôi không đồng ý rằng swap(x, x)
là bao giờ là một ý tưởng tốt. Nếu được tìm thấy trong mã của riêng tôi, tôi sẽ coi đó là lỗi hiệu suất và sửa nó. Nhưng trong trường hợp bạn muốn cho phép nó, hãy nhận ra rằng swap(x, x)
chỉ có self-move-assignemnet trên một giá trị chuyển từ. Và trong dumb_array
ví dụ của chúng tôi, điều này sẽ hoàn toàn vô hại nếu chúng tôi đơn giản bỏ qua khẳng định hoặc hạn chế nó trong trường hợp chuyển từ:
dumb_array& operator=(dumb_array&& other)
{
assert(this != &other || mSize == 0);
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
Nếu bạn tự gán hai mục chuyển đến từ (trống) dumb_array
, bạn không làm gì sai ngoài việc chèn các hướng dẫn vô ích vào chương trình của mình. Quan sát tương tự này có thể được thực hiện cho đại đa số các đối tượng.
<
Cập nhật>
Tôi đã suy nghĩ thêm về vấn đề này và thay đổi quan điểm của mình phần nào. Bây giờ tôi tin rằng việc chuyển nhượng nên được chấp nhận cho việc tự chuyển nhượng, nhưng điều kiện đăng bài về chuyển nhượng bản sao và chuyển nhượng chuyển nhượng là khác nhau:
Đối với nhiệm vụ sao chép:
x = y;
người ta phải có một điều kiện hậu mà giá trị của y
không được thay đổi. Khi &x == &y
đó hậu điều kiện này chuyển thành: việc tự sao chép sẽ không ảnh hưởng đến giá trị của x
.
Đối với nhiệm vụ di chuyển:
x = std::move(y);
ta nên có một hậu điều kiện y
có trạng thái hợp lệ nhưng không xác định. Khi &x == &y
đó hậu điều kiện này chuyển thành: x
có trạng thái hợp lệ nhưng không xác định. Tức là việc chỉ định tự di chuyển không phải là một việc không cần làm. Nhưng nó không nên sụp đổ. Điều kiện hậu này phù hợp với việc cho phép swap(x, x)
chỉ hoạt động:
template <class T>
void
swap(T& x, T& y)
{
// assume &x == &y
T tmp(std::move(x));
// x and y now have a valid but unspecified state
x = std::move(y);
// x and y still have a valid but unspecified state
y = std::move(tmp);
// x and y have the value of tmp, which is the value they had on entry
}
Những điều trên hoạt động, miễn là x = std::move(x)
không sụp đổ. Nó có thể rời khỏi x
bất kỳ trạng thái hợp lệ nhưng không xác định.
Tôi thấy ba cách để lập trình toán tử gán di chuyển dumb_array
để đạt được điều này:
dumb_array& operator=(dumb_array&& other)
{
delete [] mArray;
// set *this to a valid state before continuing
mSize = 0;
mArray = nullptr;
// *this is now in a valid state, continue with move assignment
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
Việc triển khai ở trên cho phép tự gán, nhưng *this
và other
kết thúc là một mảng có kích thước bằng 0 sau khi tự chuyển, bất kể giá trị ban đầu của *this
là bao nhiêu. Điều này là tốt.
dumb_array& operator=(dumb_array&& other)
{
if (this != &other)
{
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
}
return *this;
}
Việc triển khai ở trên chấp nhận việc tự gán giống như cách mà toán tử gán bản sao thực hiện, bằng cách biến nó thành no-op. Điều này cũng ổn.
dumb_array& operator=(dumb_array&& other)
{
swap(other);
return *this;
}
Ở trên là ok chỉ khi dumb_array
không giữ tài nguyên cần được hủy "ngay lập tức". Ví dụ, nếu tài nguyên duy nhất là bộ nhớ, điều trên là tốt. Nếu dumb_array
có thể giữ khóa mutex hoặc trạng thái đang mở của tệp, khách hàng có thể mong đợi một cách hợp lý những tài nguyên đó trên lhs của nhiệm vụ di chuyển ngay lập tức và do đó việc triển khai này có thể có vấn đề.
Chi phí đầu tiên là hai cửa hàng phụ. Chi phí của thứ hai là thử nghiệm và chi nhánh. Cả hai đều hoạt động. Cả hai đều đáp ứng tất cả các yêu cầu của Bảng 22 Yêu cầu MoveAssignable trong tiêu chuẩn C ++ 11. Thứ ba cũng hoạt động theo mô-đun không quan tâm đến tài nguyên bộ nhớ.
Tất cả ba cách triển khai có thể có chi phí khác nhau tùy thuộc vào phần cứng: Chi nhánh đắt như thế nào? Có rất nhiều đăng ký hay rất ít?
Việc mang đi là việc tự chuyển nhượng, không giống như tự sao chép, không cần phải bảo toàn giá trị hiện tại.
<
/ Cập nhật>
Một bản chỉnh sửa cuối cùng (hy vọng) lấy cảm hứng từ nhận xét của Luc Danton:
Nếu bạn đang viết một lớp cấp cao không trực tiếp quản lý bộ nhớ (nhưng có thể có các cơ sở hoặc thành viên có chức năng này), thì cách triển khai tốt nhất của việc chuyển nhượng thường là:
Class& operator=(Class&&) = default;
Thao tác này sẽ chuyển giao lần lượt từng cơ sở và từng thành viên, và sẽ không bao gồm this != &other
séc. Điều này sẽ mang lại cho bạn hiệu suất cao nhất và an toàn ngoại lệ cơ bản giả sử không có bất biến nào cần được duy trì giữa các căn cứ và thành viên của bạn. Đối với khách hàng của bạn yêu cầu an toàn ngoại lệ mạnh mẽ, hãy hướng họ tới strong_assign
.