Почему метод 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 не разрешить подобное строительство?

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

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

  • подход дженериков поддерживает номинальное ввода остального языка. Это имеет то преимущество, что позволяет компилятору (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 метод.

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

  1. вызываемому и generic и его возвращаемый тип определяются параметром универсального типа, который соответствует параметру внешнего метода.
  2. разрешение перегрузки в универсальных типах и методах откладывается до тех пор, пока не будут доступны фактические аргументы типа, чтобы мы могли выбрать правильный не-универсальный метод в месте вызова.

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

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

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

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

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

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

хочу сохранить эти преимущества.

Sidenote:

в C# есть один особый случай, я знаю of, где происходит привязка после проверка определения универсального типа. Если у вас есть new() ограничения T попытка создать экземпляр a new T() компилятор будет генерировать код, который проверяет, является ли T тип значения или нет. Затем:

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


по словам Спецификация Языка Java §14.17:

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

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

...

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

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

...

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