sobrecarga sencillos resueltos polimorfismo operadores metodos matrices funciones ejercicios ejemplos conclusion programming-languages language-design overloading method-overloading function-calls

programming-languages - sencillos - sobrecarga de operadores c++ struct



¿Sobrecarga de funciones por tipo de retorno? (14)

¿Por qué los lenguajes más tipificados estáticamente no admiten la sobrecarga de funciones / métodos por tipo de retorno? No puedo pensar en ninguno que lo haga. Parece no menos útil o razonable que soportar la sobrecarga por tipo de parámetro. ¿Cómo es que es mucho menos popular?


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 tipo provistos por el usuario cuando compila 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 había querido 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 :)

Una ventaja de esta solución es que cualquier código que incluya estas definiciones de plantillas 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 punteros:

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

Como negativo, no puede escribir int x = func(); con mi solucion 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 observando los operadores de conversión de tipos. Yo diría que "mi" solución y la A.Rex pertenecen en un frente de manera óptima para enfrentar este dilema de C ++ :)


Como ya se ha mostrado, las llamadas ambiguas de una función que difieren solo por el tipo de retorno introducen ambigüedad. La ambigüedad induce código defectuoso. El código defectuoso debe ser evitado.

La complejidad conducida por el intento de ambigüedad muestra que esto 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)


Contrariamente a lo que otros dicen, 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 decir 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 se produce una situación de este tipo, es un error en tiempo de compilación. Sin embargo, tenga una sintaxis que permita al programador desambiguar, por ejemplo, int main() { (string)func(); } 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 la función en primer lugar.

Dos de los idiomas que uso regularmente ( ab ) 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 de lista (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 de join fuerza el contexto de la lista (en la cosa que se une) mientras que el operador escalar 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 print 0+@a imprime el tamaño.) Además, cada operador puede forzar un contexto, por ejemplo, la suma + 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 una lista (posiblemente vacía) de expansiones de nombre de archivo en el valor de EXPR como lo EXPR el shell /bin/csh estándar de Unix. En el contexto escalar, glob itera a través de tales expansiones de nombre de archivo, devolviendo undef cuando se agota la lista.

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

Recuerde la siguiente regla importante: no hay ninguna regla que relacione el comportamiento de una expresión en el contexto de la lista con su comportamiento en el contexto escalar, o viceversa. Podría hacer dos cosas totalmente diferentes. Cada operador y función deciden qué tipo de valor sería más apropiado devolver en un contexto escalar. Algunos operadores devuelven la longitud de la lista que se habría devuelto en el contexto de la lista. Algunos operadores devuelven el primer valor en la lista. Algunos operadores devuelven el último valor en la lista. Algunos operadores devuelven un conteo de operaciones exitosas. En general, hacen lo que usted quiere, a menos que quiera consistencia.

así que no es una simple cuestión de tener una sola función, y luego haces una conversión simple al final. De hecho, elegí el ejemplo de localtime por esa razón.

No son solo los incorporados los que tienen este comportamiento. Cualquier usuario puede definir una función de este tipo utilizando wantarray , que le permite distinguir entre contexto de lista, escalar y vacío. Entonces, por ejemplo, puedes decidir no hacer nada si te llaman en un contexto vacío.

Ahora, puede quejarse de que esto no es una sobrecarga verdadera por valor de retorno porque solo tiene una función, que se indica en el contexto en el que se llama y luego actúa sobre esa información. Sin embargo, esto es claramente equivalente (y análogo a cómo Perl no permite literalmente la sobrecarga habitual, pero una función solo puede examinar sus argumentos). Además, resuelve muy bien la situación ambigua mencionada al comienzo de esta respuesta. Perl no se queja de que no sabe a qué método llamar; simplemente lo llama. Todo lo que tiene que hacer es averiguar en qué contexto se llamó la función, lo 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: algunas veces puedo decir operador de Perl cuando me refiero a la función. Esto no es crucial para esta discusión).

Haskell toma el otro enfoque, a saber, no tener efectos secundarios. También tiene un sistema de tipos 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 punto flotante de la entrada estándar e imprime su raíz cuadrada. Pero, ¿qué es lo sorprendente de esto? Bueno, el tipo de readLn es readLn :: Read a => IO a . Lo que esto significa es que para cualquier tipo que se pueda Read (formalmente, cada tipo que sea una instancia de la clase de tipo de Read ), readLn puede leerlo. ¿Cómo supo Haskell que quería leer un número de punto flotante? Bueno, el tipo de sqrt es sqrt :: Floating a => a -> a , lo que esencialmente significa que sqrt solo puede aceptar números de punto flotante como entradas, por lo que Haskell dedujo lo que quería.

¿Qué pasa 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 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 el valor de retorno es posible y está terminada, lo que responde parte de su pregunta.

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

Ada : "Puede parecer que la regla de resolución de sobrecarga más simple es utilizar 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 el lector humano para escanear partes de texto arbitrariamente grandes y hacer inferencias arbitrariamente complejas (como (g) arriba). Creemos que una regla mejor es aquella que hace explícita la tarea que debe realizar un lector humano o un compilador, y eso hace que esta tarea tan natural para el lector humano como sea posible ".

