web-dev-qa-db-fra.com

Surcharge de fonction par type de retour?

Pourquoi les langages traditionnels typés statiquement ne prennent-ils pas en charge la surcharge de fonctions/méthodes par type de retour? Je ne peux pas penser à ceux qui font. Cela ne semble pas moins utile ou raisonnable que de supporter une surcharge par type de paramètre. Comment se fait-il qu'il soit moins populaire?

245
dsimcha

Contrairement à ce que d’autres disent, surcharger par type de retour est possible et est fait par certaines langues modernes. L’objection habituelle est que dans le code comme

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

vous ne pouvez pas dire quel func() est appelé. Cela peut être résolu de plusieurs manières:

  1. Ayez une méthode prévisible pour déterminer quelle fonction est appelée dans une telle situation.
  2. Chaque fois qu'une telle situation se produit, c'est une erreur de compilation. Cependant, utilisez une syntaxe qui permet au programmeur de ne pas être ambiguë, par exemple. int main() { (string)func(); }.
  3. Ne pas avoir d'effets secondaires Si vous n'avez pas d'effets secondaires et que vous n'utilisez jamais la valeur de retour d'une fonction, le compilateur peut éviter de faire appel à la fonction en premier lieu.

Deux des langues que j’utilise régulièrement ( ab ) par type de retour: Perl et ) Haskell . Permettez-moi de décrire ce qu'ils font.

En Perl , il existe une distinction fondamentale entre scalar et list context ( et d’autres, mais nous prétendons qu’il y en a deux) Chaque fonction intégrée à Perl peut faire différentes choses selon le contexte dans lequel elle est appelée. Par exemple, l'opérateur join force le contexte de liste (sur l'élément à joindre) tandis que l'opérateur scalar force le contexte scalaire. Comparez donc:

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.

Chaque opérateur Perl fait quelque chose dans un contexte scalaire et dans une liste, et ils peuvent être différents, comme illustré. (Ce n'est pas juste pour des opérateurs aléatoires comme localtime. Si vous utilisez un tableau @a Dans un contexte de liste, il retourne le tableau, alors que dans un contexte scalaire, il retourne le nombre d'éléments. par exemple print @a imprime les éléments, tandis que print 0+@a imprime la taille.) En outre, chaque opérateur peut forcer un contexte, par exemple addition + force le contexte scalaire. Chaque entrée dans man perlfunc Documente cela. Par exemple, voici une partie de l'entrée pour glob EXPR:

Dans le contexte de liste, retourne une liste (éventuellement vide) des extensions de nom de fichier sur la valeur de EXPR, comme le ferait un shell Unix standard /bin/csh. Dans un contexte scalaire, glob parcourt ces extensions de nom de fichier, renvoyant undef lorsque la liste est épuisée.

Maintenant, quelle est la relation entre la liste et le contexte scalaire? Eh bien, man perlfunc Dit

N'oubliez pas la règle importante suivante: Aucune règle ne relie le comportement d'une expression dans un contexte de liste à son comportement dans un contexte scalaire, ou inversement. Cela pourrait faire deux choses totalement différentes. Chaque opérateur et chaque fonction décident du type de valeur à renvoyer dans un contexte scalaire. Certains opérateurs renvoient la longueur de la liste qui aurait été renvoyée dans un contexte de liste. Certains opérateurs renvoient la première valeur de la liste. Certains opérateurs renvoient la dernière valeur de la liste. Certains opérateurs renvoient un nombre d'opérations réussies. En général, ils font ce que vous voulez, sauf si vous voulez de la cohérence.

il n’est donc pas simple d’avoir une seule fonction, puis vous effectuez une conversion simple à la fin. En fait, j'ai choisi l'exemple localtime pour cette raison.

Ce n'est pas seulement les composants intégrés qui ont ce comportement. Tout utilisateur peut définir une telle fonction en utilisant wantarray, ce qui vous permet de faire la distinction entre les contextes list, scalar et void. Ainsi, par exemple, vous pouvez décider de ne rien faire si vous êtes appelé dans un contexte vide.

