Jaki jest powód nieużywania [[nodiscard]] C ++ 17 prawie wszędzie w nowym kodzie?

70

C ++ 17 wprowadza [[nodiscard]]atrybut, który pozwala programistom oznaczać funkcje w taki sposób, że kompilator generuje ostrzeżenie, jeśli zwracany obiekt zostanie odrzucony przez program wywołujący; ten sam atrybut można dodać do całego typu klasy.

Przeczytałem o motywacji tej funkcji w oryginalnej propozycji i wiem, że C ++ 20 doda atrybut do standardowych funkcji, takich jak std::vector::empty, których nazwy nie przekazują jednoznacznego znaczenia w odniesieniu do wartości zwracanej.

To fajna i przydatna funkcja. W rzeczywistości wydaje się to zbyt przydatne. Gdziekolwiek czytam [[nodiscard]], ludzie dyskutują o tym, jakbyś po prostu dodał go do kilku wybranych funkcji lub typów i zapomniał o reszcie. Ale dlaczego wartość nie do odrzucenia powinna być szczególnym przypadkiem, szczególnie podczas pisania nowego kodu? Czy odrzucona wartość zwracana nie jest zazwyczaj błędem lub marnowaniem zasobów?

I czy nie jest jedną z zasad projektowania samego C ++, że kompilator powinien wychwytywać jak najwięcej błędów?

Jeśli tak, to dlaczego nie dodać [[nodiscard]]własnego, nie-starszego kodu do prawie każdego pojedynczego voidniefunkcji i niemal każdego typu klasy?

Próbowałem to zrobić we własnym kodzie i działa dobrze, ale jest tak strasznie gadatliwy, że zaczyna przypominać Javę. Wydaje się bardziej naturalne, aby kompilatory domyślnie ostrzegały o odrzuconych wartościach zwrotnych, z wyjątkiem kilku innych przypadków, w których zaznaczasz swój zamiar [*] .

Ponieważ widziałem zero dyskusji na temat tej możliwości w standardowych propozycjach, wpisach na blogu, pytaniach dotyczących przepełnienia stosu lub w dowolnym innym miejscu w Internecie, muszę coś przeoczyć.

Dlaczego taka mechanika nie miałaby sensu w nowym kodzie C ++? Czy gadatliwość jest jedynym powodem, aby nie używać [[nodiscard]]prawie wszędzie?


[*] Teoretycznie możesz [[maydiscard]]zamiast tego mieć coś w rodzaju atrybutu, który można również dodać z mocą wsteczną do funkcji takich jak printfw implementacjach biblioteki standardowej.

Christian Hackl
źródło
22
Cóż, istnieje wiele funkcji, w których wartość zwrotną można rozsądnie odrzucić. operator =na przykład. I std::map::insert
immibis
2
@immibis: True. Biblioteka standardowa jest pełna takich funkcji. Ale w moim własnym kodzie są one niezwykle rzadkie; czy uważasz, że to kwestia kodu biblioteki vs. kodu aplikacji?
Christian Hackl,
9
„... strasznie gadatliwy, że zaczyna się czuć jak Java ...” - przeczytanie tego w pytaniu o C ++ sprawiło, że trochę
drgnęłem
3
@ChristianHackl Komentarze prawdopodobnie nie są właściwym miejscem do „bashowania językowego”, i oczywiście Java jest również pełna w porównaniu do innych języków. Ale pliki nagłówkowe, operatory przypisania, konstruktory kopiowania i właściwe użycie constmogą znacznie nadmuchać „prostą” klasę (a raczej „zwykły stary obiekt danych”) w C ++.
Marco13
2
@ Marco13: Nie mogę odnieść się do tak wielu punktów w jednym komentarzu. Krótko mówiąc: Java nie ma plików nagłówkowych, co zmniejsza liczbę plików, ale zwiększa łączenie; OTOH zmusza cię do umieszczenia każdej klasy najwyższego poziomu we własnym pliku i każdej funkcji w klasie. W C ++ prawie nigdy nie implementujesz operatorów przypisania i nie kopiujesz konstruktorów; funkcje te są bardziej odpowiednie dla klas standardowych bibliotek, takich jak std::vectorlub std::unique_ptr, których potrzebujesz tylko elementów danych w definicji klasy. Pracowałem w obu językach; Java jest dobrym językiem, ale jest bardziej szczegółowy.
Christian Hackl

