Pergunta

Por que apoio função de não mais mainstream linguagens de tipagem estática / método de sobrecarga por tipo de retorno? Eu não consigo pensar em nenhum que fazer. Parece não menos útil ou razoável do que apoiar sobrecarga por tipo de parâmetro. Como é que é muito menos popular?

Foi útil?

Solução

Ao contrário do que os outros estão dizendo, sobrecarga por tipo de retorno é possível e é feito por algumas línguas modernas. A objeção usual é que no código como

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

Você não pode dizer que func() está sendo chamado. Isso pode ser resolvido de duas maneiras:

  1. Ter um método previsível para determinar qual função é chamada em tal situação a.
  2. Sempre que tal situação ocorre, é um erro em tempo de compilação. No entanto, tem uma sintaxe que permite que o programador para remover a ambiguidade, por exemplo int main() { (string)func(); }.
  3. não tem efeitos colaterais. Se você não tem efeitos colaterais e você nunca usa o valor de retorno de uma função, então o compilador pode evitar sempre chamando a função em primeiro lugar.

Duas das línguas Eu regularmente ( ab ) uso sobrecarga por tipo de retorno: Perl e Haskell . Deixe-me descrever o que eles fazem.

Em Perl , há uma distinção fundamental entre escalar e lista de contexto (e outros, mas vamos fingir que há dois) . Cada função built-in em Perl pode fazer coisas diferentes dependendo do contexto em que ele é chamado. Por exemplo, o contexto join lista forças operador (na coisa sendo unidos), enquanto o contexto escalar forças operador scalar, então comparar:

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.

Cada operador em Perl faz algo no contexto escalar e algo em contexto de lista, e eles podem ser diferentes, conforme ilustrado. (Este não é apenas para os operadores aleatórios como localtime. Se você usar um @a matriz no contexto de lista, retorna a matriz, enquanto no contexto escalar, ele retorna o número de elementos. Assim, por exemplo print @a imprime os elementos, enquanto print 0+@a imprime o tamanho.) Além disso, cada operador pode força um contexto, por exemplo, forças + adição escalar contexto. Cada entrada no man perlfunc documenta isso. Por exemplo, aqui é parte da entrada para glob EXPR:

No contexto de lista, retorna um (possivelmente vazia) lista de expansões em nome de arquivo o valor de EXPR, tais como a norma Unix shell /bin/csh faria. No escalar contexto, glob itera tais expansões de nome de arquivo, retornando undef quando a lista está esgotado.

Agora, qual é a relação entre a lista e contexto escalar? Bem, man perlfunc diz

Lembre-se a seguinte regra importante: Não há nenhuma regra que relaciona a comportamento de uma expressão na lista contexto, o seu comportamento em escalar contexto, ou vice-versa. Pode fazer duas coisas totalmente diferentes. Cada operador e função decide quais tipo de valor que seria mais apropriar-se para retornar em escalar contexto. Alguns operadores retornam o comprimento da lista que teria foram devolvidos em contexto de lista. Alguns operadores retornar o primeiro valor a lista. Alguns operadores retornam o último valor na lista. Alguns operadores retornar uma contagem de sucesso operações. Em geral, eles fazem o que você quer, a menos que queira consistência.

por isso não é uma simples questão de ter uma única função, e então você fazer a conversão simples no final. Na verdade, eu escolhi o exemplo localtime por esse motivo.

Não é apenas o built-ins que têm este comportamento. Qualquer usuário pode definir tal função um usando wantarray, que lhe permite distinguir entre lista, escalar, e do contexto vazio. Assim, por exemplo, você pode decidir não fazer nada se você está sendo chamado no contexto vazio.

Agora, você pode queixar-se que este não é true sobrecarregar por becaus valor de retornoe você só tem uma função, que é contada contexto é chamado e, em seguida, age sobre essa informação. No entanto, este é claramente equivalente (e análogo ao modo como Perl não permite sobrecarga habitual literalmente, mas uma função pode apenas examinar os seus argumentos). Além disso, bem resolve a situação ambígua mencionado no início desta resposta. Perl não se queixam de que ele não sabe qual o método a chamada; ele só chama. Tudo o que tem a fazer é descobrir o que contexto a função foi chamado, que é sempre possível:

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"

(Nota:.. Eu às vezes pode dizer operador Perl quando função média Este não é crucial para esta discussão)