Vous pouvez maintenant vous plaindre que ce n'est pas true surcharger par la valeur renvoyée, car vous n'avez qu'une seule fonction, qui indique le contexte dans lequel elle est appelée et qui agit ensuite sur cette information. Cependant, ceci est clairement équivalent (et analogue à la façon dont Perl ne permet pas littéralement la surcharge habituelle, mais une fonction peut simplement examiner ses arguments). De plus, cela résout bien la situation ambiguë mentionnée au début de cette réponse. Perl ne se plaint pas de ne pas savoir quelle méthode appeler. ça l'appelle. Tout ce qu'il a à faire est de déterminer dans quel contexte la fonction a été appelée, ce qui est toujours possible:

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"

(Remarque: je peux parfois dire opérateur Perl quand je parle de fonction. Ce n'est pas crucial pour cette discussion.)

Haskell adopte l’autre approche, à savoir ne pas avoir d’effets secondaires. Il a également un système de type fort, et vous pouvez donc écrire du code comme suit:

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

Ce code lit un nombre à virgule flottante à partir de l'entrée standard et imprime sa racine carrée. Mais qu'est-ce qui est surprenant à ce sujet? Eh bien, le type de readLn est readLn :: Read a => IO a. Cela signifie que pour tout type pouvant être Read (officiellement, tout type constituant une instance de la classe de types Read,), readLn peut le lire. Comment Haskell a-t-il su que je voulais lire un nombre à virgule flottante? Eh bien, le type de sqrt est sqrt :: Floating a => a -> a, Ce qui signifie essentiellement que sqrt ne peut accepter que les nombres à virgule flottante en tant qu'entrées, et Haskell a donc déduit ce que je voulais.

Que se passe-t-il lorsque Haskell ne peut pas déduire ce que je veux? Eh bien, il y a quelques possibilités. Si je n'utilise pas du tout la valeur de retour, Haskell n'appellera tout simplement pas la fonction. Cependant, si je do utilise la valeur de retour, Haskell se plaindra de ne pas pouvoir déduire le type:

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

Je peux résoudre l'ambiguïté en spécifiant le type que je veux:

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

Quoi qu'il en soit, toute cette discussion signifie que la surcharge par la valeur renvoyée est possible et est effectuée, ce qui répond en partie à votre question.

L’autre partie de votre question est de savoir pourquoi plus de langues ne le font pas. Je laisserai les autres répondre à cela. Quelques remarques cependant: la raison principale est probablement que la possibilité de confusion est vraiment plus grande ici que dans une surcharge par type d’argument. Vous pouvez également consulter les logiques des langues individuelles:

Ada : "Il peut sembler que la règle de résolution de surcharge la plus simple consiste à utiliser tout - toutes les informations d'un contexte aussi large que possible - pour résoudre la référence surchargée. Cette règle peut être simple, mais elle ne l'est pas. Le lecteur humain doit numériser des morceaux de texte arbitrairement volumineux et faire des déductions arbitrairement complexes (telles que (g) ci-dessus). Nous pensons qu'une meilleure règle est celle qui rend explicite la tâche qu'un lecteur ou un compilateur humain doit effectuer, et cela rend cette tâche aussi naturelle que possible pour le lecteur humain ".

C++ (paragraphe 7.4.1 du "Langage de programmation C++" de Bjarne Stroustrup): "Les types de retour ne sont pas pris en compte dans la résolution de surcharge. La raison en est de conserver la résolution pour un opérateur individuel ou un appel de fonction indépendant du contexte. Considérez:

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 le type de retour était pris en compte, il ne serait plus possible de regarder un appel de sqrt() isolément et de déterminer quelle fonction était appelée. "(Remarque: pour comparaison, il n'y a pas de code dans Haskell.) implicite conversions.)

Java ( Java Language Specification 9.4.1 ): "L'une des méthodes héritées doit être un type de résultat substituable par rapport à toute autre méthode héritée; sinon, une erreur de compilation survient." (Oui, je sais que cela ne donne pas de justification. Je suis sûr que Gosling explique ce principe dans "the Java Langage de programmation"). Peut-être que quelqu'un en a une copie? Je parie que c'est le "principe de moindre surprise" en substance.) Cependant, fait amusant à propos de Java: la machine virtuelle Java permet surcharge par valeur de retour! Ceci est utilisé, par exemple, dans Scala , et peut être consulté directement via Java également en jouant avec les éléments internes.

