按返回类型重载函数?

为什么主流的静态类型语言不支持按返回类型重载函数/方法?我想不出有什么能做到。这似乎并不比支持按参数类型重载更有用或更合理。为什么它不那么受欢迎呢?

102358 次浏览

在这样一种语言中,你将如何解决以下问题:

f(g(x))

如果f有重载void f(int)void f(string)g有重载int g(int)string g(int)?你需要某种消歧器。

我认为,在需要这个函数的情况下,最好为函数选择一个新名称。

窃取c++特定的答案来自另一个非常相似的问题 (dupe?):


函数返回类型不会在重载解析中发挥作用,因为Stroustrup(我假设来自其他c++架构师的输入)希望重载解析是“上下文独立的”。参见“c++编程语言,第三版”中的“重载和返回类型”。

原因是为了保持独立于上下文的单个操作符或函数调用的解析。

他们希望它只基于重载是如何被调用的——而不是结果是如何被使用的(如果它被使用的话)。实际上,许多函数在调用时不使用结果,或者结果将被用作更大表达式的一部分。当他们决定这样做时,我确信其中一个因素是,如果返回类型是解析的一部分,那么就会有许多重载函数的调用,需要使用复杂的规则进行解析,或者必须让编译器抛出调用有歧义的错误。

而且,上帝知道,c++的重载解析已经足够复杂了……

如果函数被返回类型重载并且有这两个重载

int func();
string func();

在看到这样的调用时,编译器无法确定调用这两个函数中的哪一个

void main()
{
func();
}

由于这个原因,语言设计者通常不允许返回值重载。

然而,一些语言(如MSIL), 允许根据返回类型重载。当然,它们也面临上述困难,但它们有变通办法,为此您必须查阅它们的文档。

大多数静态语言现在也支持泛型,这将解决您的问题。如前所述,如果没有参数差异,就无法知道调用哪一个。如果你想这样做,使用泛型就可以了。

与其他人所说的相反,通过返回类型重载是可能的,并且在一些现代语言中是可以实现的。通常的反对意见是,在代码中

int func();
string func();
int main() { func(); }

你不能告诉哪个func()正在被调用。这个问题可以通过以下几种方式解决:

  1. 有一个可预测的方法来确定在这种情况下调用哪个函数。
  2. 无论何时出现这种情况,都是编译时错误。但是,要有一个允许程序员消除歧义的语法,例如int main() { (string)func(); }
  3. 没有副作用。如果你没有副作用,并且你从不使用函数的返回值,那么编译器可以避免在第一个地方调用函数。

我经常使用的两种语言(ab)通过返回类型使用重载:PerlHaskell。让我来描述一下他们的工作。

Perl中,标量列表上下文(以及其他上下文,但我们假设有两个)之间有基本的区别。Perl中的每个内置函数都可以做不同的事情,这取决于调用它的上下文。例如,join操作符强制使用列表上下文(在被连接的对象上),而scalar操作符强制使用标量上下文,因此比较:

print join " ", localtime(); # printed "58 11 2 14 0 109 3 13 0" for me right now
print scalar localtime(); # printed "Wed Jan 14 02:12:44 2009" for me right now.

Perl中的每个操作符都在标量上下文中和列表上下文中做一些事情,它们可能是不同的,如图所示。(这不仅仅适用于像localtime这样的随机操作符。如果在列表上下文中使用数组@a,它将返回数组,而在标量上下文中,它将返回元素的数量。例如,print @a打印元素,而print 0+@a打印大小。)此外,每个操作符都可以上下文,例如,添加+强制标量上下文。man perlfunc中的每个条目都记录了这一点。例如,下面是glob EXPR条目的一部分:

在列表上下文中,返回一个(可能 空)上的文件名扩展列表 EXPR的值,例如标准 Unix shell /bin/csh就可以了。在 标量上下文,glob迭代遍历 这样的文件名扩展,返回

现在,列表和标量上下文之间的关系是什么?好吧,man perlfunc

