Contrairement à ce que disent les autres, la surcharge par type de retour est possible et se fait par certains langages modernes. L'objection habituelle est qu'en code comme
int func();
string func();
int main() { func(); }
vous ne pouvez pas dire qui func()
est appelé. Cela peut être résolu de plusieurs manières:
- Avoir une méthode prévisible pour déterminer quelle fonction est appelée dans une telle situation.
- Chaque fois qu'une telle situation se produit, c'est une erreur de compilation. Cependant, avoir une syntaxe qui permet au programmeur de lever l'ambiguïté, par exemple
int main() { (string)func(); }
.
- N'ayez pas 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 d'appeler la fonction en premier lieu.
Deux des langues que j'utilise régulièrement ( ab ) utilisent la surcharge par type de retour: Perl et Haskell . Permettez-moi de décrire ce qu'ils font.
En Perl , il existe une distinction fondamentale entre le contexte scalaire et le contexte de liste (et d'autres, mais nous prétendons qu'il y en a deux). Chaque fonction intégrée dans Perl peut faire des choses différentes selon le contexte dans lequel elle est appelée. Par exemple, l' join
opérateur force le contexte de liste (sur la chose à joindre) tandis que l' scalar
opérateur force le contexte scalaire, alors comparez:
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 en Perl fait quelque chose dans un contexte scalaire et quelque chose dans un contexte de liste, et ils peuvent être différents, comme illustré. (Ce n'est pas seulement pour des opérateurs aléatoires comme localtime
. Si vous utilisez un tableau @a
dans un contexte de liste, il renvoie le tableau, tandis que dans un contexte scalaire, il retourne le nombre d'éléments. Ainsi, par exemple, print @a
imprime les éléments, tout en print 0+@a
imprime la taille. ) De plus, chaque opérateur peut forcer un contexte, par exemple l'addition +
force le contexte scalaire. Chaque entrée dans les man perlfunc
documents cela. Par exemple, voici une partie de l'entrée pour glob EXPR
:
Dans un contexte de liste, renvoie une liste (éventuellement vide) d'extensions de nom de fichier sur la valeur de EXPR
ce que /bin/csh
ferait le shell Unix standard . Dans un contexte scalaire, glob itère à travers de telles extensions de nom de fichier, retournant undef lorsque la liste est épuisée.
Maintenant, quelle est la relation entre la liste et le contexte scalaire? Eh bien, man perlfunc
dit
Souvenez-vous de la règle importante suivante: Il n'y a aucune règle qui relie le comportement d'une expression dans un contexte de liste à son comportement dans un contexte scalaire, ou vice versa. Cela pourrait faire deux choses totalement différentes. Chaque opérateur et fonction décide du type de valeur qu'il serait le plus approprié de renvoyer dans un contexte scalaire. Certains opérateurs renvoient la longueur de la liste qui aurait été retournée dans le contexte de la 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 décompte d'opérations réussies. En général, ils font ce que vous voulez, sauf si vous voulez de la cohérence.
ce n'est donc pas une simple question d'avoir une seule fonction, puis vous effectuez une conversion simple à la fin. En fait, j'ai choisi l' localtime
exemple pour cette raison.
Ce ne sont pas seulement les fonctions intégrées qui ont ce comportement. Tout utilisateur peut définir une telle fonction à l'aide de wantarray
, ce qui vous permet de faire la distinction entre le contexte liste, scalaire et vide. Ainsi, par exemple, vous pouvez décider de ne rien faire si vous êtes appelé dans un contexte nul.
Maintenant, vous pouvez vous plaindre que ce n'est pas une surcharge réelle par valeur de retour car vous n'avez qu'une seule fonction, qui est informée du contexte dans lequel elle est appelée, puis agit sur ces informations. Cependant, c'est clairement équivalent (et analogue à la façon dont Perl ne permet pas la surcharge habituelle littéralement, 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; il l'appelle juste. Il suffit 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 veux dire fonction. Ce n'est pas crucial pour cette discussion.)
Haskell adopte l'autre approche, à savoir de ne pas avoir d'effets secondaires. Il possède é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? Eh bien, le type readLn
est readLn :: Read a => IO a
. Cela signifie que pour tout type qui peut être Read
(formellement, chaque type qui est une instance de la Read
classe de type), il 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 qu'il sqrt
ne peut accepter que des nombres à virgule flottante comme 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 en premier lieu. Cependant, si je fais utiliser la valeur de retour, alors Haskell se plaindra qu'il ne peut pas 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 valeur de retour est possible et est effectuée, ce qui répond en partie à votre question.
L'autre partie de votre question est pourquoi plus de langues ne le font pas. Je laisserai les autres répondre à cela. Cependant, quelques commentaires: la raison principale est probablement que l'opportunité de confusion est vraiment plus grande ici que dans la surcharge par type d'argument. Vous pouvez également consulter les justifications des différentes langues:
Ada : "Il peut sembler que la règle de résolution de surcharge la plus simple consiste à tout utiliser - 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 n'est pas utile. Elle nécessite le lecteur humain pour analyser des morceaux de texte arbitrairement grands et pour faire des inférences arbitrairement complexes (comme (g) ci-dessus). Nous pensons qu'une meilleure règle est celle qui rend explicite la tâche qu'un lecteur humain ou un compilateur doit effectuer, et qui rend cette tâche aussi naturel que possible pour le lecteur humain. "
C ++ (sous-section 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 est de garder 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 a été appelée. "(Notez, à titre de comparaison, que dans Haskell, il n'y a pas de conversions implicites .)
Java ( Java Language Specification 9.4.1 ): "L'une des méthodes héritées doit être substituable par type de retour pour chaque autre méthode héritée, sinon une erreur de compilation se produit." (Oui, je sais que cela ne donne aucune justification. Je suis sûr que la justification est donnée par Gosling dans "Java Programming Language". Peut-être que quelqu'un en a une copie? Je parie que c'est le "principe de la moindre surprise" en substance. Cependant, fait amusant sur Java: la JVM permet la surcharge par valeur de retour! Ceci est utilisé, par exemple, dans Scala , et est également accessible directement via Java en jouant avec des internes.
PS. Enfin, il est possible de surcharger par valeur de retour 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
}