PS Pour finir, il est possible de surcharger une valeur renvoyée en C++ avec une astuce. Témoin:

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
}
513
A. Rex

Si les fonctions étaient surchargées par le type de retour et que vous aviez ces deux surcharges

int func();
string func();

il est impossible pour le compilateur de déterminer laquelle de ces deux fonctions appeler en voyant un appel comme celui-ci

void main() 
{
    func();
}

Pour cette raison, les concepteurs de langage interdisent souvent la surcharge de valeurs renvoyées.

Cependant, certaines langues (telles que MSIL) do autorisent la surcharge par type de retour. Bien sûr, ils sont également confrontés à la difficulté ci-dessus, mais ils ont des solutions de contournement pour lesquelles vous devrez consulter leur documentation.

35
Frederick The Fool

Dans un tel langage, comment résoudriez-vous les problèmes suivants:

f(g(x))

si f avait des surcharges void f(int) et void f(string) et g avait des surcharges int g(int) et string g(int)? Vous auriez besoin d'une sorte d'homonymie.

Je pense que les situations où vous pourriez en avoir besoin seraient mieux servies en choisissant un nouveau nom pour la fonction.

27
Greg Hewgill

Pour voler un C++ spécifique réponse d'une autre question très similaire (dupe?):


Les types de retour de fonction n'interviennent pas dans la résolution de surcharge simplement parce que Stroustrup (avec des contributions d'autres architectes C++) supposait que la résolution de surcharge soit indépendante du contexte. Voir 7.4.1 - "Surcharge et type de retour" dans "Langage de programmation C++, troisième édition".

La raison en est de garder la résolution pour un opérateur individuel ou un appel de fonction indépendant du contexte.

Ils voulaient que cela se base uniquement sur la manière dont la surcharge était appelée - et non sur la manière dont le résultat était utilisé (s'il était utilisé du tout). En effet, de nombreuses fonctions sont appelées sans utiliser le résultat ou celui-ci serait utilisé dans le cadre d'une expression plus grande. Un facteur qui, j'en suis sûr, est entré en jeu quand ils ont décidé que ceci était que si le type de retour faisait partie de la résolution, il y aurait de nombreux appels à des fonctions surchargées qui devraient être résolus avec des règles complexes ou qui auraient besoin du compilateur. une erreur que l'appel était ambigu.

Et, Dieu le sait, la résolution de la surcharge C++ est assez complexe en l’état actuel ...

18
Michael Burr

En haskell, c'est possible même s'il n'y a pas de surcharge de fonctions. Haskell utilise des classes de types. Dans un programme, vous pouvez voir:

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 surcharge de fonction elle-même n'est pas si populaire. La plupart des langages que j'ai vus sont: C++, peut-être Java et/ou C #. Dans tous les langages dynamiques, c’est un raccourci pour:

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

Par conséquent, il n'y a pas beaucoup de point là. La plupart des gens ne sont pas intéressés par le fait que la langue puisse vous aider à supprimer une seule ligne, quel que soit le lieu où vous l'utilisez.

La mise en correspondance des motifs est un peu similaire à la surcharge de fonctions et, parfois, fonctionne de la même manière. Ce n'est pas courant, car il n'est utile que pour quelques programmes et difficile à mettre en œuvre dans la plupart des langues.

Vous voyez qu'il existe une infinité d'autres fonctionnalités plus faciles à implémenter dans le langage, notamment:

  • Saisie dynamique
  • Support interne pour les listes, les dictionnaires et les chaînes Unicode
  • Optimisations (JIT, inférences de types, compilation)
  • Outils de déploiement intégrés
  • Soutien de la bibliothèque
  • Soutien communautaire et lieux de rassemblement
  • Bibliothèques standard riches
  • Bonne syntaxe
  • Lire la boucle d'impression eval
  • Prise en charge de la programmation réflexive
5
Cheery

Bonnes réponses! La réponse de A.Rex est en particulier très détaillée et instructive. Comme il le fait remarquer, C++ considère les opérateurs de conversion de type fournis par l'utilisateur lors de la compilation de lhs = func(); (où func est vraiment le nom d’une structure) . Ma solution de contournement est un peu différente - pas meilleure, mais différente (même si elle repose sur la même idée de base).

Alors que j'avais voulu écrire ...

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

Je me suis retrouvé avec une solution qui utilise une structure paramétrée (avec T = le type de retour):

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 avantage de cette solution est que tout code contenant ces définitions de modèle peut ajouter plus de spécialisations pour plus de types. Vous pouvez aussi faire des spécialisations partielles de la structure selon vos besoins. Par exemple, si vous vouliez un traitement spécial pour les types de pointeur:

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

En négatif, vous ne pouvez pas écrire int x = func(); avec ma solution. Vous devez écrire int x = func<int>();. Vous devez indiquer explicitement le type de retour plutôt que de demander au compilateur de le vérifier en regardant les opérateurs de conversion de type. Je dirais que "ma" solution et celles de A.Rex appartiennent à un front de Pareto-optimal des moyens de résoudre ce dilemme C++ :)