记住下面的重要规则: 没有相关的规则 表达式在列表中的行为 其行为在标量中的上下文 背景,反之亦然。也许可以 两种完全不同的东西。每一个 运算符和函数决定哪个 这是最有价值的 适合于返回标量 上下文。一些运算符返回 列表的长度 在列表上下文中返回。一些 运算符返回in的第一个值 列表中。一些运算符返回 列表中的最后一个值。一些 运算符返回成功的计数 操作。一般来说,他们做什么 除非你想要一致性

所以这不是一个简单的函数,然后在最后做简单的转换。事实上,我选择localtime例子就是出于这个原因。

并不是只有内置程序才有这种行为。任何用户都可以使用wantarray定义这样的函数,它允许你区分列表、标量和空上下文。例如,你可以决定什么都不做如果你在void context中被调用。

现在,你可能会抱怨这不是真正的通过返回值重载,因为你只有一个函数,它被告知它被调用的上下文,然后对该信息进行操作。然而,这显然是等价的(类似于Perl不允许通常的重载,但函数可以只检查它的参数)。此外,它很好地解决了这个响应开头提到的模棱两可的情况。Perl不会抱怨它不知道调用哪个方法;它只是调用它。它所要做的就是弄清楚函数是在什么上下文中被调用的,这总是可能的:

sub func {
if( not defined wantarray ) {
print "void\n";
} elsif( wantarray ) {
print "list\n";
} else {
print "scalar\n";
}
}


func(); # prints "void"
() = func(); # prints "list"
0+func(); # prints "scalar"

(注:当我指的是函数时,有时我可能会说Perl操作符。这不是本文讨论的关键。)

Haskell采用另一种方法,即没有副作用。它还具有强类型系统,因此您可以编写如下代码:

main = do n <- readLn
print (sqrt n) -- note that this is aligned below the n, if you care to run this

这段代码从标准输入读取一个浮点数,并输出它的平方根。但这有什么令人惊讶的呢?嗯,readLn的类型是readLn :: Read a => IO a。这意味着对于任何可以是Read的类型(形式上,是Read类型类的每个实例的类型),readLn都可以读取它。Haskell怎么知道我想读取一个浮点数?好吧,sqrt的类型是sqrt :: Floating a => a -> a,这本质上意味着sqrt只能接受浮点数作为输入,所以Haskell推断了我想要的东西。

要是哈斯克尔猜不出我想要什么怎么办?有几种可能性。如果我根本不使用返回值,Haskell就不会首先调用该函数。然而,如果我使用返回值,那么Haskell将抱怨它无法推断类型:

main = do n <- readLn
print n
-- this program results in a compile-time error "Unresolved top-level overloading"

我可以通过指定我想要的类型来解决歧义:

main = do n <- readLn
print (n::Int)
-- this compiles (and does what I want)

总之,这整个讨论的意思是,通过返回值重载是可能的,并且已经完成了,这回答了您的部分问题。

你问题的另一部分是为什么没有更多的语言这样做。我让别人来回答这个问题。然而,一些评论:主要原因可能是,在这里,混淆的机会确实比按参数类型重载更大。你也可以看看个别语言的基本原理:

艾达: "看起来最简单的重载解析规则是使用所有东西——来自尽可能宽的上下文中的所有信息——来解析重载引用。这条规则可能很简单,但并没有什么帮助。它要求人类读者扫描任意大块的文本,并做出任意复杂的推论(如上文(g))。我们认为,更好的规则是明确人类读者或编译器必须执行的任务,并使这个任务对人类读者来说尽可能自然。”

c++ (Bjarne Stroustrup的“c++编程语言”的7.4.1小节):“在重载解析中不考虑返回类型。原因是为了保持独立于上下文的单个操作符或函数调用的解析。考虑:

float sqrt(float);
double sqrt(double);


void f(double da, float fla)
{
float fl = sqrt(da);     // call sqrt(double)
double d = sqrt(da); // call sqrt(double)
fl = sqrt(fla);            // call sqrt(float)
d = sqrt(fla);             // call sqrt(float)
}

