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 void
niefunkcji 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 printf
w implementacjach biblioteki standardowej.
operator =
na przykład. Istd::map::insert
…const
mogą znacznie nadmuchać „prostą” klasę (a raczej „zwykły stary obiekt danych”) w C ++.std::vector
lubstd::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.Odpowiedzi:
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: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.
źródło
pop
luboperator<<
, 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.returns_an_int() = 0
, więc po coreturns_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ć.empty()
jest również sensowny, ponieważ nazwa jest dość dwuznaczna: czy to orzeczenie,is_empty()
czy mutatorclear()
? 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.[[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.Powody, dla których
[[nodiscard]]
prawie nie wszędzie: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.źródło
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.
źródło
[[nodiscard]]
wszędzie?”.