Pregunta

¿Por qué no corriente más de tipo estático de soporte de idiomas de la función/método de sobrecarga por tipo de retorno?Yo puedo pensar en nada que hacer.Parece que no es menos útil o razonable de apoyo a la sobrecarga por tipo de parámetro.¿Cómo es que es mucho menos popular?

¿Fue útil?

Solución

Al contrario de lo que dicen otros, la sobrecarga por tipo de retorno es posible y se realiza en algunos idiomas modernos. La objeción habitual es que en código como

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

no se puede saber a qué func() se está llamando. Esto se puede resolver de varias maneras:

  1. Tenga un método predecible para determinar qué función se llama en tal situación.
  2. Cada vez que ocurre una situación así, es un error en tiempo de compilación. Sin embargo, tiene una sintaxis que permite que el programador desambigüe, p. int main() { (string)func(); }.
  3. No tiene efectos secundarios. Si no tiene efectos secundarios y nunca usa el valor de retorno de una función, entonces el compilador puede evitar llamar a la función en primer lugar.

Dos de los idiomas que uso regularmente ( ab ) utilice la sobrecarga por tipo de retorno: Perl y Haskell . Déjame describir lo que hacen.

En Perl , hay una distinción fundamental entre el contexto escalar y list (y otros, pero fingiremos que hay dos) . Cada función incorporada en Perl puede hacer cosas diferentes dependiendo del contexto en el que se llama. Por ejemplo, el operador join fuerza el contexto de la lista (en la cosa que se está uniendo) mientras que el operador scalar fuerza el contexto escalar, así que compare:

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 en Perl hace algo en contexto escalar y algo en contexto de lista, y pueden ser diferentes, como se ilustra. (Esto no es solo para operadores aleatorios como localtime. Si usa una matriz @a en el contexto de la lista, devuelve la matriz, mientras que en el contexto escalar, devuelve el número de elementos. Por ejemplo, print @a imprime los elementos, mientras que print 0+@a imprime el tamaño.) Además, cada operador puede forzar un contexto, p. ej. Además + fuerza el contexto escalar. Cada entrada en man perlfunc documenta esto. Por ejemplo, aquí es parte de la entrada para glob EXPR:

  

En el contexto de la lista, devuelve un (posiblemente   vacío) lista de expansiones de nombre de archivo en   el valor de EXPR como el estándar   Unix shell /bin/csh haría. En   contexto escalar, glob itera a través de   tales expansiones de nombre de archivo, volviendo   undef cuando la lista se agota.

Ahora, ¿cuál es la relación entre la lista y el contexto escalar? Bueno, wantarray dice

  

Recuerde la siguiente regla importante:   No hay una regla que relacione el   comportamiento de una expresión en la lista   contexto a su comportamiento en escalar   contexto, o viceversa. Podría hacer   Dos cosas totalmente diferentes. Cada   operador y función decide qué   tipo de valor sería más   apropiado para volver en escalar   contexto. Algunos operadores devuelven el   longitud de la lista que tendría   sido devuelto en el contexto de la lista. Algunos   los operadores devuelven el primer valor en   la lista. Algunos operadores devuelven el   último valor en la lista. Algunos   los operadores devuelven un recuento de éxito   operaciones En general, hacen lo que   desea, a menos que desee coherencia.

así que no es simple tener una sola función, y luego hacer una conversión simple al final. De hecho, elegí el readLn ejemplo por ese motivo.

No son solo los elementos integrados los que tienen este comportamiento. Cualquier usuario puede definir dicha función utilizando readLn :: Read a => IO a, que le permite distinguir entre lista, escalar y contexto vacío. Entonces, por ejemplo, puedes decidir no hacer nada si te llaman en un contexto vacío.

Ahora, puede quejarse de que no se trata de una sobrecarga verdadera por valor de retorno porque solo tiene una función, que se le dice en el contexto en el que se invoca y luego actúa sobre esa información. Sin embargo, esto es claramente equivalente (y análogo a cómo Perl no permite la sobrecarga habitual literalmente, pero una función solo puede examinar sus argumentos). Por otra parte, muy bien resoLe encanta la situación ambigua mencionada al comienzo de esta respuesta. Perl no se queja de que no sabe a qué método llamar; solo lo llama. Todo lo que tiene que hacer es averiguar en qué contexto se llamó a la función, que siempre es posible:

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: a veces puedo decir operador Perl cuando me refiero a la función. Esto no es crucial para esta discusión).

Haskell adopta el otro enfoque, es decir, no tener efectos secundarios. También tiene un sistema de tipo fuerte, por lo que puede escribir código como el siguiente:

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

