Почему метод void в C++ может возвращать значение void, но на других языках он не может?
эта программа компилируется и запускается на C++, но не на нескольких разных языках, таких как Java и C#.
#include <iostream>
using namespace std;
void foo2() {
cout << "foo 2.n";
}
void foo() {
return foo2();
}
int main() {
foo();
return 0;
}
в Java это дает ошибку компилятора, такую как "методы Void не могут возвращать значение". Но поскольку вызываемый метод сам по себе является пустотой, он не возвращает значение. Я понимаю, что такая конструкция, вероятно, запрещена для удобства чтения. Есть ли другие возражения?
Edit: для дальнейшего использования я нашел несколько похожих вопрос здесь return-void-type-in-c-and-c На мой скромный взгляд, этот вопрос еще не ответили. Ответ "Потому что так сказано в спецификации, двигайтесь дальше" не сокращает его, так как кто-то должен был написать спецификацию в первую очередь. Может быть, я должен был спросить: "какие плюсы и минусы позволяют возвращать тип void, такой как C++"?
2 ответов
это из-за возможности его использования в шаблонах. C# и Java запрещают void
в качестве аргумента типа, но C++ разрешает вам писать код шаблона следующим образом:
template<typename T, typename TResult>
TResult foo(T x, T y)
{
return foo2(x, y);
}
если void
методы не разрешалось возвращать void
выражение, этот экземпляр шаблона будет невозможен, если TResult
был void
. Если бы это было так, вам понадобилось бы отдельное определение шаблона, если вы когда-либо хотели TResult
на самом деле быть void
.
например, помните, как в C# есть два набора общих делегатов общего назначения, а именно Func<>
и Action<>
? Ну,Action<T>
существует именно потому, что Func<T, void>
запрещено. Дизайнеры C++ не хотели вводить подобные ситуации везде, где это возможно, поэтому они решили разрешить вам использовать void
в качестве аргумента шаблона -- и случай, который вы нашли, является функцией, облегчающей именно это.
(позвольте мне написать остальное в формат pretend-Q&A.)
но почему C# и Java не разрешить подобное строительство?
во-первых, понять, как общее программирование стало возможным на этих языках:
- C# и Java generics работают, анализируя определение универсального типа (или метода) и убеждаясь, что оно действительно для общих ограничений/границ вы предоставили.
- шаблоны C++ являются поиск-и-замену механизм С мощным языком метапрограммирования вокруг них. они не обязаны иметь смысл при отсутствии конкретных аргументов шаблона -- они переходят от" шаблонного метаязыка "к" языку C++ " (так сказать) только тогда, когда они получают фактические аргументы.
зачем выбирать один подход к реализации общего программирования над другим?
- подход дженериков поддерживает номинальное ввода остального языка. Это имеет то преимущество, что позволяет компилятору (AOT) выполнять статический анализ, проверку типов, отчеты об ошибках, разрешение перегрузки и в конечном итоге генерацию кода после.
- подход шаблонов по существу утиной типизацией. Duck typing на номинально типизированном языке не имеет преимуществ, описанных выше, но он позволяет вам больше гибкости в том смысле, что это позволит потенциально "недействительные" вещи ("недействительные" с точки зрения номинальной системы типов), пока вы фактически не упоминаете эти недействительные возможности в любом месте вашей программы. Другими словами, шаблоны позволяют выразить больший набор падежей единообразно.
хорошо, так что же нужно сделать C# и Java для поддержки
void
как допустимый общий аргумент?
я бы порассуждал, чтобы ответить на этот вопрос, но я пытаться.
на уровне языка им пришлось бы отказаться от понятия, что return;
действует только в void
методы и всегда недействительны для не -void
методы. Без этого изменения может быть создано очень мало полезных методов - и все они, вероятно, должны закончиться рекурсией или безусловным throw
(которая удовлетворяет обе void
и неvoid
методы без возврата). Поэтому, чтобы сделать это полезным, C# и Java также должны были бы ввести Функция C++ позволяет вам возвращать void
выражения.
Хорошо, предположим, у вас есть это, и теперь вы можете написать такой код:
void Foo2() { }
void Foo()
{
return Foo2();
}
опять же, неродовая версия так же бесполезна в C# и Java, как и в C++. Но давайте двигаться дальше и видеть его реальную полезность, которая заключается в дженериках.
теперь вы должны иметь возможность писать общий код, как это-и TResult
теперь может быть void
(В дополнение ко всем другим типам, которые уже были разрешено):
TResult Foo<T, TResult>(T a)
{
return Foo2(a);
}
но помните, что в C# и Java разрешение перегрузки происходит "рано", а не "поздно". Тот же вызываемый объект будет выбран алгоритмом разрешения перегрузки для всех возможных TResult
. И тип проверки будет есть жаловаться, потому что вы либо возвращаете void
выражение от, возможно, не -void
способ или вы возвращаете неvoid
выражение из возможно void
метод.
другими словами, внешний метод не может быть универсальным, если:
- вызываемому и generic и его возвращаемый тип определяются параметром универсального типа, который соответствует параметру внешнего метода.
- разрешение перегрузки в универсальных типах и методах откладывается до тех пор, пока не будут доступны фактические аргументы типа, чтобы мы могли выбрать правильный не-универсальный метод в месте вызова.
что, если мы пошли с первым опция-сделать тип возврата вызываемого абонента общим и двигаться дальше?
мы мог бы сделайте это,но это просто подталкивает нашу проблему к вызываемому.
в какой-то момент нам понадобится какой-то способ "инстанцировать" какой-то void
экземпляр и, возможно, сможет получить его каким-то образом. Итак, теперь нам понадобятся конструкторы для void
(хотя каждый void
метод может считаться заводским методом, если вы косите), и нам также понадобятся переменные типа void
, возможно конвертирование void
to object
и так далее.
по сути, void
должен был бы стать регулярным типом (например,обычная пустая структура) для все намерения и цели. Последствия этого не ужасны, но я думаю, вы можете понять, почему C# и Java избегали этого.
а как насчет второго варианта - отложить разрешение перегрузки?
также вполне возможно, но обратите внимание, что это эффективно превратит дженерики в более слабые шаблоны. ("Слабее" в том смысле, что шаблоны C++ не ограничены именами типов.)
хочу сохранить эти преимущества.Sidenote:
в C# есть один особый случай, я знаю of, где происходит привязка после проверка определения универсального типа. Если у вас есть new()
ограничения T
попытка создать экземпляр a new T()
компилятор будет генерировать код, который проверяет, является ли T
тип значения или нет. Затем:
- для типов значений,
new T()
становитсяdefault(T)
-- помните, что в C# по умолчанию конструкторы struct не совсем конструкторы в CLR смысл. - для ссылочных типов,
Activator.CreateInstance
вызывается, что является косвенным вызовом конструктора с использованием отражения.
этот конкретный случай очень особенный, потому что, хотя он полностью отложил привязку метода для выполнения, компилятор все еще может выполнять статический анализ, проверку типа и генерацию кода после. В конце концов, тип выражения new T()
всегда T
и призыв к чему-то это имеет пустой формальный список параметров, который может быть тривиально разрешен и проверен.
по словам Спецификация Языка Java §14.17:
оператор return без выражения должен содержаться в одном из следующих вариантов или возникает ошибка времени компиляции:
- метод, объявленный с использованием ключевого слова void, чтобы не возвращать значение (§8.4.5)
...
оператор return с выражением должен содержаться в одном из следующих, или ошибка времени компиляции происходит:
- метод, объявленный для возврата значения
...
Итак, объявив, что метод void
, вы говорите, что он не возвращает значения, поэтому вы ограничены использованием return;
оператор без выражения.