Ответ 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 не допускают подобную конструкцию?
Во-первых, поймите, как универсальное программирование стало возможным на этих языках:
- С# и Java generics работают, анализируя определение общего типа (или метода) и убедившись, что оно действительно для общих ограничений/ограничений, которые вы предоставили.
- С++ шаблоны механизм поиска и замены с мощным языком метапрограммирования вокруг них. Они не обязаны иметь смысл в отсутствие конкретных аргументов шаблона - они переходят с "метаязыка шаблона" на "язык С++", (если можно так выразиться), только когда они получат реальные аргументы.
Зачем выбирать один подход реализации общего программирования над другим?
- Подход 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()
становитсяdefault(T)
- помните, что Конструкторы структуры С# по умолчанию не являются конструкторами в смысле CLR. - Для ссылочных типов вызывается
Activator.CreateInstance
, который является косвенным вызовом конструктора с использованием отражения.
Этот частный случай очень особенный, поскольку, хотя он полностью отложил привязку метода к среде выполнения, компилятор все равно может выполнять статический анализ, проверку типов и генерацию кода один раз. В конце концов, тип выражения new T()
всегда T
, и вызов того, что имеет пустой список формальных параметров, может быть тривиально разрешен и проверен.