Haskell leva a outra abordagem, ou seja, não tem efeitos colaterais. Ele também tem um sistema de tipo forte, e assim você pode escrever código como o seguinte:

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

Este código lê um número de ponto flutuante da entrada padrão, e imprime sua raiz quadrada. Mas o que é surpreendente sobre isso? Bem, o tipo de readLn é readLn :: Read a => IO a. O que isto significa é que, para qualquer tipo que pode ser Read (formalmente, cada tipo que é uma instância da classe tipo Read), readLn pode lê-lo. Como Haskell sabe que eu queria ler um número de ponto flutuante? Bem, o tipo de sqrt é sqrt :: Floating a => a -> a, que essencialmente significa que sqrt só pode aceitar números de ponto flutuante como entradas, e assim por Haskell inferir o que eu queria.

O que acontece quando Haskell não se pode inferir que eu quero? Bem, há algumas possibilidades. Se eu não usar o valor de retorno em tudo, Haskell simplesmente não vai chamar a função em primeiro lugar. No entanto, se eu do usar o valor de retorno, então Haskell vai reclamar que não pode inferir o tipo:

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

Eu posso resolver a ambiguidade, especificando o tipo que eu quero:

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

De qualquer forma, o que todo este meio de discussão é que a sobrecarga por valor de retorno é possível e é feito, que responde parte da sua pergunta.

A outra parte da sua pergunta é por isso que mais línguas não fazê-lo. Vou deixar que os outros responder a isso. No entanto, alguns comentários: a principal razão é provavelmente que a oportunidade para confusão é realmente maior aqui do que em sobrecarga por tipo de argumento. Você também pode olhar para lógicas de línguas individuais:

Ada : "Pode parecer que o regra mais simples de resolução de sobrecarga é usar tudo - todas as informações a partir de tão grande de um contexto possível -. para resolver a referência sobrecarregado Esta regra pode ser simples, mas não é útil ela exige que o leitor humano para fazer a varredura arbitrariamente grandes pedaços de texto,. e fazer inferências arbitrariamente complexas (como (g) acima). acreditamos que uma regra melhor é aquele que faz explícita a tarefa um leitor humano ou um compilador deve executar, e que torna essa tarefa tão natural para o leitor humano quanto possível . "

C ++ (subseção 7.4.1of Bjarne Stroustrup "The C ++ Programming Language"): "tipos de retorno não são considerados na resolução de sobrecarga A razão é manter a resolução para um operador individual ou chamada de função independente de contexto Considere o seguinte:..

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)
}

Se o tipo de retorno foram tidos em conta, ele não seria mais possível olhar para uma chamada de sqrt() em isolamento e determinar qual a função foi chamado."(Nota, para comparação, que em Haskell não existem implícita conversões.)

Java ( Java Language Specification 9.4.1 ): "um dos métodos herdados deve deve ser retorno tipo substituível por qualquer outro método herdado, caso contrário, ocorre um erro de tempo de compilação." (Sim, eu sei que isso não faz GIVum e uma lógica. Tenho certeza que o raciocínio é dada por Gosling em "A linguagem de programação Java". Talvez alguém tenha uma cópia? Aposto que é o "princípio da menor surpresa" em essência) No entanto, diversão fato sobre Java:. JVM permite sobrecarga por valor de retorno! Isto é usado, por exemplo, em Scala , e pode ser acessado diretamente através de Java assim por brincar com internos.

PS. Como nota final, é realmente possível sobrecarga por valor de retorno em C ++ com um truque. Testemunha:

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
}

Outras dicas

If functions were overloaded by the return type and you had these two overloads

int func();
string func();

there is no way the compiler could figure out which of those two functions to call upon seeing a call like this

void main() 
{
    func();
}

For this reason, language designers often disallow return-value overloading.

Some languages (such as MSIL), however, do allow overloading by return type. They too face the above difficulty of course, but they have workarounds, for which you'll have to consult their documentation.

In such a language, how would you resolve the following:

f(g(x))

if f had overloads void f(int) and void f(string) and g had overloads int g(int) and string g(int)? You would need some kind of disambiguator.

I think the situations where you might need this would be better served by choosing a new name for the function.

To steal a C++ specific answer from another very similar question (dupe?):


Function return types don't come into play in overload resolution simply because Stroustrup (I assume with input from other C++ architects) wanted overload resolution to be 'context independent'. See 7.4.1 - "Overloading and Return Type" from the "C++ Programming Language, Third Edition".