如果将返回类型考虑在内,就不可能再单独查看sqrt()的调用并确定调用了哪个函数。”(注意,为了便于比较,Haskell中没有隐式的转换。)

Java (Java语言规范9.4.1): "继承的方法中必须有一个是其他继承方法的return-type- substitute,否则会发生编译时错误。"(是的,我知道这没有一个合理的理由。我相信Gosling在“Java编程语言”中给出了基本原理。也许有人有副本?我敢打赌,这本质上就是“最小意外原则”。)然而,关于Java的一个有趣的事实:JVM 允许通过返回值重载!例如,在Scala中使用,并且可以通过处理内部函数来访问直接通过Java

PS.最后需要注意的是,在c++中,通过一个技巧可以通过返回值重载。证人:

struct func {
operator string() { return "1";}
operator int() { return 2; }
};


int main( ) {
int x    = func(); // calls int version
string y = func(); // calls string version
double d = func(); // calls int version
cout << func() << endl; // calls int version
func(); // calls neither
}

在haskell中,这是可能的,即使它没有函数重载。Haskell使用类型类。在程序中你可以看到:

class Example a where
example :: Integer -> a


instance Example Integer where  -- example is now implemented for Integer
example :: Integer -> Integer
example i = i * 10

函数重载本身并不流行。我所见过的大多数使用它的语言是c++,也许是java和/或c#。在所有动态语言中,它是:

define example:i
↑i type route:
Integer = [↑i & 0xff]
String = [↑i upper]




def example(i):
if isinstance(i, int):
return i & 0xff
elif isinstance(i, str):
return i.upper()

因此没有什么意义。大多数人不感兴趣的是语言是否能帮助你在使用它的时候省去一行字。

模式匹配有点类似于函数重载,我想有时工作方式也类似。但它并不常见,因为它只对少数程序有用,而且在大多数语言上实现起来很棘手。

你可以看到,在语言中还有无限多更好更容易实现的特性,包括:

  • 动态类型
  • 内部支持列表,字典和unicode字符串
  • 优化(JIT、类型推断、编译)
  • 集成部署工具
  • 库支持
  • 社区支持和聚集场所
  • 丰富的标准库
  • 好的语法
  • 读取eval打印循环
  • 对反射编程的支持

好的答案!A.Rex的回答尤其详细且有启发性。正如他指出的,c++ 在编译lhs = func(); (func实际上是一个结构体的名字)时考虑用户提供的类型转换操作符。我的解决方法有点不同——不是更好,只是不同(尽管它基于相同的基本思想)。

而我有想要要写…

template <typename T> inline T func() { abort(); return T(); }


template <> inline int func()
{ <<special code for int>> }


template <> inline double func()
{ <<special code for double>> }


.. etc, then ..


int x = func(); // ambiguous!
int x = func<int>(); // *also* ambiguous!?  you're just being difficult, g++!

我最终得到了一个使用参数化结构体(T =返回类型)的解决方案:

template <typename T>
struct func
{
operator T()
{ abort(); return T(); }
};


// explicit specializations for supported types
// (any code that includes this header can add more!)


template <> inline
func<int>::operator int()
{ <<special code for int>> }


template <> inline
func<double>::operator double()
{ <<special code for double>> }


.. etc, then ..


int x = func<int>(); // this is OK!
double d = func<double>(); // also OK :)

这种解决方案的一个好处是,任何包含这些模板定义的代码都可以为更多类型添加更多专门化。此外,您还可以根据需要对结构进行部分特殊化。例如,如果你想对指针类型进行特殊处理:

template <typename T>
struct func<T*>
{
operator T*()
{ <<special handling for T*>> }
};

作为一个负数,你不能用我的解写int x = func();。你必须写int x = func<int>();。您必须显式地说明返回类型是什么,而不是让编译器通过查看类型转换操作符来确定它。我想说的是,“我的”解决方案和a . rex的解决方案都属于解决c++困境的帕累托最优前方法:)