3
Adam McKee

si vous souhaitez surcharger des méthodes avec différents types de retour, ajoutez simplement un paramètre fictif avec la valeur par défaut pour autoriser l'exécution de la surcharge, mais n'oubliez pas que le type de paramètre doit être différent pour que la logique de surcharge fonctionne ensuite. par exemple sur Delphi:

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

l'utiliser comme ça

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;
1
ZORRO_BLANCO

Comme déjà montré, les appels ambigus d'une fonction qui diffère seulement par le type de retour introduit une ambiguïté. L'ambiguïté induit un code défectueux. Le code défectueux doit être évité.

La complexité motivée par la tentative d'ambiguïté montre que ce n'est pas un bon bidouillage. Outre un exercice intellectuel, pourquoi ne pas utiliser des procédures avec des paramètres de référence.

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

Pour mémoire, Octave permet d’obtenir des résultats différents selon que l’élément de retour est 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. également décomposition en valeurs singulières .

0
YvesgereY

Celui-ci est légèrement différent pour C++; Je ne sais pas si cela serait considéré comme une surcharge par type de retour directement. Il s’agit plus d’une spécialisation de modèles qui agit à la manière de.

til.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

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

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

Cet exemple n'utilise pas exactement la résolution de surcharge de fonction par type de retour. Toutefois, cette classe non-objet c ++ utilise la spécialisation de modèle pour simuler la résolution de surcharge de fonction par type de retour avec une méthode statique privée.

Chacune des fonctions convertToType appelle le modèle de fonction stringToValue() et si vous examinez les détails de mise en œuvre ou l'algorithme de ce modèle de fonction, il appelle getValue<T>( param, param ) et renvoie sauvegardez un type T et enregistrez-le dans un T* qui est passé dans le modèle de fonction stringToValue() en tant que paramètre.

Autre que quelque chose comme ça; C++ n'a pas vraiment de mécanisme pour avoir une résolution de surcharge de fonction par type de retour. Je ne suis pas au courant de l'existence d'autres structures ou mécanismes susceptibles de simuler une résolution par type de retour.

0
Francis Cugler

Dans .NET, nous utilisons parfois un paramètre pour indiquer la sortie souhaitée d’un résultat générique, puis nous effectuons une conversion pour obtenir ce que nous attendons.

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

Peut-être que cet exemple pourrait aussi aider:

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

cette fonctionnalité de surcharge n'est pas difficile à gérer, si vous la regardez d'une manière légèrement différente. considérer ce qui suit,

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

si une langue renvoyait une surcharge, elle autoriserait la surcharge de paramètres, mais pas les duplications. cela résoudrait le problème de:

main (){
f(x)
}

car il n’ya qu’un seul f (int choix) à choisir.

0
paulon0n