C ++ (subsección 7.4.1de Bjarne Stroustrup "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 para un operador individual o función independiente del contexto de la llamada. 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 tomara en cuenta el tipo de retorno, ya no sería posible ver una llamada de sqrt() de forma aislada y determinar qué función se llamó ". (Tenga en cuenta, para comparación, 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 un tipo de devolución sustituible por cualquier otro método heredado; de lo contrario, se produce un error en tiempo de compilación". (Sí, sé que esto no tiene una razón. Estoy seguro de que Gosling da la razón en "el lenguaje de programación Java". ¿Quizás alguien tiene una copia? Apuesto a que es el "principio de la menor sorpresa" en esencia. ) Sin embargo, es un dato divertido sobre Java: ¡la JVM permite la sobrecarga por valor de retorno! Esto se usa, por ejemplo, en Scala , y también se puede acceder directamente a través de Java mediante juegos internos.

PD. Como nota final, en realidad es posible sobrecargar el 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 }


Creo que esta es una brecha en la definición moderna de C ++ ... ¿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 lanzar un error en el ejemplo "3" y aceptar el código en el ejemplo "1 + 2"?


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

DO#

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

Tal vez este ejemplo podría ayudar también:

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


En haskell es posible aunque no tenga una sobrecarga de funciones. Haskell utiliza clases de tipo. En un programa podías 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í no es tan popular. La mayoría de los idiomas 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 en ello. La mayoría de la gente no está interesada en si el lenguaje puede ayudarlo a dejar una línea por donde la use.

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

Verá que hay muchas otras funciones fáciles de implementar para implementar en el lenguaje, que incluyen:

  • Tipificación dinámica
  • Soporte interno para listas, diccionarios y cadenas Unicode.
  • Optimizaciones (JIT, inferencias tipo, compilación)
  • Herramientas de implementación integradas
  • Soporte de biblioteca
  • Apoyo comunitario y lugares de reunión.
  • Bibliotecas estándar enriquecidas
  • Buena sintaxis
  • Leer el bucle de impresión eval
  • Soporte para la programación reflexiva.

En tal lenguaje, ¿cómo resolvería lo siguiente:

f(g(x))

si f tuvo sobrecargas void f(int) y void f(string) y g tuvo sobrecargas int g(int) y string g(int) ? Necesitarías algún tipo de desambiguador.

Creo que las situaciones en las que podría necesitar esto se resolverían mejor eligiendo un nuevo nombre para la función.


Esta característica de sobrecarga no es difícil de manejar, si la miras de una manera ligeramente diferente. considera lo siguiente,

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

si un idioma devolviera 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 (opción int) para elegir.


Este es ligeramente diferente para C ++; No sé si se consideraría sobrecarga por tipo de devolución directamente. Es más de una plantilla de especialización que actúa de la manera 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 utiliza exactamente la resolución de sobrecarga de función por tipo de retorno, sin embargo, esta clase de objeto no c ++ utiliza la especialización de plantilla para simular la resolución de sobrecarga de función por tipo de retorno con un método estático privado.

Cada una de las funciones convertToType está llamando a la plantilla de función stringToValue() y si observa los detalles de implementación o el algoritmo de esta plantilla de función, está llamando a getValue<T>( param, param ) y está devolviendo un tipo T y almacenándolo en una T* que se pasa a la plantilla de función stringToValue() como uno de sus parámetros.

Aparte de algo como esto; C ++ no tiene realmente un mecanismo para tener resolución de sobrecarga de funciones por tipo de retorno. Puede haber otras construcciones o mecanismos de los que no tengo conocimiento que podrían simular la resolución por tipo de devolución.


La mayoría de los lenguajes estáticos también son compatibles con los genéricos, lo que resolvería su problema. Como se dijo antes, sin tener diferencias de parámetros, no hay manera de saber a cuál llamar. Entonces, si quieres hacer esto, solo usa los genéricos y llámalo un día.


Para el registro, Octave permite diferentes resultados según que el elemento de retorno sea escalar contra una matriz.

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 la descomposición del valor singular .


Para robar una respuesta específica de C ++ de otra pregunta muy similar (¿engañosa?):

Los tipos de retorno de función no entran en juego en la resolución de sobrecarga simplemente porque Stroustrup (supongo que con la entrada de otros arquitectos de C ++) quería que la resolución de sobrecarga fuera "independiente del contexto". Consulte 7.4.1 - "Sobrecarga y tipo de retorno" de "C ++ Programming Language, Third Edition".

El motivo es mantener la resolución para un operador individual o función llamada al contexto independiente.

Querían que se basara solo en cómo se llamaba la sobrecarga, no en cómo se usaba el resultado (si se usaba). De hecho, muchas funciones se llaman sin usar el resultado o el resultado se usaría como parte de una expresión más grande. Un factor que estoy seguro que entró en juego cuando decidieron que esto era que si el tipo de retorno era parte de la resolución, habría muchas llamadas a funciones sobrecargadas que tendrían que resolverse con reglas complejas o tendrían que lanzar el compilador Un error que la llamada fue ambigua.

Y, Dios sabe, la resolución de sobrecarga de C ++ es lo suficientemente compleja como está ...


Si desea sobrecargar métodos con diferentes tipos de retorno, solo agregue un parámetro ficticio con un valor predeterminado para permitir la ejecución de la sobrecarga, pero no olvide que el tipo de parámetro debe ser diferente, de modo que la lógica de sobrecarga que se ejecute 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;

utilízalo 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;


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

int func(); string func();

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

void main() { func(); }

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

Sin embargo, algunos idiomas (como MSIL) permiten la sobrecarga por tipo de devolución. Por supuesto, ellos también enfrentan la dificultad anterior, pero tienen soluciones alternativas, por lo que tendrá que consultar su documentación.