如前所述-仅因返回类型而不同的函数的模糊调用会引入模糊。 模糊性会导致有缺陷的代码。 必须避免有缺陷的代码 试图产生歧义所导致的复杂性表明这不是一个好的hack。 除了一个智力练习-为什么不使用带有引用参数的过程

procedure(reference string){};
procedure(reference int){};
string blah;
procedure(blah)

如果你稍微换个角度来看,这个重载特性并不难管理。考虑以下几点:

public Integer | String f(int choice){
if(choice==1){
return new string();
}else{
return new Integer();
}}

如果一种语言确实返回重载,它将允许参数重载,但不允许重复。 这将解决:

的问题
main (){
f(x)
}

因为只有一个f(int)选项可供选择。

在. net中,有时我们使用一个参数来指示一个通用结果的期望输出,然后进行转换以得到我们期望的结果。

c#

public enum FooReturnType{
IntType,
StringType,
WeaType
}


class Wea {
public override string ToString()
{
return "Wea class";
}
}


public static object Foo(FooReturnType type){
object result = null;
if (type == FooReturnType.IntType)
{
/*Int related actions*/
result = 1;
}
else if (type == FooReturnType.StringType)
{
/*String related actions*/
result = "Some important text";
}
else if (type == FooReturnType.WeaType)
{
/*Wea related actions*/
result = new Wea();
}
return result;
}


static void Main(string[] args)
{
Console.WriteLine("Expecting Int from Foo: " + Foo(FooReturnType.IntType));
Console.WriteLine("Expecting String from Foo: " + Foo(FooReturnType.StringType));
Console.WriteLine("Expecting Wea from Foo: " + Foo(FooReturnType.WeaType));
Console.Read();
}

也许这个例子也有帮助:

c++

    #include <iostream>


enum class FooReturnType{ //Only C++11
IntType,
StringType,
WeaType
}_FooReturnType;


class Wea{
public:
const char* ToString(){
return "Wea class";
}
};


void* Foo(FooReturnType type){
void* result = 0;
if (type == FooReturnType::IntType) //Only C++11
{
/*Int related actions*/
result = (void*)1;
}
else if (type == FooReturnType::StringType) //Only C++11
{
/*String related actions*/
result = (void*)"Some important text";
}
else if (type == FooReturnType::WeaType) //Only C++11
{
/*Wea related actions*/
result = (void*)new Wea();
}
return result;
}


int main(int argc, char* argv[])
{
int intReturn = (int)Foo(FooReturnType::IntType);
const char* stringReturn = (const char*)Foo(FooReturnType::StringType);
Wea *someWea = static_cast<Wea*>(Foo(FooReturnType::WeaType));
std::cout << "Expecting Int from Foo: " << intReturn << std::endl;
std::cout << "Expecting String from Foo: " << stringReturn << std::endl;
std::cout << "Expecting Wea from Foo: " << someWea->ToString() << std::endl;
delete someWea; // Don't leak oil!
return 0;
}

根据返回元素是标量还是数组,倍频程允许不同的结果。

x = min ([1, 3, 0, 2, 0])
⇒  x = 0


[x, ix] = min ([1, 3, 0, 2, 0])
⇒  x = 0
ix = 3 (item index)

Cf也奇异值分解

这一点在c++中略有不同;我不知道它是否会被认为直接通过返回类型重载。它更像是一种模板专门化,以。

util.h

#ifndef UTIL_H
#define UTIL_H


#include <string>
#include <sstream>
#include <algorithm>


class util {
public:
static int      convertToInt( const std::string& str );
static unsigned convertToUnsigned( const std::string& str );
static float    convertToFloat( const std::string& str );
static double   convertToDouble( const std::string& str );


private:
util();
util( const util& c );
util& operator=( const util& c );


template<typename T>
static bool stringToValue( const std::string& str, T* pVal, unsigned numValues );


template<typename T>
static T getValue( const std::string& str, std::size_t& remainder );
};


#include "util.inl"


#endif UTIL_H

util.inl