Este código lee un número de coma flotante de la entrada estándar e imprime su raíz cuadrada. Pero, ¿qué es lo sorprendente de esto? Bueno, el tipo de Read es sqrt. Lo que esto significa es que para cualquier tipo que puede ser sqrt :: Floating a => a -> a (formalmente, cada tipo que es una instancia de la clase de tipo sqrt()), <=> puede leerlo. ¿Cómo sabía Haskell que quería leer un número de coma flotante? Bueno, el tipo de <=> es <=>, lo que esencialmente significa que <=> solo puede aceptar números de punto flotante como entradas, por lo que Haskell dedujo lo que quería.

¿Qué sucede cuando Haskell no puede inferir lo que quiero? Bueno, hay algunas posibilidades. Si no uso el valor de retorno, Haskell simplemente no llamará a la función en primer lugar. Sin embargo, si do uso el valor de retorno, Haskell se quejará de que no puede inferir el tipo:

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

Puedo resolver la ambigüedad especificando el tipo que quiero:

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

De todos modos, lo que significa toda esta discusión es que la sobrecarga por valor de retorno es posible y se hace, lo que responde parte de su pregunta.

La otra parte de tu pregunta es por qué no lo hacen más idiomas. Dejaré que otros respondan eso. Sin embargo, algunos comentarios: la razón principal es probablemente que la oportunidad de confusión es realmente mayor aquí que en la sobrecarga por tipo de argumento. También puede ver los fundamentos de idiomas individuales:

Ada : " It Podría parecer que la regla de resolución de sobrecarga más simple es usar todo, toda la información del contexto más amplio posible, para resolver la referencia sobrecargada. Esta regla puede ser simple, pero no es útil. Requiere que el lector humano escanee piezas de texto arbitrariamente grandes y que haga inferencias arbitrariamente complejas (como (g) arriba). Creemos que una mejor regla es aquella que hace explícita la tarea que debe realizar un lector humano o un compilador, y que hace que esta tarea sea lo más natural posible para el lector humano. & Quot;

C ++ (subsección 7.4.1 de Bjarne Stroustrup's " El lenguaje de programación C ++ "): " Los tipos de retorno no se consideran en la resolución de sobrecarga. La razón es mantener la resolución de un operador individual o llamada de función independiente del contexto. Considere:

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

Si se tuviera en cuenta el tipo de retorno, ya no sería posible mirar una llamada de <=> de forma aislada y determinar qué función se llamó. " (Tenga en cuenta, para comparar, que en Haskell no hay conversiones implícitas ).

Java ( Especificación del lenguaje Java 9.4.1 ): " Uno de los métodos heredados debe ser el tipo de retorno sustituible por cualquier otro método heredado; de lo contrario, se produce un error en tiempo de compilación. " (Sí, sé que esto no da una razón. Estoy seguro de que la razón es dada por Gosling en & Quot; el lenguaje de programación Java & Quot ;. ¿Quizás alguien tiene una copia? Apuesto a que es el < !> quot; principio de la menor sorpresa " en esencia.) Sin embargo, un dato curioso sobre Java: ¡JVM permite sobrecargar por valor de retorno! Esto se usa, por ejemplo, en Scala , y puede seraccedido a directamente a través de Java también jugando con elementos internos.

PS. Como nota final, en realidad es posible sobrecargar por valor de retorno en C ++ con un truco. Testigo:

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
}

Otros consejos

Si las funciones fueron sobrecargadas por el tipo de retorno y usted tuvo estas dos sobrecargas

int func();
string func();

no hay forma de que el compilador pueda determinar a cuál de esas dos funciones llamar al ver una llamada como esta

void main() 
{
    func();
}

Por esta razón, los diseñadores de idiomas a menudo no permiten la sobrecarga del valor de retorno.

Algunos idiomas (como MSIL), sin embargo, do permiten la sobrecarga por tipo de retorno. También enfrentan la dificultad anterior, por supuesto, pero tienen soluciones alternativas, por lo que deberá consultar su documentación.

En tal idioma, ¿cómo se resuelve el siguiente:

f(g(x))

si f había sobrecargas void f(int) y void f(string) y g había sobrecargas int g(int) y string g(int)?Usted podría necesitar algún tipo de disambiguator.

Creo que las situaciones en las que usted puede ser que necesite esto sería mejor servido por la elección de un nuevo nombre para la función.

En haskell es posible aunque no tenga sobrecarga de funciones. Haskell usa clases de tipo. En un programa puedes ver:

class Example a where
    example :: Integer -> a

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

La sobrecarga de funciones en sí misma no es tan popular. La mayoría de los lenguajes que he visto con él son C ++, quizás java y / o C #. En todos los lenguajes dinámicos es una abreviatura de:

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

Por lo tanto, no tiene mucho sentido. La mayoría de las personas no están interesadas en saber si el idioma puede ayudarlo a soltar una sola línea por donde lo use.

La coincidencia de patrones es algo similar a la sobrecarga de funciones, y supongo que a veces funciona de manera similar. Sin embargo, no es común porque es útil solo para algunos programas y es difícil de implementar en la mayoría de los idiomas.

Verá que hay infinitas otras funciones mejores y más fáciles de implementar para implementar en el lenguaje, que incluyen:

  • Escritura dinámica
  • Soporte interno para listas, diccionarios y cadenas unicode
  • Optimizaciones (JIT, inferencia de tipos, compilación)
  • Herramientas de implementación integradas
  • Soporte de biblioteca
  • Apoyo comunitario y lugares de reunión
  • Bibliotecas estándar ricas
  • Buena sintaxis
  • Leer eval print loop
  • Soporte para programación reflexiva

¡Buenas respuestas! La respuesta de A.Rex en particular es muy detallada e instructiva. Como señala, C ++ considera los operadores de conversión de tipos proporcionados por el usuario al compilar lhs = func(); (donde func es realmente el nombre de una estructura) . Mi solución es un poco diferente, no mejor, solo diferente (aunque se basa en la misma idea básica).

Mientras que tenía quería escribir ...

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

Terminé con una solución que usa una estructura parametrizada (con T = el tipo de retorno):

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

Un beneficio de esta solución es que cualquier código que incluya estas definiciones de plantilla puede agregar más especializaciones para más tipos. También puede hacer especializaciones parciales de la estructura según sea necesario. Por ejemplo, si desea un manejo especial para los tipos de puntero:

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

Como negativo, no puedes escribir int x = func(); con mi solución. Tienes que escribir int x = func<int>();. Debe decir explícitamente cuál es el tipo de retorno, en lugar de pedirle al compilador que lo analice mirando los operadores de conversión de tipo. Yo diría que & Quot; my & Quot; Tanto la solución como A.Rex pertenecen a un pareto-óptimo frente de formas de abordar este dilema de C ++ :)

