Lý do cho việc không sử dụng [[gật đầu]] của C ++ gần như ở mọi nơi trong mã mới?


70

C ++ 17 giới thiệu [[nodiscard]]thuộc tính, cho phép lập trình viên đánh dấu các hàm theo cách trình biên dịch tạo cảnh báo nếu đối tượng trả về bị loại bỏ bởi người gọi; cùng một thuộc tính có thể được thêm vào toàn bộ một loại lớp.

Tôi đã đọc về động lực cho tính năng này trong đề xuất ban đầu và tôi biết rằng C ++ 20 sẽ thêm thuộc tính vào các hàm tiêu chuẩn như std::vector::empty, tên của chúng không truyền đạt ý nghĩa rõ ràng về giá trị trả về.

Đó là một tính năng thú vị và hữu ích. Trong thực tế, nó gần như quá hữu ích. Ở mọi nơi tôi đọc [[nodiscard]], mọi người thảo luận về nó như thể bạn chỉ cần thêm nó vào một vài chức năng hoặc loại đã chọn và quên đi phần còn lại. Nhưng tại sao một giá trị không thể loại bỏ phải là một trường hợp đặc biệt, đặc biệt là khi viết mã mới? Không phải là một giá trị trả lại bị loại bỏ thường là một lỗi hoặc ít nhất là lãng phí tài nguyên?

Và không phải là một trong những nguyên tắc thiết kế của chính C ++ mà trình biên dịch sẽ bắt càng nhiều lỗi càng tốt?

Nếu vậy, tại sao không thêm [[nodiscard]]mã không thuộc tính kế thừa của riêng bạn vào hầu hết mọi loại không voidchức năng và hầu hết mọi loại lớp đơn?

Tôi đã cố gắng làm điều đó bằng mã của riêng mình và nó hoạt động tốt, ngoại trừ nó quá dài đến nỗi nó bắt đầu giống như Java. Theo mặc định, có vẻ tự nhiên hơn nhiều khi trình biên dịch cảnh báo về các giá trị trả về bị loại bỏ theo mặc định trừ một số trường hợp khác mà bạn đánh dấu ý định của mình [*] .

Vì tôi đã thấy không có cuộc thảo luận nào về khả năng này trong các đề xuất tiêu chuẩn, mục blog, câu hỏi về Stack Overflow hoặc bất cứ nơi nào khác trên internet, tôi phải thiếu một cái gì đó.

Tại sao các cơ chế như vậy sẽ không có ý nghĩa trong mã C ++ mới? Có phải sự dài dòng là lý do duy nhất để không sử dụng [[nodiscard]]hầu hết mọi nơi?


[*] Về lý thuyết, bạn có thể có một cái gì đó giống như một [[maydiscard]]thuộc tính thay vào đó, cũng có thể được thêm hồi tố vào các chức năng như printftrong triển khai thư viện chuẩn.


22
Vâng, có rất nhiều chức năng trong đó giá trị trả về thể bị loại bỏ một cách hợp lý. operator =ví dụ. Và std::map::insert.
dùng253751

2
@immibis: Đúng. Thư viện tiêu chuẩn có đầy đủ các chức năng như vậy. Nhưng trong mã của riêng tôi, chúng có xu hướng cực kỳ hiếm; Bạn có nghĩ rằng đó là vấn đề của mã thư viện so với mã ứng dụng?
Christian Hackl

9
"... Thật dài dòng rằng nó bắt đầu giống như Java ..." - đọc điều này trong một câu hỏi về C ++ khiến tôi co giật một chút: - /
Marco13

3
@ChristianHackl Các ý kiến ​​có thể không phải là nơi thích hợp cho "bashing ngôn ngữ", và tất nhiên, Java cũng dài dòng so với các ngôn ngữ khác. Nhưng các tệp tiêu đề, toán tử gán, các hàm tạo sao chép và cách sử dụng đúng constcó thể làm mờ một lớp "đơn giản" khác (hay đúng hơn là "đối tượng dữ liệu cũ đơn giản") đáng kể trong C ++.
Marco13

2
@ Marco13: Tôi không thể giải quyết nhiều điểm trong một bình luận. Chúng ta hãy nói ngắn gọn: Java không có các tệp tiêu đề, làm giảm số lượng tệp nhưng tăng khả năng ghép nối; OTOH nó buộc bạn phải đặt mọi lớp cấp cao nhất vào tệp riêng của mình và mọi hàm trong một lớp. Trong C ++, bạn hầu như không bao giờ tự thực hiện các toán tử gán và sao chép hàm tạo; các hàm đó phù hợp hơn với các lớp thư viện chuẩn như std::vectorhoặc std::unique_ptr, trong đó bạn chỉ cần các thành viên dữ liệu trong định nghĩa lớp của mình. Tôi đã làm việc với cả hai ngôn ngữ; Java là một ngôn ngữ ổn, nhưng nó dài dòng hơn.
Christian Hackl

