TL; DR: Chuyển qua tham chiếu const vẫn là một ý tưởng tốt trong C ++, tất cả mọi thứ được xem xét. Không phải là một tối ưu hóa sớm.
TL; DR2: Hầu hết các câu ngạn ngữ đều không có ý nghĩa, cho đến khi chúng thực hiện.
Mục đích
Câu trả lời này chỉ cố gắng mở rộng mục được liên kết trên Nguyên tắc cốt lõi C ++ (lần đầu tiên được đề cập trong bình luận của amon) một chút.
Câu trả lời này không cố gắng giải quyết vấn đề làm thế nào để suy nghĩ và áp dụng đúng các câu ngạn ngữ khác nhau được lưu hành rộng rãi trong giới lập trình viên, đặc biệt là vấn đề hòa giải giữa các kết luận hoặc bằng chứng mâu thuẫn.
Khả năng ứng dụng
Câu trả lời này chỉ áp dụng cho các lệnh gọi hàm (phạm vi lồng nhau không thể tháo rời trên cùng một luồng).
(Lưu ý bên cạnh.) Khi những thứ có thể vượt qua có thể thoát khỏi phạm vi (nghĩa là có thời gian tồn tại vượt quá phạm vi bên ngoài), điều quan trọng hơn là đáp ứng nhu cầu của ứng dụng về quản lý trọn đời đối tượng trước mọi thứ khác. Thông thường, điều này đòi hỏi sử dụng các tài liệu tham khảo cũng có khả năng quản lý trọn đời, chẳng hạn như con trỏ thông minh. Một sự thay thế có thể là sử dụng một người quản lý. Lưu ý rằng, lambda là một loại phạm vi có thể tháo rời; lambda chụp hành xử như có phạm vi đối tượng. Do đó, hãy cẩn thận với chụp lambda. Ngoài ra, hãy cẩn thận với cách lambda được thông qua - bằng bản sao hoặc bằng cách tham khảo.
Khi nào vượt qua giá trị
Đối với các giá trị là vô hướng (nguyên thủy chuẩn phù hợp với thanh ghi máy và có giá trị ngữ nghĩa) mà không cần giao tiếp bằng cách biến đổi (tham chiếu chia sẻ), hãy chuyển theo giá trị.
Đối với các tình huống trong đó callee yêu cầu nhân bản một đối tượng hoặc tổng hợp, chuyển qua giá trị, trong đó bản sao của callee đáp ứng nhu cầu cho một đối tượng nhân bản.
Khi nào vượt qua bằng cách tham khảo, vv
đối với tất cả các tình huống khác, chuyển qua con trỏ, tham chiếu, con trỏ thông minh, tay cầm (xem: thành ngữ xử lý cơ thể), v.v ... Bất cứ khi nào lời khuyên này được thực hiện, hãy áp dụng nguyên tắc chính xác như bình thường.
Những thứ (tập hợp, đối tượng, mảng, cấu trúc dữ liệu) đủ lớn về dung lượng bộ nhớ phải luôn được thiết kế để tạo điều kiện thuận lợi cho việc chuyển qua tham chiếu, vì lý do hiệu suất. Lời khuyên này chắc chắn áp dụng khi nó có hàng trăm byte trở lên. Lời khuyên này là đường biên khi nó là hàng chục byte.
Mô hình bất thường
Có những mô hình lập trình mục đích đặc biệt nặng về sao chép theo ý định. Ví dụ: xử lý chuỗi, tuần tự hóa, giao tiếp mạng, cách ly, gói thư viện của bên thứ ba, giao tiếp giữa các bộ nhớ dùng chung, v.v. mảng byte.
Làm thế nào đặc tả ngôn ngữ ảnh hưởng đến câu trả lời này, trước khi tối ưu hóa được xem xét.
Sub-TL; DR Tuyên truyền một tham chiếu sẽ không gọi mã; đi qua tham chiếu const thỏa mãn tiêu chí này. Tuy nhiên, tất cả các ngôn ngữ khác đáp ứng tiêu chí này một cách dễ dàng.
(Các lập trình viên Novice C ++ nên bỏ qua phần này hoàn toàn.)
(Phần đầu của phần này được lấy cảm hứng một phần từ câu trả lời của gnasher729. Tuy nhiên, đã có một kết luận khác.)
C ++ cho phép các hàm tạo sao chép do người dùng định nghĩa và các toán tử gán.
(Đây là (là) một sự lựa chọn táo bạo (vừa) vừa tuyệt vời vừa đáng tiếc. Nó chắc chắn là một sự khác biệt so với chuẩn mực chấp nhận được ngày nay trong thiết kế ngôn ngữ.)
Ngay cả khi lập trình viên C ++ không định nghĩa một, trình biên dịch C ++ phải tạo ra các phương thức như vậy dựa trên các nguyên tắc ngôn ngữ, và sau đó xác định xem có cần thực thi mã bổ sung khác không memcpy
. Ví dụ, a class
/ struct
có chứa một std::vector
thành viên phải có một hàm tạo sao chép và một toán tử gán không phải là nhỏ.
Trong các ngôn ngữ khác, các hàm tạo sao chép và nhân bản đối tượng không được khuyến khích (trừ khi thực sự cần thiết và / hoặc có ý nghĩa đối với ngữ nghĩa của ứng dụng), bởi vì các đối tượng có ngữ nghĩa tham chiếu, theo thiết kế ngôn ngữ. Các ngôn ngữ này thường sẽ có cơ chế thu gom rác dựa trên khả năng tiếp cận thay vì quyền sở hữu dựa trên phạm vi hoặc tính tham chiếu.
Khi một tham chiếu hoặc con trỏ (bao gồm tham chiếu const) được truyền xung quanh trong C ++ (hoặc C), lập trình viên được đảm bảo rằng sẽ không có mã đặc biệt nào (các hàm do người dùng xác định hoặc do trình biên dịch tạo ra), ngoại trừ việc truyền giá trị địa chỉ (tham chiếu hoặc con trỏ). Đây là một sự rõ ràng về hành vi mà các lập trình viên C ++ cảm thấy thoải mái.
Tuy nhiên, bối cảnh là ngôn ngữ C ++ phức tạp không cần thiết, do đó, sự rõ ràng trong hành vi này giống như một ốc đảo (môi trường sống có thể sống sót) ở đâu đó quanh khu vực bụi hạt nhân.
Để thêm nhiều phước lành (hoặc xúc phạm), C ++ giới thiệu các tham chiếu phổ quát (giá trị r) để tạo điều kiện cho các toán tử di chuyển do người dùng xác định (hàm tạo di chuyển và toán tử gán chuyển động) có hiệu suất tốt. Điều này có lợi cho trường hợp sử dụng có liên quan cao (việc di chuyển (chuyển) các đối tượng từ thể hiện này sang thể hiện khác), bằng cách giảm nhu cầu sao chép và nhân bản sâu. Tuy nhiên, trong các ngôn ngữ khác, thật phi logic khi nói về sự di chuyển của các vật thể như vậy.
(Phần ngoài chủ đề) Một phần dành riêng cho một bài viết, "Muốn tốc độ? Vượt qua giá trị!" được viết vào khoảng năm 2009.
Bài viết đó được viết vào năm 2009 và giải thích sự biện minh thiết kế cho giá trị r trong C ++. Bài viết đó trình bày một lập luận phản biện hợp lệ cho kết luận của tôi trong phần trước. Tuy nhiên, ví dụ mã của bài viết và yêu cầu về hiệu suất đã bị từ chối.
Sub-TL; DR Việc thiết kế ngữ nghĩa giá trị r trong C ++ cho phép một ngữ nghĩa phía người dùng thanh lịch đáng ngạc nhiên trên một Sort
chức năng, ví dụ. Thanh lịch này là không thể mô hình (bắt chước) trong các ngôn ngữ khác.
Một chức năng sắp xếp được áp dụng cho toàn bộ cấu trúc dữ liệu. Như đã đề cập ở trên, sẽ rất chậm nếu có nhiều sự sao chép có liên quan. Là một tối ưu hóa hiệu suất (có liên quan thực tế), một hàm sắp xếp được thiết kế để phá hủy trong khá nhiều ngôn ngữ khác ngoài C ++. Phá hủy có nghĩa là cấu trúc dữ liệu đích được sửa đổi để đạt được mục tiêu sắp xếp.
Trong C ++, người dùng có thể chọn gọi một trong hai cách triển khai: một cách thực hiện phá hủy với hiệu suất tốt hơn hoặc một cách bình thường không sửa đổi đầu vào. (Mẫu được bỏ qua cho ngắn gọn.)
/*caller specifically passes in input argument destructively*/
std::vector<T> my_sort(std::vector<T>&& input)
{
std::vector<T> result(std::move(input)); /* destructive move */
std::sort(result.begin(), result.end()); /* in-place sorting */
return result; /* return-value optimization (RVO) */
}
/*caller specifically passes in read-only argument*/
std::vector<T> my_sort(const std::vector<T>& input)
{
/* reuse destructive implementation by letting it work on a clone. */
/* Several things involved; e.g. expiring temporaries as r-value */
/* return-value optimization, etc. */
return my_sort(std::vector<T>(input));
}
/*caller can select which to call, by selecting r-value*/
std::vector<T> v1 = {...};
std::vector<T> v2 = my_sort(v1); /*non-destructive*/
std::vector<T> v3 = my_sort(std::move(v1)); /*v1 is gutted*/
Bên cạnh việc sắp xếp, sự tao nhã này cũng hữu ích trong việc thực hiện thuật toán tìm trung bình phá hoại trong một mảng (ban đầu chưa được sắp xếp), bằng cách phân vùng đệ quy.
Tuy nhiên, lưu ý rằng, hầu hết các ngôn ngữ sẽ áp dụng cách tiếp cận cây tìm kiếm nhị phân cân bằng để sắp xếp, thay vì áp dụng thuật toán sắp xếp phá hủy cho mảng. Do đó, sự liên quan thực tế của kỹ thuật này không cao như nó có vẻ.
Làm thế nào tối ưu hóa trình biên dịch ảnh hưởng đến câu trả lời này
Khi nội tuyến (và cả tối ưu hóa toàn bộ chương trình / tối ưu hóa thời gian liên kết) được áp dụng trên một số mức gọi hàm, trình biên dịch có thể thấy (đôi khi triệt để) luồng dữ liệu. Khi điều này xảy ra, trình biên dịch có thể áp dụng nhiều tối ưu hóa, một số trong đó có thể loại bỏ việc tạo toàn bộ các đối tượng trong bộ nhớ. Thông thường, khi tình huống này được áp dụng, sẽ không có vấn đề gì nếu các tham số được truyền theo giá trị hoặc bằng tham chiếu const, bởi vì trình biên dịch có thể phân tích toàn diện.
Tuy nhiên, nếu hàm cấp thấp hơn gọi thứ gì đó nằm ngoài phân tích (ví dụ: thứ gì đó trong thư viện khác ngoài biên dịch hoặc biểu đồ cuộc gọi quá đơn giản), thì trình biên dịch phải tối ưu hóa phòng thủ.
Các đối tượng lớn hơn giá trị thanh ghi máy có thể được sao chép theo hướng dẫn tải / lưu trữ bộ nhớ rõ ràng hoặc bằng một cuộc gọi đến memcpy
chức năng đáng kính . Trên một số nền tảng, trình biên dịch tạo các lệnh SIMD để di chuyển giữa hai vị trí bộ nhớ, mỗi lệnh di chuyển hàng chục byte (16 hoặc 32).
Thảo luận về vấn đề dài dòng hoặc lộn xộn thị giác
Các lập trình viên C ++ đã quen với điều này, tức là miễn là một lập trình viên không ghét C ++, thì việc viết hoặc đọc tham chiếu const trong mã nguồn không phải là khủng khiếp.
Các phân tích lợi ích chi phí có thể đã được thực hiện nhiều lần trước đây. Tôi không biết nếu có bất kỳ nghiên cứu khoa học nào cần được trích dẫn. Tôi đoán hầu hết các phân tích sẽ là không khoa học hoặc không thể tái sản xuất.
Đây là những gì tôi tưởng tượng (không có tài liệu tham khảo bằng chứng hoặc đáng tin cậy) ...
- Có, nó ảnh hưởng đến hiệu suất của phần mềm được viết bằng ngôn ngữ này.
- Nếu trình biên dịch có thể hiểu mục đích của mã, thì nó có khả năng đủ thông minh để tự động hóa điều đó
- Thật không may, trong các ngôn ngữ thiên về tính đột biến (trái ngược với độ tinh khiết của chức năng), trình biên dịch sẽ phân loại hầu hết mọi thứ là bị đột biến, do đó, việc khấu trừ tự động sẽ loại bỏ hầu hết mọi thứ là không phải là hằng
- Chi phí tinh thần phụ thuộc vào con người; những người nhận thấy điều này là một chi phí tinh thần cao sẽ từ chối C ++ như một ngôn ngữ lập trình khả thi.