Di chuyển ngữ nghĩa không nhất thiết phải là một cải tiến lớn khi bạn trả về một giá trị - và khi / nếu bạn sử dụng shared_ptr
(hoặc một cái gì đó tương tự) có lẽ bạn sẽ sớm bi quan. Trong thực tế, gần như tất cả các trình biên dịch hiện đại hợp lý thực hiện những gì được gọi là Tối ưu hóa giá trị trả về (RVO) và Tối ưu hóa giá trị trả về được đặt tên (NRVO). Điều này có nghĩa rằng khi bạn đang trả lại một giá trị, thay vì thực sự sao chép các giá trị ở tất cả, họ chỉ đơn giản chuyển một con trỏ / tham chiếu ẩn đến nơi giá trị sẽ được chỉ định sau khi trả về và hàm sử dụng giá trị đó để tạo giá trị nơi nó sẽ kết thúc. Tiêu chuẩn C ++ bao gồm các quy định đặc biệt để cho phép điều này, vì vậy ngay cả khi (ví dụ), trình tạo bản sao của bạn có tác dụng phụ có thể nhìn thấy, không bắt buộc phải sử dụng hàm tạo sao chép để trả về giá trị. Ví dụ:
#include <vector>
#include <numeric>
#include <iostream>
#include <stdlib.h>
#include <algorithm>
#include <iterator>
class X {
std::vector<int> a;
public:
X() {
std::generate_n(std::back_inserter(a), 32767, ::rand);
}
X(X const &x) {
a = x.a;
std::cout << "Copy ctor invoked\n";
}
int sum() { return std::accumulate(a.begin(), a.end(), 0); }
};
X func() {
return X();
}
int main() {
X x = func();
std::cout << "sum = " << x.sum();
return 0;
};
Ý tưởng cơ bản ở đây khá đơn giản: tạo một lớp có đủ nội dung, chúng tôi muốn tránh sao chép nó, nếu có thể ( std::vector
chúng tôi điền vào 32767 ints ngẫu nhiên). Chúng tôi có một ctor sao chép rõ ràng sẽ hiển thị cho chúng tôi khi / nếu nó được sao chép. Chúng tôi cũng có thêm một ít mã để làm một cái gì đó với các giá trị ngẫu nhiên trong đối tượng, vì vậy trình tối ưu hóa sẽ không (ít nhất là dễ dàng) loại bỏ mọi thứ về lớp chỉ vì nó không làm gì cả.
Sau đó chúng ta có một số mã để trả về một trong các đối tượng này từ một hàm và sau đó sử dụng tính tổng để đảm bảo đối tượng thực sự được tạo, không chỉ bị bỏ qua hoàn toàn. Khi chúng tôi chạy nó, ít nhất là với hầu hết các trình biên dịch hiện đại / gần đây, chúng tôi thấy rằng trình tạo bản sao mà chúng tôi đã viết không bao giờ chạy cả - và vâng, tôi khá chắc chắn rằng ngay cả một bản sao nhanh với một bản sao shared_ptr
vẫn chậm hơn so với việc không sao chép ở tất cả.
Di chuyển cho phép bạn thực hiện một số lượng lớn những việc bạn không thể làm (trực tiếp) mà không có chúng. Hãy xem xét phần "hợp nhất" của một loại hợp nhất bên ngoài - bạn có 8 tệp bạn sẽ hợp nhất với nhau. Lý tưởng nhất là bạn muốn đặt tất cả 8 tệp đó vào một vector
- nhưng vì vector
( kể từ C ++ 03) cần có thể sao chép các phần tử và ifstream
không thể sao chép, bạn bị mắc kẹt với một số unique_ptr
/ shared_ptr
, hoặc một cái gì đó theo thứ tự đó để có thể đặt chúng vào một vectơ. Lưu ý rằng ngay cả khi (ví dụ) chúng tôi reserve
không gian trong vector
đó vì vậy chúng tôi chắc chắn rằng chúng tôi ifstream
sẽ không bao giờ thực sự bị sao chép, trình biên dịch sẽ không biết điều đó, vì vậy mã sẽ không được biên dịch mặc dù chúng tôi biết rằng hàm tạo sao chép sẽ không bao giờ sử dụng nào.
Mặc dù nó vẫn không thể được sao chép, trong C ++ 11 ifstream
có thể được di chuyển. Trong trường hợp này, các đối tượng có thể sẽ không bao giờ bị di chuyển, nhưng thực tế là chúng có thể nếu trình biên dịch hài lòng, vì vậy chúng ta có thể đặt các ifstream
đối tượng của mình vector
trực tiếp mà không cần hack con trỏ thông minh.
Một vectơ không mở rộng là một ví dụ khá hay về thời gian di chuyển ngữ nghĩa thực sự có thể / rất hữu ích. Trong trường hợp này, RVO / NRVO sẽ không giúp đỡ, vì chúng tôi không xử lý giá trị trả về từ một hàm (hoặc bất cứ thứ gì tương tự). Chúng ta có một vector giữ một số đối tượng và chúng ta muốn di chuyển các đối tượng đó vào một khối bộ nhớ mới, lớn hơn.
Trong C ++ 03, điều đó đã được thực hiện bằng cách tạo các bản sao của các đối tượng trong bộ nhớ mới, sau đó phá hủy các đối tượng cũ trong bộ nhớ cũ. Tuy nhiên, việc tạo ra tất cả những bản sao đó chỉ để vứt bỏ những bản cũ là khá lãng phí thời gian. Trong C ++ 11, bạn có thể mong đợi chúng sẽ được di chuyển thay thế. Điều này thường cho phép chúng ta, về bản chất, thực hiện một bản sao nông thay vì một bản sao sâu (nói chung là chậm hơn nhiều). Nói cách khác, với một chuỗi hoặc vectơ (chỉ một vài ví dụ), chúng ta chỉ sao chép (các) con trỏ trong các đối tượng, thay vì tạo các bản sao của tất cả dữ liệu mà các con trỏ đề cập đến.
shared_ptr
chỉ vì mục đích sao chép nhanh) và nếu ngữ nghĩa di chuyển có thể đạt được điều tương tự mà gần như không có mã hóa, ngữ nghĩa- và sự sạch sẽ-hình phạt.