В чем причина того, что [+ [nodiscard]] в C ++ 17 почти везде не используется в новом коде?

70

C ++ 17 вводит [[nodiscard]]атрибут, который позволяет программистам отмечать функции таким образом, что компилятор выдает предупреждение, если возвращаемый объект отбрасывается вызывающей стороной; один и тот же атрибут может быть добавлен ко всему типу класса.

Я читал о мотивации этой функции в исходном предложении , и я знаю, что C ++ 20 добавит атрибут к стандартным функциям вроде std::vector::empty, имена которых не передают однозначного значения относительно возвращаемого значения.

Это крутая и полезная функция. На самом деле, это кажется слишком полезным. Везде, где я читаю [[nodiscard]], люди обсуждают это так, как если бы вы просто добавили его в несколько избранных функций или типов и забыли обо всех остальных. Но почему неотбрасываемое значение должно быть особым случаем, особенно при написании нового кода? Разве возвращаемое возвращаемое значение обычно не является ошибкой или, по крайней мере, тратой ресурсов?

И не является ли один из принципов проектирования самого C ++, что компилятор должен отлавливать как можно больше ошибок?

Если это так, то почему бы не добавить [[nodiscard]]свой собственный, не унаследованный код почти к каждой отдельной voidфункции и почти к каждому типу класса?

Я пытался сделать это в своем собственном коде, и он отлично работает, за исключением того, что он настолько ужасный, что начинает ощущаться как Java. Казалось бы, гораздо более естественно заставить компиляторы предупреждать об исключенных возвращаемых значениях по умолчанию, за исключением нескольких других случаев, когда вы отмечаете свое намерение [*] .

Поскольку я видел нулевые дискуссии об этой возможности в стандартных предложениях, записях в блогах, вопросах переполнения стека или где-либо еще в Интернете, я должен что-то упустить.

Почему такая механика не имеет смысла в новом коде C ++? Является ли многословие единственной причиной, по которой его нельзя использовать [[nodiscard]]почти везде?


[*] Теоретически, [[maydiscard]]вместо этого у вас может быть что-то вроде атрибута, который также может быть задним числом добавлен к функциям, как printfв реализациях стандартной библиотеки.

Кристиан Хакл
источник
22
Ну, есть множество функций, где возвращаемое значение может быть разумно отброшено. operator =например. И std::map::insert.
user253751
2
@immibis: правда. Стандартная библиотека полна таких функций. Но в моем собственном коде они, как правило, встречаются крайне редко; Как вы думаете, это вопрос кода библиотеки или кода приложения?
Кристиан Хакл,
9
«... ужасно многословно, что это начинает ощущаться как Java ...» - чтение этого вопроса в вопросе о C ++ заставило меня немного подергиваться: - /
Marco13
3
@ChristianHackl Комментарии, вероятно, не являются подходящим местом для "языкового избиения", и, конечно, Java также многословна по сравнению с другими языками. Но файлы заголовки, операторы присваивания, копировать конструктор и правильное использование constмогут раздуваться в противном случае «простой» класс (или , вернее , «простой старый объект данных») значительно в C ++.
Marco13
2
@ Marco13: я не могу ответить на столько вопросов в одном комментарии. Давайте сделаем это кратко: Java не имеет заголовочных файлов, что уменьшает количество файлов, но увеличивает связь; OTOH вынуждает вас поместить каждый класс верхнего уровня в свой собственный файл и каждую функцию в классе. В C ++ вы почти никогда не реализуете операторы присваивания и сами копируете конструкторы; эти функции более актуальны для классов стандартной библиотеки, таких как std::vectoror std::unique_ptr, для которых вам просто нужны члены-данные в определении класса. Я работал с обоими языками; Ява - хороший язык, но он более многословен.
Кристиан Хакл,

Ответы:

73

В новом коде, который не должен быть совместим со старыми стандартами, используйте этот атрибут везде, где это целесообразно. Но для C ++ [[nodiscard]]делает плохой дефолт. Вы предлагаете:

Казалось бы, гораздо более естественно заставить компиляторы предупреждать об исключенных возвращаемых значениях по умолчанию, за исключением нескольких других случаев, когда вы отмечаете свое намерение.

Это внезапно приведет к тому, что существующий правильный код выдаст много предупреждений. Хотя такое изменение технически можно считать обратно совместимым, поскольку любой существующий код все еще успешно компилируется, на практике это будет огромным изменением семантики.

Проектные решения для существующего, зрелого языка с очень большой кодовой базой обязательно отличаются от проектных решений для совершенно нового языка. Если бы это был новый язык, тогда предупреждение по умолчанию было бы разумным. Например, язык Nim требует явного отбрасывания ненужных значений - это было бы похоже на перенос каждого выражения выражения в C ++ с приведением (void)(...).

[[nodiscard]]Атрибут является наиболее полезным в двух случаях:

  • если функция не имеет эффектов, кроме возврата определенного результата, то есть является чистой. Если результат не используется, вызов, безусловно, бесполезен. С другой стороны, отбрасывание результата не будет неправильным.

  • если возвращаемое значение должно быть проверено, например, для C-подобного интерфейса, который возвращает коды ошибок вместо броска. Это основной вариант использования. Для идиоматического C ++ это будет довольно редко.

