Làm thế nào để đảm bảo tách bản sao hoạt động?


89

Tại cuộc họp Tiêu chuẩn ISO C ++ của Oulu năm 2016, một đề xuất có tên là Đảm bảo tách bản sao thông qua các danh mục giá trị được đơn giản hóa đã được ủy ban tiêu chuẩn bỏ phiếu thành C ++ 17.

Làm thế nào chính xác làm việc tách bản sao đảm bảo hoạt động? Nó có đề cập đến một số trường hợp mà bản sao đã được cho phép hay cần thay đổi mã để đảm bảo sao chép không?

Câu trả lời:


129

Việc xóa bản sao được phép xảy ra trong một số trường hợp. Tuy nhiên, ngay cả khi nó được cho phép, mã vẫn phải có thể hoạt động như thể bản sao chưa được giải thích. Cụ thể, phải có một bản sao có thể truy cập và / hoặc phương thức khởi tạo di chuyển.

Bản sao bảo đảm elision xác định lại một số khái niệm C ++, sao cho một số trường hợp nhất định trong đó các bản sao / di chuyển có thể được giải quyết không thực sự gây ra một bản sao / di chuyển nào cả . Trình biên dịch không tạo ra một bản sao; tiêu chuẩn nói rằng không bao giờ có thể xảy ra việc sao chép như vậy.

Hãy xem xét chức năng này:

T Func() {return T();}

Theo quy tắc xóa bản sao không được đảm bảo, điều này sẽ tạo một giá trị tạm thời, sau đó chuyển từ tạm thời đó sang giá trị trả về của hàm. Thao tác di chuyển đó có thể được giải quyết, nhưng Tvẫn phải có một phương thức khởi tạo di chuyển có thể truy cập ngay cả khi nó chưa bao giờ được sử dụng.

Tương tự:

T t = Func();

Đây là bản sao khởi tạo của t. Điều này sẽ sao chép khởi tạo tvới giá trị trả về Func. Tuy nhiên, Tvẫn phải có một phương thức khởi tạo di chuyển, mặc dù nó sẽ không được gọi.

Bản sao bảo đảm elision xác định lại ý nghĩa của một biểu thức prvalue . Pre-C ++ 17, prvalues ​​là các đối tượng tạm thời. Trong C ++ 17, một biểu prvalue chỉ đơn thuần là một cái gì đó mà có thể hiện thực một tạm thời, nhưng nó không phải là một tạm thời được nêu ra.

Nếu bạn sử dụng prvalue để khởi tạo một đối tượng thuộc kiểu của prvalue, thì không có tạm thời nào được thực hiện. Khi bạn làm như vậy return T();, điều này sẽ khởi tạo giá trị trả về của hàm thông qua một prvalue. Vì hàm đó trả về Tnên không có tạm thời nào được tạo; việc khởi tạo prvalue chỉ đơn giản là trực tiếp khởi tạo giá trị trả về.

Điều cần hiểu là, vì giá trị trả về là prvalue nên nó chưa phải là một đối tượng . Nó chỉ là một bộ khởi tạo cho một đối tượng, giống như vậy T().

Khi bạn làm như vậy T t = Func();, prvalue của giá trị trả về sẽ trực tiếp khởi tạo đối tượng t; không có giai đoạn "tạo tạm thời và sao chép / di chuyển". Vì Func()giá trị trả về của là một prvalue tương đương với T(), tđược khởi tạo trực tiếp bởi T(), chính xác như thể bạn đã làm T t = T().

Nếu một prvalue được sử dụng theo bất kỳ cách nào khác, prvalue sẽ hiện thực hóa một đối tượng tạm thời, đối tượng này sẽ được sử dụng trong biểu thức đó (hoặc bị loại bỏ nếu không có biểu thức). Vì vậy, nếu bạn đã làm vậy const T &rt = Func();, prvalue sẽ hiện thực hóa một tệp tạm thời (sử dụng T()làm trình khởi tạo), tham chiếu của nó sẽ được lưu trữ trong đó rt, cùng với nội dung mở rộng vòng đời tạm thời thông thường.

