Surcharge de la fonction par type de retour?
Pourquoi ne pas en plus grand des langages statiquement typés fonction de support/surcharge de la méthode par le type de retour? Je ne pense pas du tout que faire. Il ne semble pas moins utile ou raisonnable que de supporter une surcharge par type de paramètre. Comment se fait il est beaucoup moins populaire?
14 réponses
contrairement à ce que d'autres disent, la surcharge 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(); }
on ne sait pas quel est le nom func()
. Cela peut être résolu de plusieurs façons:
- prévisible méthode 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 désambiguer, par exemple
int main() { (string)func(); }
. - N'ont pas d'effets secondaires. Si vous n'avez pas d'effets secondaires et vous ne jamais utiliser la valeur de retour d'une fonction, le compilateur peut éviter l'appel de la fonction en premier lieu.
deux des langues I régulièrement ( ab ) utiliser surcharge par type de retour: Perl et Haskell . Permettez-moi de décrire ce qu'ils font.
Dans Perl , il y a une distinction fondamentale entre scalaire et liste contexte (et d'autres, mais nous allons faire semblant il y a deux). Chaque fonction intégrée dans Perl peut faire différentes choses selon le contexte dans lequel il est appelé. Par exemple, l'opérateur join
force le contexte de la liste (sur la chose à 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 de 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 les opérateurs aléatoires comme localtime
. Si vous utilisez un tableau @a
dans le contexte list, il renvoie le tableau, alors que dans le contexte scalaire, il renvoie le nombre d'éléments. Ainsi, 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 l'ajout +
forces contexte scalaire. Chaque entrée dans man perlfunc
le documente. Par exemple, voici une partie de l'entrée glob EXPR
:
Dans un contexte de liste, retourne une (peut-être empty) liste des extensions de noms de fichiers sur valeur de
EXPR
comme la norme Unix shell/bin/csh
ferait l'affaire. Dans contexte scalaire, glob itère à travers telles extensions de noms de fichiers, retournant le fnud lorsque la liste est épuisée.
Quelle est la relation entre list et le contexte scalaire? man perlfunc
dit
rappelez-vous la règle importante suivante: Il n'y a pas de règle qui se rapporte à la comportement d'une expression dans list contexte de son comportement dans scalaire contexte, ou vice versa. Il pourrait faire deux choses totalement différentes. Chacun de l'opérateur et de la fonction qui décide sorte de valeur, il serait plus approprié pour retourner dans scalaire cadre. Certains opérateurs renvoient la la longueur de la liste qui aurait été retourné dans un contexte de liste. Quelque les opérateurs renvoient la première valeur en liste. Certains opérateurs renvoient la la dernière valeur dans la liste. Quelque les opérateurs retournent un nombre de succès opérations. En général, ils faire ce que tu veux, sauf si tu veux de la cohérence.
donc ce n'est pas une simple question d'avoir une seule fonction, et puis vous faites la conversion simple à la fin. En fait, j'ai choisi l'exemple localtime
pour cette raison.
ce n'est pas seulement les built-In qui ont ce comportement. N'importe quel utilisateur peut définir une telle fonction en utilisant wantarray
, ce qui vous permet de distinguer entre list, scalar et void context. Ainsi, par exemple, vous pouvez décider de ne rien faire si vous êtes appelé dans le contexte vide.
Maintenant, vous pouvez vous plaindre que ce n'est pas vrai surcharger par la valeur de retour parce que vous avez seulement une fonction, qui est dit le contexte dans lequel il est appelé et puis agit 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). En outre, bien résout le situation ambiguë mentionnée au début de la présente réponse. Perl ne se plaint pas qu'il ne sait pas quelle méthode appeler; il appelle juste. Tout ce qu'il a à faire est de comprendre 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"
(Note: je peux parfois dire opérateur Perl quand je veux dire fonction. Ce n'est pas crucial pour cette discussion.)
Haskell prend l'autre approche, à savoir pas d'effets secondaires. Il a également un système de type fort, et donc vous pouvez écrire le 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 de virgule flottante de l'entrée standard, et imprime sa racine carrée. Mais ce qui est surprenant à propos de cela? Le type de readLn
est readLn :: Read a => IO a
. Cela signifie que pour tout type qui peut être Read
(formellement, tout type qui est une instance de la classe de type Read
), readLn
peut le lire. Comment Haskell a su que je voulais lire un nombre flottant? Eh bien , le type de sqrt
est sqrt :: Floating a => a -> a
, ce qui signifie essentiellement que sqrt
ne peut accepter les nombres à virgule flottante comme entrées, et donc Haskell inféré ce que je voulais.
que se passe-t-il quand Haskell ne peut pas inférer ce que je veux? Eh bien, il y a quelques possibilités. Si je n'utilise pas la valeur de retour, Haskell n'est tout simplement pas appeler la fonction en premier lieu. Toutefois, si je 1519600920" do utiliser la valeur de retour, puis Haskell se plaindra qu'il ne peut pas inférer 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, ce que toute cette discussion signifie est que la surcharge par la valeur de retour est possible et est fait, ce qui répond à une partie de votre question.
l'autre partie de votre question est pourquoi plus de langues ne le font pas. Je vais laisser les autres répondre à ça. Cependant, quelques commentaires: la raison principale est probablement que la possibilité 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 pourrait sembler que la règle la plus simple de résolution de surcharge est d'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 il est pas utile. Il exige du lecteur humain qu'il numérise arbitrairement de grands morceaux de texte et qu'il fasse des inférences arbitrairement complexes (comme (g) ci-dessus). Nous croyons qu'une meilleure règle est celle qui rend explicite la tâche d'un lecteur humain ou un compilateur doit effectuer, et qui rend cette tâche comme naturel pour le lecteur humain que possible."
C++ (sous-section 7.4.1 du "langage de programmation C++" de Bjarne Stroustrup): "les types de retour ne sont pas considérés en résolution de surcharge. Le la raison est de garder la résolution pour un opérateur ou une fonction d'appel indépendant du contexte. Considérons:
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()
seul et de déterminer quelle fonction a été appelée."(Note, à titre de comparaison, qu'il n'y a pas de conversions "implicites à Haskell.)
Java ( Java Language Specification 9.4.1 ): "L'une des méthodes héritées doit être de type return substituable à toute autre méthode héritée; sinon, une erreur de compilation se produit."(Oui, je sais que cela ne donne pas de raison. Je suis sûr que le raisonnement est donné par Gosling dans "le langage de programmation Java". Peut-être que quelqu'un en a une copie? Je parie que c'est le" principe de la moindre surprise " en essence.) Cependant, fait amusant sur Java: la JVM permet de surcharger par la valeur de retour! Ce est utilisé pour exemple, dans Scala , et peut être accédé directement par Java ainsi en jouant autour avec les internes.
PS. Comme note finale, il est en fait possible de surcharger par la valeur de retour en C++ avec un truc. 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
}
si les fonctions étaient surchargées par le type de retour et que vous aviez ces deux surcharges
int func();
string func();
il n'y a pas moyen que le compilateur puisse trouver laquelle de ces deux fonctions appeler en voyant un appel comme celui-ci
void main()
{
func();
}
pour cette raison, les concepteurs de langue interdisent souvent la surcharge de la valeur de retour.
certaines langues (comme MSIL), cependant, do permettent la surcharge par type de retour. Eux aussi font face à la difficulté ci-dessus bien sûr, mais ils ont des solutions de rechange, pour lesquelles vous devrez consulter leur documentation.
dans un tel langage, comment résoudriez-vous ce qui suit:
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 de disambiguator.
je pense que les situations où vous pourriez avoir besoin de ceci seraient mieux servies en choisissant un nouveau nom pour la fonction.
Pour voler un C++ spécifique réponse à partir d'une autre question très semblable (dupe?):
les types de retour de fonction ne jouent pas dans la résolution de surcharge simplement parce que Stroustrup (je suppose avec la contribution d'autres architectes C++) voulait que la résolution de surcharge soit "indépendante du contexte". Voir 7.4.1 - " Overloading and Return Type "dans le"C++ Programming Language, Third Edition".
la raison est de garder la résolution pour un opérateur ou une fonction d'appel individuel indépendant du contexte.
Ils voulaient qu'il soit basé uniquement sur la façon dont la surcharge a été appelé - pas comment le résultat a été utilisée (si elle a été utilisée). En effet, de nombreuses fonctions sont appelées sans utiliser le résultat ou le résultat serait utilisé comme partie d'une plus grande expression. Un facteur que je suis sûr est entré en jeu quand ils ont décidé cela é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 devraient amener le compilateur à lancer une erreur que l'appel était ambigu.
Et, Dieu sait, la résolution de surcharge C++ est assez complexe en l'état...
dans haskell c'est possible même si elle n'a pas de surcharge de fonction. Haskell utilise des classes de type. 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 la fonction elle-même n'est pas si populaire. La plupart des langues que j'ai vues avec Sont C++, peut-être java et/ou C#. Dans toutes les langues 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()
il n'y a donc pas grand chose à faire. La plupart des gens ne sont pas intéressés si la langue peut vous aider à laisser tomber un seule ligne par l'endroit où vous l'utilisez.
correspondance de modèle est quelque peu similaire à la surcharge de la fonction, et je suppose que parfois fonctionnent de la même façon. Ce n'est pas courant cependant parce qu'il est utile seulement pour quelques programmes et est difficile à mettre en œuvre sur la plupart des langues.
vous voyez qu'il y a infiniment d'autres fonctionnalités plus faciles à implémenter dans le langage, y compris:
- typage Dynamique
- support interne pour listes, dictionnaires et chaînes unicode
- Optimisations (JIT, type d'inférence, de la compilation)
- Intégré les outils de déploiement d'1519140920"
- support de bibliothèque
- soutien communautaire et lieux de rassemblement
- Riches bibliothèques standard
- bonne syntaxe
- lire boucle d'impression eval
- le Soutien à l' réfléchissant programmation
bonnes réponses! La réponse D'A. Rex, en particulier, est très détaillée et instructive. Comme il le souligne, C++ fait tenir compte des opérateurs de conversion de type fournis par l'utilisateur lors de la compilation lhs = func();
(où func est vraiment le nom d'une structure) . Mon contournement est un peu différent - pas mieux, juste différent (bien qu'il soit basé sur la même idée de base).
alors que j'avais voulu pour é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++!
j'ai fini 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 n'importe quel code qui inclut 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 au besoin. Par exemple, si vous souhaitez une manipulation spéciale pour les types de pointeur:
template <typename T>
struct func<T*>
{
operator T*()
{ <<special handling for T*>> }
};
comme un négatif, vous Je ne peux pas écrire int x = func();
avec ma solution. Vous devez écrire int x = func<int>();
. Vous devez dire explicitement ce qu'est le type de retour, plutôt que de demander au compilateur de le repérer en regardant les opérateurs de conversion de type. Je dirais que "ma" solution et celle de A. Rex appartiennent toutes deux à un pareto-optimal front de façons de résoudre ce dilemme c++:)
comme déjà montré - les appels Ambigus d'une fonction qui ne diffère que par le type de retour introduisent l'ambiguïté. L'ambiguïté induit un code défectueux. Le code défectueux doit être évité.
la complexité provoquée par la tentative d'ambiguïté montre qu'il ne s'agit pas d'un bon piratage. En dehors d'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)
cette surcharge fonctionnalité n'est pas difficile à gérer, si vous le regardez d'une manière légèrement différente. considérez ce qui suit,
public Integer | String f(int choice){
if(choice==1){
return new string();
}else{
return new Integer();
}}
si une langue retournait la surcharge, elle autoriserait la surcharge des paramètres, mais pas les duplications. cela résoudrait le problème de:
main (){
f(x)
}
parce qu'il n'y a qu'un seul f(choix int) à choisir.
dans .NET, parfois nous utilisons un paramètre pour indiquer la sortie désirée à partir d'un résultat générique, puis nous avons fait 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 aider aussi:
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;
}
Pour le dossier Octave permet d'obtenir différents résultats selon le retour de l'élément scalaire vs tableau.
x = min ([1, 3, 0, 2, 0])
⇒ x = 0
[x, ix] = min ([1, 3, 0, 2, 0])
⇒ x = 0
ix = 3 (item index)
Cf aussi décomposition de la valeur au singulier .
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 est plus d'un modèle de spécialisation qui agit dans la manière 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;
}
cet exemple n'est pas exactement résolution de surcharge de fonction par type de retour, cependant cette Classe c++ non object 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 regardez les détails d'implémentation ou l'algorithme de ce modèle de fonction il appelle getValue<T>( param, param )
et il retourne un type T
et le stocke dans un T*
qui est passé dans le modèle de fonction stringToValue()
comme l'un de ses paramètres.
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. Il peut y avoir d'autres constructions ou mécanismes dont je ne suis pas au courant qui pourraient simuler la résolution par type de retour.
si vous voulez surcharger les méthodes avec différents types de retour, il suffit d'ajouter un paramètre fictif avec la valeur par défaut pour permettre l'exécution de la surcharge, mais n'oubliez pas que le type de paramètre doit être différent de sorte que la logique de surcharge fonctionne ensuite est un E. G on delphi:
type
myclass = class
public
function Funct1(dummy: string = EmptyStr): String; overload;
function Funct1(dummy: Integer = -1): Integer; overload;
end;
utilisez - le comme ceci
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;
la plupart des langues statiques prennent également en charge les génériques, ce qui résoudrait votre problème. Comme indiqué précédemment, sans avoir de diffs de paramètres, il n'y a pas moyen de savoir lequel appeler. Donc, si vous voulez faire cela, il suffit d'utiliser des génériques et l'appeler un jour.
je pense que c'est une lacune dans la définition c++ moderne... pourquoi ?
int func();
double func();
// example 1. → defined
int i = func();
// example 2. → defined
double d = func();
// example 3. → NOT defined. error
void main()
{
func();
}
Pourquoi un compilateur C++ ne peut pas jeter une erreur dans l'exemple "3" et accepter le code dans l'exemple "1+2"??