为什么 void 方法在 C++ 可以返回 void 值,而在其他语言中却不能?

2022-09-04 01:05:50

该程序以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方法无法返回值”。但是,由于被调用的方法本身就是一个 void,因此它不会返回值。我知道,为了可读性,这样的结构可能是被禁止的。还有其他反对意见吗?

编辑:为了将来参考,我在这里发现了一些类似的问题 return-void-type-in-c-and-c 在我看来,这个问题还没有得到回答。回答“因为它在规范中这样说,继续前进”并没有削减它,因为首先必须有人编写规范。也许我应该问“允许返回像C++这样的空洞类型的利弊是什么?”


答案 1

这是因为它可以在模板中使用。C# 和 Java 禁止作为类型参数,但C++允许它允许您编写如下模板代码:void

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

如果不允许方法返回表达式,则 如果 是 ,则无法进行此模板实例化。如果是这种情况,如果您想要实际使用,则需要一个单独的模板定义。voidvoidTResultvoidTResultvoid

例如,请记住在 C# 中如何有两组通用通用委托,即 和 ?好吧,存在正是因为被禁止。C++设计师不想尽可能地引入这样的情况,所以他们决定允许你用作模板参数 - 你发现的情况是一个功能,可以促进这一点。Func<>Action<>Action<T>Func<T, void>void


(请允许我以假装问答的形式编写其余部分。

但是为什么C#和Java不允许类似的结构呢?

首先,了解如何在这些语言中实现泛型编程:

为什么选择一种实现泛型编程的方法而不是另一种方法?

  • 泛型方法保持语言其余部分的名义类型。这样做的好处是允许(AOT)编译器进行静态分析,类型检查,错误报告,重载解决以及最终一代码生成。
  • 模板方法本质上是鸭子类型。Duck在名义上类型化的语言中打字没有上述优点,但它可以让你有更大的灵活性,因为它将允许潜在的“无效”的东西(从名义类型系统的角度来看是“无效的”),只要你没有在程序的任何地方提到这些无效的可能性。换句话说,模板允许您一表达更大的案例集。

好吧,那么C#和Java需要做些什么来支持作为有效的泛型参数呢?void

我必须推测才能回答这个问题,但我会尝试。

在语言层面,他们必须放弃仅在方法中有效且对非方法始终无效的概念。如果没有此更改,很少有有用的方法可以实例化 - 并且它们可能都必须以递归或无条件(满足void和非void方法而不返回)结束。因此,为了使这很有用,C#和Java还必须引入C++功能,允许您返回表达式。return;voidvoidthrowvoid

好吧,让我们假设你有这个,现在你可以写这样的代码:

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

同样,非通用版本在C#和Java中与在C++中一样无用。但是,让我们继续前进,看看它的真正用途,这是在泛型中。

您现在应该能够编写这样的泛型代码 - 并且现在可以(除了已经允许的所有其他类型之外):TResultvoid

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

但请记住,在C#和Java中,重载解析发生在“早期”,而不是“晚期”。重载解析算法将为每种可能的 选择相同的被调用方。类型检查器将不得不抱怨,因为你要么从一个可能的方法返回一个表达式,要么从一个可能的方法返回一个非表达式。TResultvoidvoidvoidvoid

换句话说,外部方法不能是泛型的,除非

  1. 被调用方也是泛型的,其返回类型由与外部方法的泛型类型参数匹配的泛型类型参数定义。
  2. 泛型类型和方法中的重载解析被推迟到实际的类型参数可用时,以便我们可以在调用点选择正确的非泛型方法。

如果我们选择第一个选项 - 使被调用方的返回类型通用并继续前进呢?

我们可以这样做,但它只是把我们的问题推给被叫方。

在某些时候,我们需要某种方法来“实例化”某种实例,并可以选择以某种方式接收它。所以现在我们需要构造函数(尽管每个方法都可以算作一个工厂方法,如果你眯眼的话),我们还需要类型的变量,可能的从到的转换,等等。voidvoidvoidvoidvoidobject

基本上,对于所有意图和目的,都必须成为常规类型(例如,常规空结构)。这其中的含义并不可怕,但我认为你可以理解为什么C#和Java避免了它。void

第二种选择 - 推迟过载解决方案呢?

也是完全可能的,但请注意,它将有效地将泛型转换为较弱的模板。(“较弱”的意思是,C++模板不限于类型名称

同样,这不会是世界末日,但它将涉及失去我之前描述的泛型的优势。C#和Java的设计者显然希望保留这些优势。


附注:

在 C# 中,我知道有一种特殊情况,即绑定发生在泛型类型定义验证之后。如果 对 有约束,并且尝试实例化新的 T(),编译器将生成代码来检查是否为值类型。然后:new()TT

这种特殊情况非常特殊,因为即使它完全推迟了对运行时的方法绑定,编译器仍然可以执行静态分析、类型检查和代码生成一次。毕竟,表达式的类型总是这样,对具有空形式参数列表的内容的调用可以简单地解析和验证。new T()T


答案 2

根据 Java 语言规范 §14.17

不带表达式的 return 语句必须包含在下列语句之一中,否则会发生编译时错误:

  • 使用关键字 void 声明不返回值的方法 (§8.4.5)

...

带有表达式的 return 语句必须包含在以下选项之一中,否则会发生编译时错误:

  • 声明为返回值的方法

...

因此,通过声明方法为 ,您是在说它不返回任何值,因此您只能使用没有表达式的语句。voidreturn;