Почему метод void в С++ возвращает значение void, но на других языках он не может?

Эта программа компилируется и запускается на С++, но не на нескольких разных языках, таких как Java и С#.

#include <iostream>
using namespace std;

void foo2() {
  cout << "foo 2.\n";
}

void foo() {
    return foo2();
}

int main() {

    foo();

    return 0;
}

В Java это дает ошибку компилятора, например: "Методы Void не могут вернуть значение". Но так как вызываемый метод является самой void, он не возвращает значение. Я понимаю, что подобная конструкция, вероятно, запрещена для удобства чтения. Есть ли другие возражения?

Изменить: для справки в будущем я нашел здесь аналогичный вопрос return-void-type-in-c-and-c По моему скромному мнению этот вопрос еще не ответил. Ответ "Потому что он так говорит в спецификации, перейдите", не сокращает его, так как кто-то должен был написать спецификацию в первую очередь. Возможно, мне следовало спросить: "Каковы плюсы и минусы, позволяющие возвращать тип пустоты, такой как С++"?

Ответы

Ответ 1

Это из-за возможности его использования в шаблонах. С# и Java запрещают void как аргумент типа, но С++ разрешает вам писать код шаблона следующим образом:

template<typename T, typename TResult>
TResult foo(T x, T y)
{
    return foo2(x, y);
}

Если методам void не было разрешено возвращать выражение void, это создание шаблона было бы невозможным, если TResult был void. Если бы это было так, вам понадобилось бы отдельное определение шаблона, если бы вы хотели, чтобы TResult фактически был void.

Например, помните, как в С# есть два набора общих универсальных делегатов, а именно Func<> и Action<>? Ну, Action<T> существует именно потому, что Func<T, void> запрещен. Дизайнеры С++ не захотели вводить подобные ситуации везде, где это было возможно, поэтому они решили разрешить использовать void в качестве аргумента шаблона - и найденный вами случай - это функция, которая облегчает именно это.


(Позвольте мне записать остальные в формате pretend-Q & A).

Но почему С# и Java не допускают подобную конструкцию?

Во-первых, поймите, как универсальное программирование стало возможным на этих языках:

Зачем выбирать один подход реализации общего программирования над другим?

  • Подход generics поддерживает номинальную типизацию остальной части языка. Это имеет то преимущество, что позволяет компилятору (AOT) выполнять статический анализ, проверку типов, отчеты об ошибках, разрешение перегрузки и, в конечном итоге, генерировать код.
  • В основе шаблонов лежит утиная печать. Утка на типизированном языке не имеет преимуществ, описанных выше, но это позволяет вам проявлять большую гибкость в том смысле, что она позволит потенциально "недействительные" вещи ( "недействительными" с точки зрения системы номинального типа), как если вы на самом деле не упоминаете те недопустимые возможности в любой точке вашей программы. Другими словами, шаблоны позволяют вам выражать больший набор случаев равномерно.

Хорошо, так что С# и Java должны сделать, чтобы поддерживать void как допустимый общий аргумент?

Я должен был бы спекулировать, чтобы ответить на это, но я попробую.

На уровне языка им придется отказаться от понятия, что return; допустимо только в методах void и всегда недействителен для методов void. Без этого изменения можно было бы создать очень мало полезных методов - и все они, вероятно, должны были бы закончиться рекурсией или безусловным throw (, который удовлетворяет обеим void и не void без возврата). Поэтому, чтобы сделать это полезным, С# и Java также должны были бы представить функцию С++, позволяющую возвращать выражения void.

Хорошо, допустим, у вас есть это, и теперь вы можете написать такой код:

void Foo2() { }
void Foo()
{
    return Foo2();
}

Опять же, универсальная версия бесполезна в С# и Java, как и на С++. Но давайте двигаться дальше и видеть его реальную полезность, которая находится в дженериках.

Теперь вы можете написать такой общий код, и теперь TResult может быть void (в дополнение ко всем другим типам, которые уже были разрешены):

TResult Foo<T, TResult>(T a)
{
    return Foo2(a);
}

Но помните, что в С# и Java разрешение перегрузки происходит "раньше", а не "поздно". Такой же вызов будет выбран алгоритмом разрешения перегрузки для всех возможных TResult. И проверка типа должна будет жаловаться, потому что вы либо возвращаете выражение void из метода возможно не void, либо возвращаете выражение не void из метода void.

Другими словами, внешний метод не может быть общим, если:

  • Вызов также является общим, а его тип возврата определяется параметром общего типа, который соответствует внешнему методу.
  • Разрешение перегрузки в общих типах и методах откладывается до тех пор, пока фактические аргументы типа не станут доступными, чтобы мы могли выбрать правильный не общий метод в месте вызова.

Что делать, если мы пошли с первым вариантом - сделать тип возврата вызываемого абонента общим и двигаться дальше?

Мы могли бы это сделать, но это просто подталкивает нашу проблему к вызываемому.

В какой-то момент нам понадобится какой-то способ "создать экземпляр" какого-то экземпляра void и, возможно, сможет его каким-то образом получить. Итак, теперь нам нужны конструкторы для void (хотя каждый метод void может считаться методом factory, если вы косоглазите), и нам также понадобятся переменные типа void, возможные преобразования из void в object и т.д.

В принципе, void должен был бы стать обычным типом (например, обычная пустая структура) для всех целей и задач. Последствия этого не страшны, но я думаю, вы можете понять, почему С# и Java избегали этого.

Как насчет второго варианта - отложить разрешение перегрузки?

Также вполне возможно, но обратите внимание, что он эффективно превратит generics в более слабые шаблоны. ( "Слабее" в том смысле, что шаблоны С++ не ограничены именами типов.)

Опять же, это не будет конец света, но это будет связано с потерей преимуществ дженериков, которые я описал ранее. Дизайнеры С# и Java явно хотят сохранить эти преимущества.


Sidenote:

В С# есть один специальный случай, о котором я знаю, где привязка происходит после проверки определения общего типа. Если у вас есть ограничение new() на T и вы пытаетесь выполнить экземпляр new T(), компилятор сгенерирует код, который проверяет, является ли T тип значения или нет. Тогда:

Этот частный случай очень особенный, поскольку, хотя он полностью отложил привязку метода к среде выполнения, компилятор все равно может выполнять статический анализ, проверку типов и генерацию кода один раз. В конце концов, тип выражения new T() всегда T, и вызов того, что имеет пустой список формальных параметров, может быть тривиально разрешен и проверен.

Ответ 2

В соответствии с Спецификация языка Java §14.17:

Оператор return без Expression должен содержаться в одном из следующих или возникает ошибка компиляции:

  • Метод, объявленный с использованием ключевого слова void, чтобы не возвращать значение (§8.4.5)

...

Оператор return с выражением должен содержаться в одном из следующих или возникает ошибка компиляции:

  • Метод, объявленный для возврата значения

...

Итак, объявив, что метод void, вы говорите, что он не возвращает никакого значения, поэтому вы ограничены использованием оператора return; без выражения.