Một điều được đảm bảo elision cho phép bạn làm là trả về các đối tượng bất động. Ví dụ: lock_guardkhông thể sao chép hoặc di chuyển, vì vậy bạn không thể có một hàm trả về nó theo giá trị. Nhưng với đảm bảo tách bản sao, bạn có thể.

Sự tách rời đảm bảo cũng hoạt động với quá trình khởi tạo trực tiếp:

new T(FactoryFunction());

Nếu FactoryFunctiontrả về Ttheo giá trị, biểu thức này sẽ không sao chép giá trị trả về vào bộ nhớ được cấp phát. Thay vào đó, nó sẽ cấp phát bộ nhớ và sử dụng bộ nhớ được cấp phát làm bộ nhớ giá trị trả về cho lệnh gọi hàm trực tiếp.

Vì vậy, các hàm gốc trả về theo giá trị có thể trực tiếp khởi tạo bộ nhớ được cấp phát heap mà không cần biết về nó. Tất nhiên, miễn là các chức năng này tuân theo các quy tắc nội bộ của việc tách bản sao được đảm bảo. Họ phải trả về một loại giá trị prvalue T.

Tất nhiên, điều này cũng hoạt động:

new auto(FactoryFunction());

Trong trường hợp bạn không thích viết tên kiểu chữ.


Điều quan trọng là phải nhận ra rằng các đảm bảo trên chỉ hoạt động đối với các giá trị tương đương. Đó là, bạn không có gì đảm bảo khi trả về một biến được đặt tên :

T Func()
{
   T t = ...;
   ...
   return t;
}

Trong trường hợp này, tvẫn phải có một hàm tạo sao chép / di chuyển có thể truy cập được. Có, trình biên dịch có thể chọn để tối ưu hóa việc sao chép / di chuyển. Nhưng trình biên dịch vẫn phải xác minh sự tồn tại của một hàm tạo sao chép / di chuyển có thể truy cập được.

Vì vậy, không có gì thay đổi đối với tối ưu hóa giá trị trả về được đặt tên (NRVO).


1
@BenVoigt: Đưa các kiểu do người dùng định nghĩa có thể sao chép không thể sao chép được vào thanh ghi không phải là điều khả thi mà ABI có thể làm, cho dù có sẵn hay không.
Nicol Bolas

1
Giờ đây, các quy tắc đã được công khai, có thể đáng để cập nhật điều này với khái niệm "prvalues ​​là lần khởi tạo".
Johannes Schaub - litb

6
@ JohannesSchaub-litb: Nó chỉ "mơ hồ" nếu bạn biết quá nhiều về những chi tiết vụn vặt của tiêu chuẩn C ++. Đối với 99% cộng đồng C ++, chúng tôi biết "xóa bản sao được đảm bảo" đề cập đến điều gì. Bài báo thực tế đề xuất tính năng thậm chí có tiêu đề "Bản sao bảo đảm Elision". Việc thêm "thông qua các danh mục giá trị được đơn giản hóa" chỉ đơn thuần khiến người dùng khó hiểu và khó hiểu. Ngoài ra, đó là một từ nhầm lẫn, vì các quy tắc này không thực sự "đơn giản hóa" các quy tắc xung quanh danh mục giá trị. Cho dù bạn muốn hay không, thuật ngữ "đảm bảo tách bản sao" đề cập đến tính năng này và không có gì khác.
Nicol Bolas

1
Tôi rất muốn có thể chọn một giá trị và mang nó đi khắp nơi. Tôi đoán rằng đây chỉ là một (một lần) std::function<T()>thực sự.
Yakk - Adam Nevraumont

1
@LukasSalich: Đó là một câu hỏi C ++ 11. Câu trả lời này là về một tính năng C ++ 17.
Nicol Bolas
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.