Để hiểu tại sao đây là một mô hình tốt, chúng ta nên kiểm tra các lựa chọn thay thế, cả trong C ++ 03 và C ++ 11.
Chúng tôi có phương pháp C ++ 03 để lấy std::string const&
:
struct S
{
std::string data;
S(std::string const& str) : data(str)
{}
};
trong trường hợp này, sẽ luôn có một bản sao được thực hiện. Nếu bạn xây dựng từ một chuỗi C thô, a std::string
sẽ được xây dựng, sau đó được sao chép lại: hai phân bổ.
Có phương thức C ++ 03 lấy tham chiếu đến a std::string
, sau đó hoán đổi nó thành cục bộ std::string
:
struct S
{
std::string data;
S(std::string& str)
{
std::swap(data, str);
}
};
đó là phiên bản C ++ 03 của "ngữ nghĩa chuyển động", và swap
thường có thể được tối ưu hóa để thực hiện rất rẻ (giống như a move
). Nó cũng nên được phân tích trong ngữ cảnh:
S tmp("foo"); // illegal
std::string s("foo");
S tmp2(s); // legal
và buộc bạn phải hình thành một không tạm thời std::string
, sau đó loại bỏ nó. (Một tạm thời std::string
không thể liên kết với một tham chiếu không phải const). Tuy nhiên, chỉ có một phân bổ được thực hiện. Phiên bản C ++ 11 sẽ sử dụng một &&
và yêu cầu bạn gọi nó bằng std::move
hoặc với một tạm thời: điều này yêu cầu người gọi tạo một bản sao rõ ràng bên ngoài lệnh gọi và chuyển bản sao đó vào hàm hoặc hàm tạo.
struct S
{
std::string data;
S(std::string&& str): data(std::move(str))
{}
};
Sử dụng:
S tmp("foo"); // legal
std::string s("foo");
S tmp2(std::move(s)); // legal
Tiếp theo, chúng ta có thể tạo phiên bản C ++ 11 đầy đủ, hỗ trợ cả sao chép và move
:
struct S
{
std::string data;
S(std::string const& str) : data(str) {} // lvalue const, copy
S(std::string && str) : data(std::move(str)) {} // rvalue, move
};
Sau đó, chúng ta có thể kiểm tra cách thức này được sử dụng:
S tmp( "foo" ); // a temporary `std::string` is created, then moved into tmp.data
std::string bar("bar"); // bar is created
S tmp2( bar ); // bar is copied into tmp.data
std::string bar2("bar2"); // bar2 is created
S tmp3( std::move(bar2) ); // bar2 is moved into tmp.data
Rõ ràng là 2 kỹ thuật quá tải này ít nhất cũng hiệu quả, nếu không muốn nói là hơn hai kiểu C ++ 03 ở trên. Tôi sẽ gọi phiên bản quá tải 2 này là phiên bản "tối ưu nhất".
Bây giờ, chúng ta sẽ kiểm tra phiên bản từng bản sao:
struct S2 {
std::string data;
S2( std::string arg ):data(std::move(x)) {}
};
trong mỗi tình huống đó:
S2 tmp( "foo" ); // a temporary `std::string` is created, moved into arg, then moved into S2::data
std::string bar("bar"); // bar is created
S2 tmp2( bar ); // bar is copied into arg, then moved into S2::data
std::string bar2("bar2"); // bar2 is created
S2 tmp3( std::move(bar2) ); // bar2 is moved into arg, then moved into S2::data
Nếu bạn so sánh song song phiên bản này với phiên bản "tối ưu nhất", chúng tôi bổ sung chính xác một lần move
! Không phải một lần chúng tôi làm thêm copy
.
Vì vậy, nếu chúng ta giả định rằng đó move
là giá rẻ, phiên bản này mang lại cho chúng ta hiệu suất gần tương đương với phiên bản tối ưu nhất, nhưng ít mã hơn 2 lần.
Và nếu bạn đang sử dụng giả sử từ 2 đến 10 đối số, thì việc giảm mã theo cấp số nhân - giảm 2 lần với 1 đối số, 4x với 2, 8x với 3, 16x với 4, 1024x với 10 đối số.
Bây giờ, chúng ta có thể giải quyết vấn đề này thông qua chuyển tiếp hoàn hảo và SFINAE, cho phép bạn viết một hàm tạo hoặc mẫu hàm duy nhất có 10 đối số, SFINAE có đảm bảo rằng các đối số thuộc loại phù hợp, rồi di chuyển-hoặc sao chép chúng vào trạng thái địa phương theo yêu cầu. Mặc dù điều này ngăn cản vấn đề kích thước chương trình tăng lên hàng nghìn lần, nhưng vẫn có thể có cả đống hàm được tạo từ mẫu này. (khởi tạo hàm mẫu tạo ra các hàm)
Và nhiều hàm được tạo ra có nghĩa là kích thước mã thực thi lớn hơn, chính nó có thể làm giảm hiệu suất.
Đối với chi phí của một vài move
giây, chúng tôi nhận được mã ngắn hơn và hiệu suất gần như giống nhau và mã thường dễ hiểu hơn.
Bây giờ, điều này chỉ hoạt động vì chúng ta biết, khi hàm (trong trường hợp này là một hàm tạo) được gọi, chúng ta sẽ muốn một bản sao cục bộ của đối số đó. Ý tưởng là nếu chúng ta biết rằng chúng ta sẽ tạo một bản sao, chúng ta nên cho người gọi biết rằng chúng ta đang tạo một bản sao bằng cách đưa nó vào danh sách đối số của chúng ta. Sau đó, họ có thể tối ưu hóa thực tế là họ sẽ cung cấp cho chúng ta một bản sao (ví dụ: bằng cách chuyển sang đối số của chúng ta).
Một ưu điểm khác của kỹ thuật 'lấy theo giá trị' là các hàm tạo di chuyển thường không được chấp nhận. Điều đó có nghĩa là các hàm nhận theo giá trị và di chuyển ra khỏi đối số của chúng thường có thể không được chấp nhận, di chuyển bất kỳ hàm nào throw
ra khỏi phần thân của chúng và vào phạm vi gọi (đôi khi ai có thể tránh nó bằng cách xây dựng trực tiếp, hoặc xây dựng các mục và move
vào đối số, để kiểm soát nơi xảy ra ném).