Odpowiedzi:

73

W nowym kodzie, który nie musi być zgodny ze starszymi standardami, używaj tego atrybutu tam, gdzie jest to uzasadnione. Ale dla C ++ [[nodiscard]]sprawia , że złe ustawienie domyślne. Sugerujesz:

Wydaje się bardziej naturalne, aby kompilatory domyślnie ostrzegały o odrzuconych wartościach zwrotnych, z wyjątkiem kilku innych przypadków, w których zaznaczasz swój zamiar.

To nagle spowodowałoby, że istniejący, poprawny kod emitował wiele ostrzeżeń. Chociaż taka zmiana może być technicznie uznana za zgodną wstecz, ponieważ każdy istniejący kod nadal się kompiluje, to w praktyce byłaby to ogromna zmiana semantyki.

Decyzje projektowe dla istniejącego, dojrzałego języka z bardzo dużą bazą kodu niekoniecznie różnią się od decyzji projektowych dla zupełnie nowego języka. Gdyby to był nowy język, domyślnie ostrzeżenie byłoby rozsądne. Na przykład język Nim wymaga jawnego odrzucenia niepotrzebnych wartości - byłoby to podobne do zawijania każdej instrukcji wyrażenia w C ++ rzutowaniem (void)(...).

[[nodiscard]]Atrybut jest najbardziej przydatna w dwóch przypadkach:

  • jeśli funkcja nie wywołuje żadnych efektów poza zwróceniem określonego wyniku, tj. jest czysta. Jeśli wynik nie zostanie wykorzystany, połączenie z pewnością będzie bezużyteczne. Z drugiej strony odrzucenie wyniku nie byłoby błędne.

  • czy wartość zwrotna musi zostać sprawdzona, np. dla interfejsu podobnego do C, który zwraca kody błędów zamiast wyrzucania. Jest to podstawowy przypadek użycia. W przypadku idiomatic C ++ będzie to dość rzadkie.

Te dwa przypadki pozostawiają ogromną przestrzeń nieczystych funkcji, które zwracają wartość, gdzie takie ostrzeżenie byłoby mylące. Na przykład:

  • Rozważ typ danych kolejki za pomocą .pop()metody, która usuwa element i zwraca kopię usuniętego elementu. Taka metoda jest często wygodna. Są jednak przypadki, w których chcemy jedynie usunąć element bez uzyskania kopii. Jest to całkowicie uzasadnione i ostrzeżenie nie byłoby pomocne. Inny projekt (np. std::vector) Dzieli te obowiązki, ale ma inne kompromisy.

    Należy pamiętać, że w niektórych przypadkach i tak kopia musi zostać wykonana, więc dzięki zwrotowi RVO kopia byłaby bezpłatna.

  • Rozważ płynne interfejsy, w których każda operacja zwraca obiekt, aby można było wykonać dalsze operacje. W C ++ najczęstszym przykładem jest operator wstawiania strumienia <<. Dodanie [[nodiscard]]atrybutu do każdego <<przeciążenia byłoby niezwykle kłopotliwe .

Te przykłady pokazują, że kompilowanie idiomatycznego kodu C ++ bez ostrzeżeń w języku „C ++ 17 domyślnie z nodiscard” byłoby dość żmudne.

Zauważ, że Twój nowy, błyszczący kod C ++ 17 (w którym możesz użyć tych atrybutów) może być nadal kompilowany razem z bibliotekami, które są ukierunkowane na starsze standardy C ++. Ta kompatybilność wstecz jest kluczowa dla ekosystemu C / C ++. Zatem ustawienie domyślnego nodiscard spowodowałoby wiele ostrzeżeń dla typowych, idiomatycznych przypadków użycia - ostrzeżeń, których nie można naprawić bez daleko idących zmian w kodzie źródłowym biblioteki.

Prawdopodobnie problemem tutaj nie jest zmiana semantyki, ale to, że cechy każdego standardu C ++ dotyczą zakresu na jednostkę kompilacji, a nie zakresu na plik. Jeśli / kiedy jakiś przyszły standard C ++ odejdzie od plików nagłówkowych, taka zmiana byłaby bardziej realistyczna.