Câu trả lời:


73

Trong mã mới không cần tương thích với các tiêu chuẩn cũ hơn, hãy sử dụng thuộc tính đó bất cứ nơi nào hợp lý. Nhưng đối với C ++, [[nodiscard]]làm cho một mặc định xấu. Bạn đề nghị:

Nó có vẻ tự nhiên hơn nhiều để làm cho trình biên dịch cảnh báo về các giá trị trả về bị loại bỏ theo mặc định ngoại trừ một vài trường hợp khác mà bạn đánh dấu ý định của mình.

Điều đó sẽ đột nhiên gây ra mã chính xác hiện có để phát ra nhiều cảnh báo. Mặc dù về mặt kỹ thuật có thể được coi là tương thích ngược vì bất kỳ mã hiện có nào vẫn biên dịch thành công, đó sẽ là một thay đổi lớn về ngữ nghĩa trong thực tế.

Các quyết định thiết kế cho một ngôn ngữ hiện có, trưởng thành với cơ sở mã rất lớn nhất thiết phải khác với các quyết định thiết kế cho một ngôn ngữ hoàn toàn mới. Nếu đây là một ngôn ngữ mới, thì cảnh báo theo mặc định sẽ hợp lý. Ví dụ, ngôn ngữ Nim yêu cầu loại bỏ các giá trị không cần thiết một cách rõ ràng - điều này sẽ tương tự như gói mọi câu lệnh biểu thức trong C ++ bằng một biểu mẫu (void)(...).

Một [[nodiscard]]thuộc tính hữu ích nhất trong hai trường hợp:

  • nếu một hàm không có hiệu ứng nào ngoài việc trả về một kết quả nhất định, tức là hoàn toàn. Nếu kết quả không được sử dụng, cuộc gọi chắc chắn là vô ích. Mặt khác, loại bỏ kết quả sẽ không chính xác.

  • nếu giá trị trả về phải được kiểm tra, ví dụ: đối với giao diện giống như C trả về mã lỗi thay vì ném. Đây là trường hợp sử dụng chính. Đối với C ++ thành ngữ, điều đó sẽ khá hiếm.

Hai trường hợp này để lại một không gian lớn các hàm không tinh khiết trả về giá trị, trong đó cảnh báo như vậy sẽ gây hiểu nhầm. Ví dụ:

  • Xem xét một kiểu dữ liệu hàng đợi với một .pop()phương thức loại bỏ một phần tử và trả về một bản sao của phần tử bị loại bỏ. Phương pháp như vậy thường thuận tiện. Tuy nhiên, có một số trường hợp chúng tôi chỉ muốn xóa phần tử mà không nhận được bản sao. Điều đó là hoàn toàn hợp pháp, và một cảnh báo sẽ không hữu ích. Một thiết kế khác (chẳng hạn như std::vector) phân chia các trách nhiệm này, nhưng điều đó có sự đánh đổi khác.

    Lưu ý rằng trong một số trường hợp, một bản sao phải được tạo bằng mọi cách vì vậy nhờ RVO trả lại bản sao sẽ được miễn phí.

  • Xem xét các giao diện trôi chảy, trong đó mỗi hoạt động trả về đối tượng để có thể thực hiện các hoạt động tiếp theo. Trong C ++, ví dụ phổ biến nhất là toán tử chèn luồng <<. Sẽ rất cồng kềnh khi thêm một [[nodiscard]]thuộc tính cho mỗi và <<quá tải.

Những ví dụ này chứng minh rằng việc biên dịch mã C ++ thành ngữ mà không có cảnh báo theo một C ++ 17 bằng ngôn ngữ mặc định sẽ rất tẻ nhạt.

Lưu ý rằng mã C ++ 17 mới sáng bóng của bạn (nơi bạn có thể sử dụng các thuộc tính này) vẫn có thể được biên dịch cùng với các thư viện nhắm mục tiêu các tiêu chuẩn C ++ cũ hơn. Khả năng tương thích ngược này rất quan trọng đối với hệ sinh thái C / C ++. Vì vậy, làm cho gật đầu mặc định sẽ dẫn đến nhiều cảnh báo cho các trường hợp sử dụng thành ngữ, điển hình - cảnh báo mà bạn không thể sửa mà không thay đổi sâu rộng đến mã nguồn của thư viện.

Có thể cho rằng, vấn đề ở đây không phải là sự thay đổi về ngữ nghĩa mà là các tính năng của từng tiêu chuẩn C ++ áp dụng trên phạm vi đơn vị biên dịch và không phải trên phạm vi mỗi tệp. Nếu / khi một số tiêu chuẩn C ++ trong tương lai chuyển khỏi các tệp tiêu đề, thì một thay đổi như vậy sẽ thực tế hơn.


