为什么 C++ 中的 void 方法 return 可以是 void 值,而在其他语言中却不能?
Why can a void method in C++ return a void value, but in other languages it cannot?
此程序可以在 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 methods cannot return a value' 的编译器错误。但是由于被调用的方法本身是一个 void,它没有 return 一个值。我知道为了可读性,可能禁止这样的构造。还有其他异议吗?
编辑:为了将来参考,我在这里发现了一些类似的问题
以我的拙见,这个问题还没有回答。
回复 'Because it says so in the specification, move on' 并没有削减它,因为首先必须有人编写规范。也许我应该问 'What are the pros and cons of allowing returning a void type like C++'?
根据 Java Language Specification §14.17:
A return statement with no Expression must be contained in one of the following, or a compile-time error occurs:
- A method that is declared, using the keyword void, not to return a value (§8.4.5)
...
A return statement with an Expression must be contained in one of the following, or a compile-time error occurs:
- A method that is declared to return a value
...
所以,通过声明一个方法是 void
,你是说它 returns 没有价值,所以你只能使用没有表达式的 return;
语句。
这是因为它有可能在模板中使用。 C# 和 Java 禁止将 void
作为类型参数,但 C++ 允许它允许您编写如下模板代码:
template<typename T, typename TResult>
TResult foo(T x, T y)
{
return foo2(x, y);
}
如果 void
方法不允许 return 一个 void
表达式,如果 TResult
是 void
,则此模板实例化将是不可能的。如果是这种情况,如果您希望 TResult
实际上是 void
.
,则需要一个单独的模板定义
例如,还记得 C# 中有两组通用的通用委托,即 Func<>
和 Action<>
吗?好吧,Action<T>
存在正是因为 Func<T, void>
被禁止。 C++ 设计者不想尽可能地引入这样的情况,因此他们决定允许您使用 void
作为模板参数——而您发现的 case 正是一个有助于实现这一点的功能。
(让我以假装问答的形式写下其余部分。)
But why do C# and Java not allow a similar construct?
首先,了解如何在这些语言中实现泛型编程:
- C# 和 Java 泛型通过解析泛型类型(或方法)定义并确保它对您拥有的泛型 constraints/bounds 有效 来工作提供。
- C++ 模板是 a search-and-replace mechanism with a powerful metaprogramming language around them. They are not required to make sense in the absence of specific template arguments——它们只有在获得实际参数时才会从 "template metalanguage" 到 "C++ language"(可以这么说)。
Why pick one approach of implementing generic programming over the other?
- 泛型方法维护了语言其余部分的 nominal typing。这样做的好处是允许 (AOT) 编译器进行静态分析、类型检查、错误报告、重载解析和最终代码生成 once.
- 模板方法本质上是 duck typing。名义类型语言中的 Duck 类型不具有上述优点,但它允许您更灵活,因为它可能允许 "invalid" 事情(从名义类型的角度来看 "invalid"类型系统),只要您实际上没有在程序中的任何地方提及那些无效的可能性。换句话说,模板允许您统一.
表达更大的案例集
Okay, so what would C# and Java need to do to support void
as a valid generic argument?
我不得不推测才能回答这个问题,但我会尝试的。
在语言层面,他们必须放弃 return;
仅在 void
方法中有效并且对非 void
方法始终无效的概念。如果没有这个改变,很少有有用的方法可以被实例化——它们可能都必须以递归或无条件的 throw
(which satisfies both void
and non-void
methods without returning) 结束。因此,为了使其有用,C# 和 Java 还必须引入允许您使用 return void
表达式的 C++ 功能。
好吧,假设您已经有了,现在您可以编写如下代码:
void Foo2() { }
void Foo()
{
return Foo2();
}
同样,非泛型版本在 C# 和 Java 中与在 C++ 中一样无用。但是让我们继续前进,看看它的真正用处,那就是在泛型中。
您现在应该能够像这样编写通用代码——TResult
现在可以是 void
(除了已经允许的所有其他类型之外):
TResult Foo<T, TResult>(T a)
{
return Foo2(a);
}
但请记住,在 C# 和 Java 中,重载解析发生在 "early",而不是 "late"。重载决议算法将为每个可能的 TResult
选择相同的被调用者。并且类型检查器将 有 抱怨,因为你要么 return 从可能的非 void
方法中输入 void
表达式,要么你正在 return 从可能的 void
方法中获取非 void
表达式。
也就是说外层方法不能泛型,除非:
- 被调用者也是泛型,其return类型由与外部方法匹配的泛型类型参数定义。
- 泛型类型和方法中的重载解析被推迟到实际类型参数可用时,以便我们可以在调用点选择正确的非泛型方法。
What if we went with the first option - make the callee's return type generic and move on?
我们可以那样做,但这只会把我们的问题推给被调用者。
在某些时候,我们需要一些方法来 "instantiate" 某种 void
实例,并且可以选择以某种方式接收它。所以现在我们需要 void
的构造函数(尽管每个 void
方法都可以算作工厂方法,如果你眯着眼睛),我们还需要 void
类型的变量,可能来自 void
到 object
,依此类推。
基本上,为了 所有 的意图和目的,void
必须成为常规类型(例如 a regular empty struct)。这并不可怕,但我认为您可以理解为什么 C# 和 Java 避免它。
What about the second option - postpone overload resolution?
也完全有可能,但请注意,它会有效地将泛型变成较弱的模板。 ("Weaker" 的意思是 C++ templates aren't restricted to typenames。)
同样,这不会是世界末日,但会失去我之前描述的泛型的优势。 C# 和 Java 的设计者显然 想要 保留这些优势。
旁注:
在 C# 中,我知道一种特殊情况,绑定发生在 泛型类型定义验证之后。如果你在 T
上有一个 new()
约束并且你试图 instantiate a new T()
,编译器将生成代码来检查 T
是否是一个值类型。那么:
- 对于值类型,
new T()
变为 default(T)
——记住 C# default struct constructors aren't really constructors in the CLR sense。
- 对于引用类型,调用
Activator.CreateInstance
,这是使用反射的间接构造函数调用。
这种特殊情况非常特殊,因为即使它已将方法绑定完全推迟到运行时,编译器仍然可以执行静态分析、类型检查和代码生成一次。毕竟,表达式 new T()
的类型始终是 T
并且可以轻松解析和验证对具有空形式参数列表的对象的调用。
此程序可以在 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 methods cannot return a value' 的编译器错误。但是由于被调用的方法本身是一个 void,它没有 return 一个值。我知道为了可读性,可能禁止这样的构造。还有其他异议吗?
编辑:为了将来参考,我在这里发现了一些类似的问题
根据 Java Language Specification §14.17:
A return statement with no Expression must be contained in one of the following, or a compile-time error occurs:
- A method that is declared, using the keyword void, not to return a value (§8.4.5)
...
A return statement with an Expression must be contained in one of the following, or a compile-time error occurs:
- A method that is declared to return a value
...
所以,通过声明一个方法是 void
,你是说它 returns 没有价值,所以你只能使用没有表达式的 return;
语句。
这是因为它有可能在模板中使用。 C# 和 Java 禁止将 void
作为类型参数,但 C++ 允许它允许您编写如下模板代码:
template<typename T, typename TResult>
TResult foo(T x, T y)
{
return foo2(x, y);
}
如果 void
方法不允许 return 一个 void
表达式,如果 TResult
是 void
,则此模板实例化将是不可能的。如果是这种情况,如果您希望 TResult
实际上是 void
.
例如,还记得 C# 中有两组通用的通用委托,即 Func<>
和 Action<>
吗?好吧,Action<T>
存在正是因为 Func<T, void>
被禁止。 C++ 设计者不想尽可能地引入这样的情况,因此他们决定允许您使用 void
作为模板参数——而您发现的 case 正是一个有助于实现这一点的功能。
(让我以假装问答的形式写下其余部分。)
But why do C# and Java not allow a similar construct?
首先,了解如何在这些语言中实现泛型编程:
- C# 和 Java 泛型通过解析泛型类型(或方法)定义并确保它对您拥有的泛型 constraints/bounds 有效 来工作提供。
- C++ 模板是 a search-and-replace mechanism with a powerful metaprogramming language around them. They are not required to make sense in the absence of specific template arguments——它们只有在获得实际参数时才会从 "template metalanguage" 到 "C++ language"(可以这么说)。
Why pick one approach of implementing generic programming over the other?
- 泛型方法维护了语言其余部分的 nominal typing。这样做的好处是允许 (AOT) 编译器进行静态分析、类型检查、错误报告、重载解析和最终代码生成 once.
- 模板方法本质上是 duck typing。名义类型语言中的 Duck 类型不具有上述优点,但它允许您更灵活,因为它可能允许 "invalid" 事情(从名义类型的角度来看 "invalid"类型系统),只要您实际上没有在程序中的任何地方提及那些无效的可能性。换句话说,模板允许您统一. 表达更大的案例集
Okay, so what would C# and Java need to do to support
void
as a valid generic argument?
我不得不推测才能回答这个问题,但我会尝试的。
在语言层面,他们必须放弃 return;
仅在 void
方法中有效并且对非 void
方法始终无效的概念。如果没有这个改变,很少有有用的方法可以被实例化——它们可能都必须以递归或无条件的 throw
(which satisfies both void
and non-void
methods without returning) 结束。因此,为了使其有用,C# 和 Java 还必须引入允许您使用 return void
表达式的 C++ 功能。
好吧,假设您已经有了,现在您可以编写如下代码:
void Foo2() { }
void Foo()
{
return Foo2();
}
同样,非泛型版本在 C# 和 Java 中与在 C++ 中一样无用。但是让我们继续前进,看看它的真正用处,那就是在泛型中。
您现在应该能够像这样编写通用代码——TResult
现在可以是 void
(除了已经允许的所有其他类型之外):
TResult Foo<T, TResult>(T a)
{
return Foo2(a);
}
但请记住,在 C# 和 Java 中,重载解析发生在 "early",而不是 "late"。重载决议算法将为每个可能的 TResult
选择相同的被调用者。并且类型检查器将 有 抱怨,因为你要么 return 从可能的非 void
方法中输入 void
表达式,要么你正在 return 从可能的 void
方法中获取非 void
表达式。
也就是说外层方法不能泛型,除非:
- 被调用者也是泛型,其return类型由与外部方法匹配的泛型类型参数定义。
- 泛型类型和方法中的重载解析被推迟到实际类型参数可用时,以便我们可以在调用点选择正确的非泛型方法。
What if we went with the first option - make the callee's return type generic and move on?
我们可以那样做,但这只会把我们的问题推给被调用者。
在某些时候,我们需要一些方法来 "instantiate" 某种 void
实例,并且可以选择以某种方式接收它。所以现在我们需要 void
的构造函数(尽管每个 void
方法都可以算作工厂方法,如果你眯着眼睛),我们还需要 void
类型的变量,可能来自 void
到 object
,依此类推。
基本上,为了 所有 的意图和目的,void
必须成为常规类型(例如 a regular empty struct)。这并不可怕,但我认为您可以理解为什么 C# 和 Java 避免它。
What about the second option - postpone overload resolution?
也完全有可能,但请注意,它会有效地将泛型变成较弱的模板。 ("Weaker" 的意思是 C++ templates aren't restricted to typenames。)
同样,这不会是世界末日,但会失去我之前描述的泛型的优势。 C# 和 Java 的设计者显然 想要 保留这些优势。
旁注:
在 C# 中,我知道一种特殊情况,绑定发生在 泛型类型定义验证之后。如果你在 T
上有一个 new()
约束并且你试图 instantiate a new T()
,编译器将生成代码来检查 T
是否是一个值类型。那么:
- 对于值类型,
new T()
变为default(T)
——记住 C# default struct constructors aren't really constructors in the CLR sense。 - 对于引用类型,调用
Activator.CreateInstance
,这是使用反射的间接构造函数调用。
这种特殊情况非常特殊,因为即使它已将方法绑定完全推迟到运行时,编译器仍然可以执行静态分析、类型检查和代码生成一次。毕竟,表达式 new T()
的类型始终是 T
并且可以轻松解析和验证对具有空形式参数列表的对象的调用。