The reason is to keep resolution for an individual operator or function call context-independent.

They wanted it to be based only on how the overload was called - not how the result was used (if it was used at all). Indeed, many functions are called without using the result or the result would be used as part of a larger expression. One factor that I'm sure came into play when they decided this was that if the return type was part of the resolution there would be many calls to overloaded functions that would need to be resolved with complex rules or would have to have the compiler throw an error that the call was ambiguous.

And, Lord knows, C++ overload resolution is complex enough as it stands...

In haskell it's possible even though it doesn't have function overloading. Haskell uses type classes. In a program you could see:

class Example a where
    example :: Integer -> a

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

Function overloading itself is not so popular. Mostly languages I've seen with it are C++, perhaps java and/or C#. In all dynamic languages it's a shorthand for:

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()

Therefore there's no much point in it. Most people aren't interested whether language can help you drop a single line per where ever you use it.

Pattern matching is somewhat similar to function overloading, and I guess sometimes work similarly. It's not common though because it is useful only for few programs and is tricky to implement on most of languages.

You see there's infinitely many other better easier-to-implement features to implement into the language, including:

  • Dynamic typing
  • Internal support for lists, dictionaries and unicode strings
  • Optimizations (JIT, type inferencing, compiling)
  • Integrated deployment tools
  • Library support
  • Community support and gathering places
  • Rich standard libraries
  • Good syntax
  • Read eval print loop
  • Support for reflective programming

Good answers! A.Rex's answer in particular is very detailed and instructive. As he points out, C++ does consider user-supplied type-conversion operators when compiling lhs = func(); (where func is really the name of a struct). My workaround is a bit different - not better, just different (although it's based on the same basic idea).

Whereas I had wanted to write...

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++!

I ended up with a solution that uses a parameterized struct (with T = the return type):

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 :)

A benefit of this solution is that any code which includes these template definitions can add more specializations for more types. Also you can do partial specializations of the struct as needed. For example, if you wanted special handling for pointer types:

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

As a negative, you can't write int x = func(); with my solution. You have to write int x = func<int>();. You have to explicitly say what the return type is, rather than asking the compiler to suss it out by looking at type conversion operators. I would say that "my" solution and A.Rex's both belong in a pareto-optimal front of ways to tackle this C++ dilemma :)

if you want to overload methods with different return types, just add a dummy parameter with default value to allow the overload execution, but don't forget the parameter type should be different so the overload logic works next is an e.g on delphi:

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

use it like this

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;

As already shown - ambiguous calls of a function that differs only by return type introduces ambiguity. Ambiguity induces defective code. Defective code must be avoided.

The complexity driven by the attempt to ambiguity shows that this is not a good hack. Apart from an intellectual exercise - why not use procedures with reference parameters.

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

this overloading feature is not hard to manage, if you look at it in a slightly different way. consider the following,

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

if a language did return overloading it would allow parameter overloading, but not duplications. this would solve the problem of:

main (){
f(x)
}

because there is only one f(int choice) to choose from.

In .NET, sometimes we use one parameter to indicate the desired output from a generic result, and then made a conversion to get what we expect.

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();
    }

Maybe this example could help too:

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;
}

For the record, Octave allows different outcome according to return element being scalar vs array.

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

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

Cf also Singular Value Decomposition.

This one is slightly different for C++; I don't know if it would be considered overloading by return type directly. It is more of a template specialization that acts in the manner of.

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;
}

This example is not exactly using function overload resolution by return type, however this c++ non object class is using template specialization to simulate function overload resolution by return type with a private static method.

Each of the convertToType functions are calling the function template stringToValue() and if you look at the implementation details or algorithm of this function template it is calling getValue<T>( param, param ) and it is returning back a type T and storing it into a T* that is passed into the stringToValue() function template as one of its parameters.

Other than something like this; C++ does not really have a mechanism to have function overloading resolution by return type. There may be other constructs or mechanisms that I'm not aware of that could simulate resolution by return type.

I think this is a GAP in modern C++ definition… why ?

int func();
double func();

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

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

// example 3. → NOT defined. error
void main() 
{
    func();
}

Why can a C++ compiler can not throw an error in example "3" and accept the code in example "1+2" ??

Most static languages also now support generics, which would solve your problem. As stated before, without having parameter diffs, there is not way to know which one to call. So if you want to do this, just use generics and call it a day.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top