Эти два случая оставляют огромное пространство нечистых функций, которые действительно возвращают значение, где такое предупреждение могло бы ввести в заблуждение. Например:

  • Рассмотрим тип данных очереди с .pop()методом, который удаляет элемент и возвращает копию удаленного элемента. Такой метод часто удобен. Однако в некоторых случаях мы хотим удалить только элемент без получения копии. Это совершенно законно, и предупреждение не поможет. Другой дизайн (такой как std::vector) разделяет эти обязанности, но у этого есть другие компромиссы.

    Обратите внимание, что в некоторых случаях копия должна быть сделана в любом случае, так что благодаря RVO возврат копии будет бесплатным.

  • Рассмотрим текущие интерфейсы, где каждая операция возвращает объект, чтобы можно было выполнить дальнейшие операции. В C ++ наиболее распространенным примером является оператор вставки потока <<. Было бы чрезвычайно обременительно добавлять [[nodiscard]]атрибут к каждой <<перегрузке.

Эти примеры демонстрируют, что сборка идиоматического кода C ++ без предупреждений под языком «C ++ 17 с nodiscard по умолчанию» была бы довольно утомительной.

Обратите внимание, что ваш блестящий новый код C ++ 17 (где вы можете использовать эти атрибуты) все еще может быть скомпилирован вместе с библиотеками, предназначенными для более старых стандартов C ++. Эта обратная совместимость имеет решающее значение для экосистемы C / C ++. Таким образом, установка nodiscard по умолчанию привела бы ко многим предупреждениям для типичных идиоматических сценариев использования - предупреждений, которые нельзя исправить без далеко идущих изменений в исходном коде библиотеки.

Возможно, проблема здесь не в изменении семантики, а в том, что функции каждого стандарта C ++ применяются к области на единицу компиляции, а не к области для каждого файла. Если / когда какой-то будущий стандарт C ++ отойдет от заголовочных файлов, такое изменение будет более реалистичным.

Амон
источник
6
Я проголосовал за это, поскольку он содержит полезные идеи. Пока не уверен, что он действительно отвечает на вопрос. Повторно нечистые функции, такие как popили operator<<, будут явно помечены моим воображаемым [[maydiscard]]атрибутом, поэтому предупреждения не будут генерироваться. Вы говорите, что такие функции, как правило, гораздо более распространены, чем мне хотелось бы верить, или гораздо чаще, чем в моих собственных кодах? Ваш первый абзац о существующем коде, безусловно, правдив, но все же заставляет задуматься, а кто-нибудь еще в данный момент добавляет весь свой новый код [[nodiscard]], а если нет, то почему.
Кристиан Хакл,
23
@ChristianHackl: В истории C ++ было время, когда считалось хорошей практикой возвращать значения как const. Вы не можете сказать returns_an_int() = 0, так почему должно returns_a_str() = ""работать? C ++ 11 показал, что это на самом деле ужасная идея, потому что теперь весь этот код запрещал перемещения, потому что значения были постоянными. Я думаю, что правильный урок этого урока: «не ваше дело, что ваши собеседники делают с вашим результатом». Игнорирование результата является вполне допустимой вещью, которую могут делать абоненты, и если вы не знаете, что это неправильно (очень высокая планка), вы не должны блокировать его.
GManNickG
13
Нодискарта empty()также полезна, потому что название весьма неоднозначно: это предикат is_empty()или мутатор clear()? Обычно вы не ожидаете, что такой мутатор вернет что-либо, поэтому предупреждение о возвращаемом значении может предупредить программиста о проблеме. С более четким именем этот атрибут не будет таким необходимым.
Амон
13
@ChristianHackl: Как уже говорили, чистые функции - хороший пример того, когда это следует использовать. Я бы пошел дальше и сказал, что должен быть [[pure]]атрибут, и это должно подразумевать [[nodiscard]]. Таким образом, понятно, почему этот результат не следует отбрасывать. Но кроме чистых функций, я не могу думать о другом классе функций, которые должны иметь такое поведение. И большинство функций не являются чистыми, поэтому я не считаю nodiscard по умолчанию правильным выбором.
GManNickG
5
@GManNickG: После попытки и не отлаживать код , который игнорировал коды ошибок, я сильно верю , что подавляющее большинство кодов ошибок должно быть проверено по умолчанию.
Mooing Duck
9

Причины я бы не [[nodiscard]]везде:

  1. (большой :) Это ввести путь слишком много шума в моих заголовках.
  2. (Major :) Я не думаю, что я должен делать сильные умозрительные предположения о коде других людей. Вы хотите отменить возвращаемое значение, которое я вам даю? Хорошо, вышибись.
  3. (незначительный :) Вы гарантируете несовместимость с C ++ 14

Теперь, если вы установите его по умолчанию, вы заставите всех разработчиков библиотек заставить всех своих пользователей не сбрасывать возвращаемые значения. Это было бы ужасно. Или вы заставляете их добавлять [[maydiscard]]неисчислимое количество функций, что тоже ужасно.

einpoklum - восстановить Монику
источник
0

Как пример: оператор << имеет возвращаемое значение, которое зависит от вызова, либо абсолютно необходимого, либо абсолютно бесполезного. (std :: cout << x << y, первый нужен, потому что он возвращает поток, а второй вообще не нужен). Теперь сравните с printf, где все отбрасывают возвращаемое значение, которое существует для проверки ошибок, но затем оператор << не проверяет ошибки, поэтому он не мог быть таким полезным в первую очередь. Так что в обоих случаях нодискард будет просто вредным.

gnasher729
источник
Это отвечает на вопрос, который не был задан, а именно: «В чем причина не использовать [[nodiscard]]везде?».
Кристиан Хакл