6
Tôi đã nêu lên điều này, vì nó chứa những hiểu biết hữu ích. Chưa chắc chắn nếu nó thực sự trả lời câu hỏi, mặc dù. Các hàm không tinh khiết như pophoặc operator<<, những hàm này sẽ được đánh dấu rõ ràng bằng [[maydiscard]]thuộc tính tưởng tượng của tôi , do đó không có cảnh báo nào được tạo. Bạn có nói rằng các hàm như vậy thường phổ biến hơn nhiều so với tôi muốn tin, hoặc nói chung phổ biến hơn nhiều so với các cơ sở mã của riêng tôi? Đoạn đầu tiên của bạn về mã hiện tại chắc chắn là đúng, nhưng vẫn khiến tôi tự hỏi liệu có ai khác hiện đang tiêu tất cả mã mới của họ không [[nodiscard]], và nếu không, tại sao không.
Christian Hackl

23
@ChristianHackl: Có một thời gian trong lịch sử của C ++, nơi nó được coi là thông lệ tốt để trả về các giá trị như const. Bạn không thể nói returns_an_int() = 0, vậy tại sao nên returns_a_str() = ""làm việc? C ++ 11 cho thấy đây thực sự là một ý tưởng tồi tệ, bởi vì bây giờ tất cả các mã này đều cấm di chuyển vì các giá trị là const. Tôi nghĩ rằng việc bỏ đi thích hợp từ bài học đó là "nó không phụ thuộc vào bạn những gì người gọi của bạn làm với kết quả của bạn". Bỏ qua kết quả là một điều hoàn toàn hợp lệ mà người gọi có thể muốn làm, và trừ khi bạn biết nó sai (một thanh rất cao), bạn không nên chặn nó.
GManNickG

13
Một cái gật đầu empty()cũng hợp lý bởi vì cái tên khá mơ hồ: nó là một vị ngữ is_empty()hay nó là một người đột biến clear()? Thông thường, bạn sẽ không mong đợi một trình biến đổi như vậy trả lại bất cứ điều gì, vì vậy một cảnh báo về giá trị trả về có thể cảnh báo cho lập trình viên về một vấn đề. Với một tên rõ ràng hơn, thuộc tính này sẽ không cần thiết.
amon

13
@ChristianHackl: Như những người khác đã nói, tôi nghĩ rằng các hàm thuần túy là một ví dụ tốt về việc khi nào nên sử dụng nó. Tôi sẽ đi một bước xa hơn và nói rằng nên có một [[pure]]thuộc tính, và nó nên ngụ ý [[nodiscard]]. Theo cách đó, rõ ràng tại sao kết quả này không nên bị loại bỏ. Nhưng khác với các hàm thuần túy, tôi không thể nghĩ ra một loại hàm nào khác có hành vi này. Và hầu hết các chức năng không thuần túy, do đó tôi không nghĩ rằng gật đầu như một mặc định là lựa chọn đúng đắn.
GManNickG

5
@GManNickG: Đã thử và thất bại trong việc gỡ lỗi mã mà bỏ qua mã lỗi, tôi tin chắc rằng phần lớn các mã lỗi cần phải được kiểm tra theo mặc định.
Vịt Mooing

9

Những lý do tôi [[nodiscard]]hầu như không ở khắp mọi nơi:

  1. (lớn :) Điều này sẽ giới thiệu cách quá nhiều tiếng ồn trong tiêu đề của tôi.
  2. (chính :) Tôi không cảm thấy mình nên đưa ra các giả định đầu cơ mạnh mẽ về mã của người khác. Bạn muốn loại bỏ giá trị trả lại tôi cung cấp cho bạn? Tốt thôi, đánh gục mình đi.
  3. (nhỏ :) Bạn sẽ đảm bảo không tương thích với C ++ 14

Bây giờ, nếu bạn đặt mặc định, bạn sẽ buộc tất cả các nhà phát triển thư viện buộc tất cả người dùng của họ không loại bỏ các giá trị trả về. Điều đó thật kinh khủng. Hoặc bạn buộc họ phải thêm [[maydiscard]]vô số chức năng, đó cũng là loại kinh khủng.


0

Ví dụ: toán tử << có giá trị trả về phụ thuộc vào cuộc gọi hoặc hoàn toàn cần thiết hoặc hoàn toàn vô dụng. (std :: cout << x << y, cái đầu tiên là cần thiết bởi vì nó trả về luồng, cái thứ hai không có ích gì cả). Bây giờ so sánh với printf nơi mọi người loại bỏ giá trị trả về có để kiểm tra lỗi, nhưng sau đó toán tử << không có kiểm tra lỗi vì vậy nó không hữu ích ở nơi đầu tiên. Vì vậy, trong cả hai trường hợp, gật đầu sẽ chỉ gây hại.


Điều này trả lời một câu hỏi không được hỏi, tức là "lý do không sử dụng [[nodiscard]]ở mọi nơi?".
Christian Hackl
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.