template<typename T>
static bool util::stringToValue( const std::string& str, T* pValue, unsigned numValues ) {
int numCommas = std::count(str.begin(), str.end(), ',');
if (numCommas != numValues - 1) {
return false;
}


std::size_t remainder;
pValue[0] = getValue<T>(str, remainder);


if (numValues == 1) {
if (str.size() != remainder) {
return false;
}
}
else {
std::size_t offset = remainder;
if (str.at(offset) != ',') {
return false;
}


unsigned lastIdx = numValues - 1;
for (unsigned u = 1; u < numValues; ++u) {
pValue[u] = getValue<T>(str.substr(++offset), remainder);
offset += remainder;
if ((u < lastIdx && str.at(offset) != ',') ||
(u == lastIdx && offset != str.size()))
{
return false;
}
}
}
return true;
}

util.cpp

#include "util.h"


template<>
int util::getValue( const std::string& str, std::size_t& remainder ) {
return std::stoi( str, &remainder );
}


template<>
unsigned util::getValue( const std::string& str, std::size_t& remainder ) {
return std::stoul( str, &remainder );
}


template<>
float util::getValue( const std::string& str, std::size_t& remainder ) {
return std::stof( str, &remainder );
}


template<>
double util::getValue( const std::string& str, std::size_t& remainder ) {
return std::stod( str, &remainder );
}


int util::convertToInt( const std::string& str ) {
int i = 0;
if ( !stringToValue( str, &i, 1 ) ) {
std::ostringstream strStream;
strStream << __FUNCTION__ << " Bad conversion of [" << str << "] to int";
throw strStream.str();
}
return i;
}


unsigned util::convertToUnsigned( const std::string& str ) {
unsigned u = 0;
if ( !stringToValue( str, &u, 1 ) ) {
std::ostringstream strStream;
strStream << __FUNCTION__ << " Bad conversion of [" << str << "] to unsigned";
throw strStream.str();
}
return u;
}


float util::convertToFloat(const std::string& str) {
float f = 0;
if (!stringToValue(str, &f, 1)) {
std::ostringstream strStream;
strStream << __FUNCTION__ << " Bad conversion of [" << str << "] to float";
throw strStream.str();
}
return f;
}


double util::convertToDouble(const std::string& str) {
float d = 0;
if (!stringToValue(str, &d, 1)) {
std::ostringstream strStream;
strStream << __FUNCTION__ << " Bad conversion of [" << str << "] to double";
throw strStream.str();
}
return d;
}

这个例子并没有精确地使用根据返回类型的函数重载解析,但是这个c++非对象类使用模板专门化来通过私有静态方法模拟根据返回类型的函数重载解析。

每个convertToType函数都在调用函数模板stringToValue(),如果你查看这个函数模板的实现细节或算法,它正在调用getValue<T>( param, param ),并返回一个类型T,并将其存储到T*中,后者作为参数之一传递给stringToValue()函数模板。

除了这样的东西;c++并没有通过返回类型来实现函数重载解析的机制。可能还有我不知道的其他构造或机制可以通过返回类型模拟解析。

我认为这是现代c++定义中的一个GAP……为什么?

int func();
double func();


// example 1. → defined
int i = func();


// example 2. → defined
double d = func();


// example 3. → NOT defined. error
void main()
{
func();
}
为什么c++编译器不能抛出例子“3”中的错误 接受例子“1+2”中的代码??< / p >

如果你想重载具有不同返回类型的方法,只需添加使用默认值的虚拟参数来允许重载执行,但不要忘记参数类型应该不同,因此重载逻辑工作接下来是delphi中的示例:

type
myclass = class
public
function Funct1(dummy: string = EmptyStr): String; overload;
function Funct1(dummy: Integer = -1): Integer; overload;
end;

像这样使用它

procedure tester;
var yourobject : myclass;
iValue: integer;
sValue: string;
begin
yourobject:= myclass.create;
iValue:= yourobject.Funct1(); //this will call the func with integer result
sValue:= yourobject.Funct1(); //this will call the func with string result
end;