amon
źródło
6
Poprosiłem o to, ponieważ zawiera on przydatne spostrzeżenia. Nie jestem jednak pewien, czy tak naprawdę odpowiada na pytanie. Ponownie nieczyste funkcje, takie jak poplub operator<<, byłyby wyraźnie oznaczone moim wyobrażonym [[maydiscard]]atrybutem, więc nie byłyby generowane żadne ostrzeżenia. Czy mówisz, że takie funkcje są ogólnie o wiele bardziej powszechne, niż chciałbym w to wierzyć, lub o wiele bardziej ogólne niż w moich własnych bazach kodów? Twój pierwszy akapit na temat istniejącego kodu jest z pewnością prawdziwy, ale wciąż zastanawiam się, czy ktokolwiek obecnie przetwarza cały swój nowy kod [[nodiscard]], a jeśli nie, dlaczego nie.
Christian Hackl
23
@ChristianHackl: Był taki czas w historii C ++, że uznawano za dobrą praktykę zwracanie wartości jako const. Nie możesz powiedzieć returns_an_int() = 0, więc po co returns_a_str() = ""pracować? C ++ 11 pokazał, że to naprawdę okropny pomysł, ponieważ teraz cały ten kod zabraniał ruchów, ponieważ wartości były const. Wydaje mi się, że właściwym wyciągnięciem z tej lekcji jest „to nie od ciebie zależy, co zrobią twoi rozmówcy z twoim wynikiem”. Zignorowanie wyniku jest całkowicie poprawną rzeczą, do której dzwoniący może chcieć, a jeśli nie wiesz, że to źle (bardzo wysoki pasek), nie powinieneś go blokować.
GManNickG
13
Rozsądek empty()jest również sensowny, ponieważ nazwa jest dość dwuznaczna: czy to orzeczenie, is_empty()czy mutator clear()? Zwykle nie spodziewałbyś się, że taki mutator zwróci cokolwiek, więc ostrzeżenie o wartości zwracanej może ostrzec programistę o problemie. Przy wyraźniejszej nazwie ten atrybut nie byłby tak potrzebny.
amon
13
@ChristianHackl: Jak powiedzieli inni, uważam, że czyste funkcje są dobrym przykładem tego, kiedy należy to wykorzystać. Chciałbym pójść o krok dalej i powiedzieć, że powinien istnieć [[pure]]atrybut, który powinien implikować [[nodiscard]]. W ten sposób jasne jest, dlaczego tego wyniku nie należy odrzucać. Ale poza funkcjami czystymi nie mogę wymyślić innej klasy funkcji, które powinny mieć takie zachowanie. I większość funkcji nie jest czysta, dlatego nie sądzę, aby domyślnie nodiscard było właściwym wyborem.
GManNickG
5
@GManNickG: Po próbie debugowania kodu, który zignorował kody błędów, nie powiodło się. Zdecydowanie uważam, że zdecydowana większość kodów błędów musi zostać domyślnie sprawdzona.
Mooing Duck,
9

Powody, dla których [[nodiscard]]prawie nie wszędzie:

  1. (główny :) To wprowadziłoby sposób zbyt dużo hałasu w moich nagłówków.
  2. (major :) Nie uważam, że powinienem przyjmować silne spekulacyjne założenia dotyczące kodu innych osób. Chcesz odrzucić wartość zwrotu, którą ci daję? Dobrze, znokautuj się.
  3. (drobne :) Gwarantujesz niekompatybilność z C ++ 14

Teraz, jeśli ustawisz go jako domyślny, zmusisz wszystkich programistów bibliotek do zmuszenia wszystkich użytkowników do odrzucania zwracanych wartości. To byłoby okropne. Albo zmusisz ich do dodania [[maydiscard]]niezliczonej liczby funkcji, co również jest trochę okropne.

einpoklum
źródło
0

Na przykład: operator << ma wartość zwracaną, która zależy od połączenia albo absolutnie potrzebna, albo absolutnie bezużyteczna. (std :: cout << x << y, pierwszy jest potrzebny, ponieważ zwraca strumień, drugi nie jest w ogóle przydatny). Teraz porównaj z printf, gdzie wszyscy odrzucają zwracaną wartość, która jest dostępna do sprawdzania błędów, ale operator << nie ma sprawdzania błędów, więc nie może być tak użyteczny. W obu przypadkach nodiscard byłby po prostu szkodliwy.

gnasher729
źródło
Odpowiada to na pytanie, które nie zostało zadane, tj. „Jaki jest powód nieużywania [[nodiscard]]wszędzie?”.
Christian Hackl