si desea sobrecargar métodos con diferentes tipos de retorno, simplemente agregue un parámetro ficticio con valor predeterminado para permitir la ejecución de sobrecarga, pero no olvide que el tipo de parámetro debe ser diferente, por lo que la lógica de sobrecarga funciona a continuación es, por ejemplo, en Delphi:

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

úsalo así

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;

Como ya se mostró, las llamadas ambiguas de una función que difiere solo por tipo de retorno introducen ambigüedad. La ambigüedad induce código defectuoso. Se debe evitar el código defectuoso.

La complejidad impulsada por el intento de ambigüedad muestra que este no es un buen truco. Aparte de un ejercicio intelectual, ¿por qué no utilizar procedimientos con parámetros de referencia?

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

esta característica de sobrecarga no es difícil de administrar, si la miras de una manera ligeramente diferente. considere lo siguiente,

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

si un idioma devuelve la sobrecarga, permitiría la sobrecarga de parámetros, pero no las duplicaciones. esto resolvería el problema de:

main (){
f(x)
}

porque solo hay una f (int elección) para elegir.

En .NET, a veces usamos un parámetro para indicar la salida deseada de un resultado genérico, y luego realizamos una conversión para obtener lo que esperamos.

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

Quizás este ejemplo también podría ayudar:

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

Para el registro, Octave permite un resultado diferente según el elemento de retorno es escalar frente a 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 también Descomposición de valor singular .

Esto es ligeramente diferente para C++;No sé si sería considerado una sobrecarga por el tipo de retorno directamente.Es más de una plantilla de especialización que actúa en la forma de.

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

Este ejemplo no es exactamente el uso de la función de la resolución de sobrecarga por tipo de retorno, sin embargo, c++ no de la clase de objeto es el uso de la plantilla de especialización para simular la función de la resolución de sobrecarga por tipo de retorno con un privado método estático.

Cada uno de los convertToType las funciones se llama a la función de la plantilla stringToValue() y si nos fijamos en los detalles de la implementación o el algoritmo de esta plantilla de función es llamada getValue<T>( param, param ) y es la devolución de un tipo de T y almacenarla en un T* que se pasa en el stringToValue() plantilla de función como uno de sus parámetros.

Algo como esto;C++, en realidad no tiene un mecanismo para hacer que la función de la sobrecarga de la resolución por tipo de retorno.Puede haber otras estructuras o mecanismos con los que no soy consciente de que podría simular la resolución por tipo de retorno.

Creo que este es un GAP en la definición moderna de C ++ & # 8230; por qué?

int func();
double func();

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

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

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

¿Por qué un compilador de C ++ no puede arrojar un error en el ejemplo " 3 " y acepte el código en el ejemplo " 1 + 2 " ??

La mayoría de los idiomas estáticos ahora también admiten genéricos, lo que resolvería su problema. Como se indicó anteriormente, sin tener diferencias de parámetros, no hay forma de saber a cuál llamar. Entonces, si quieres hacer esto, solo usa genéricos y llámalo por día.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top