OVH Cloud OVH Cloud

Surcharge d'opérateurs

9 réponses
Avatar
Boris Sargos
Salut,

C# n'autorise pas la surcharge des opérateurs [] et (), ce qui est très
embêtant quand on vient du C++, comme moi.

J'aimerais avoir la possibilité de créer des foncteurs (ou
classes-fonctions), ainsi que des opérateurs du genre [,].

Comment peut-on contourner le problème ?


Toute piste est bienvenue.
Merci.

9 réponses

Avatar
Ludovic SOEUR
Bonjour Boris,

Je ne suis pas d'accord avec vous, on peut surcharger [] et (). En plus, on
peut faire mieux qu'en C++ puisque l'on peut typer la surcharge. Par exemple
pour [], on peut très bien avoir un int comme en C++ mais aussi un string,
un double ou autre. Le compilateur se chargera de choisir la bonne
surcharge. De même, pour l'opérateur de cast () du c++, on retrouve la même
chose en c# avec la possibilité de rendre explicite ou implicite le
cast...ce qui offre de nombreuses possibilité.

Regardez le petit bout de code ci-joint pour connaître la syntaxe de la
surcharge des 2 opérateurs qui n'est pas trivial. Ensuite regardez la doc
pour plus d'info.

using System;
using System.Collections;

public class MonTest {
static void Main() {
MaListe ml=new MaListe();
ml.Add("tata");
ml.Add("titi");
ml.Add("toto");
ml.Add("tutu");
Console.WriteLine(ml[0]);
Console.WriteLine(ml[1]);
Console.WriteLine(ml[2]);
Console.WriteLine(ml[3]);
Console.WriteLine(ml);
}
}

public class MaListe {
ArrayList al=new ArrayList();
public MaListe() {}
public void Add(String s) {al.Add(s);}

public String this [int index] {
get {return (String)al[index];}
set {al[index]=value;}
}

public static implicit operator string(MaListe maListe) {
String result="Contenu :";
foreach(String s in maListe.al) result+=System.Environment.NewLine+s;
return result;
}
}

J'espère avoir répondu à votre question.
Cordialement,
Ludovic Soeur.


"Boris Sargos" a écrit dans le message de
news:di46s1$4ju$
Salut,

C# n'autorise pas la surcharge des opérateurs [] et (), ce qui est très
embêtant quand on vient du C++, comme moi.

J'aimerais avoir la possibilité de créer des foncteurs (ou
classes-fonctions), ainsi que des opérateurs du genre [,].

Comment peut-on contourner le problème ?


Toute piste est bienvenue.
Merci.


Avatar
Ambassadeur Kosh
Salut Boris

class BorisList
{
double this[string key,string key2] { get {...} set {...}}
}

par contre, pour le () implicite, oublie ça. le C++ est un dépotoire à
embrouilles en grande partie à cause des casts implicites, c'est pas pour en
ramener en C#. ademettons encore qu'ils soient explicit,
pourquoi ne pas écrire simplement une méthode. regarde dans String, tu
trouves ToCharArray...

class BorisList
{
IList<String> ToStringList() {...}
}

pour les foncteurs, si tu entends pointeurs de fonction, cad passer en
parametre un agent chargé de faire un boulot et qui a un contrat precis, tu
as plusieurs solutions : les delegate et les class

A+
Avatar
adebaene
Ambassadeur Kosh wrote:
Salut Boris

class BorisList
{
double this[string key,string key2] { get {...} set {...}}
}

par contre, pour le () implicite, oublie ça. le C++ est un dépotoire à
embrouilles en grande partie à cause des casts implicites, c'est pas po ur en
ramener en C#.


Mouais.... Sauf que les casts en C# ont repris la syntaxe ignoble du C,
au lieu des static_cast, dynamic_cast, etc... du C++, beaucoup plus
lisibles. C'est moisn génant vu qu'un cast foireux lève
immédiatement une exception, mais restes que chercher une opération
de cast dans un code existant à coup de "Rechercher", c'est infaisable
avec la syntaxe ().

Par ailleurs, aussi bien le C++ que le C# autorisent d'écrire des
opérateurs de conversion implicites ou explicites (en C++, via un
constructeur "explicit"), comme tu préfères, donc...

ademettons encore qu'ils soient explicit,
pourquoi ne pas écrire simplement une méthode. regarde dans String, tu
trouves ToCharArray...


Cette approche a très nettement ces limites quand tu fait de la
programmation générique à coup de templates ou, de manière plus
limitée, de génériques : Si tu veux manipuler un type "T" inconnu,
c'est beaucoup plus simple et plus naturel d'imposer comme contrainte
que T doit définir un operateur (), où être convertible
(implicitement ou explicitement) dans tel type, plutôt que d'imposer
qu'il ait une méthode "To_TrucChose" ou "Do_CeciOuCela".

Ceci-dit, avec les génériques, l'approche va être un peu différente
vu qu'il y a des contraintes énumérées formellement sur les types
autorisés pour la spécialisation du générique. Je ne sais pas
encore trop si ca va ête bénéfique ou pas... De toute façon, comme
les génériques ne permettent pas la spécialisation partielle ou
totale, ce qu'on peut faire avec en manière de programmation
générique est très limité.


pour les foncteurs, si tu entends pointeurs de fonction,


Et non! Un foncteur c'est un objet qui se comporte coimme une fonction,
donc qui surcharge (une ou plusieurs fois) l'operateur (). (en plus on
considère souvent que un foncteur doit être copiable, mais c'est unc
concept purement C++ là).

Arnaud
MVP - VC
Avatar
Ambassadeur Kosh
> Cette approche a très nettement ces limites quand tu fait de la
programmation générique à coup de templates ou, de manière plus
limitée, de génériques : Si tu veux manipuler un type "T" inconnu,
c'est beaucoup plus simple et plus naturel d'imposer comme contrainte
que T doit définir un operateur (), où être convertible
(implicitement ou explicitement) dans tel type, plutôt que d'imposer
qu'il ait une méthode "To_TrucChose" ou "Do_CeciOuCela".



hmmm. c'est typique STL ça.
maintenant si on mets un parametre supplémentaire (type ou valeur) qui fait
le boulot, ça revient au même... je crois meme que ça enrichi le truc.
bon faut voir...

chercher une opération
de cast dans un code existant à coup de "Rechercher", c'est infaisable
avec la syntaxe ().



completement. et un outils qui trouve les casts dans les sources, ça
manque...
j'ai essayé d'en faire un pour VS, mais c'est trop chaud... abandon du truc.
si y'a un courageux qui m'entend ;)

encore trop si ca va ête bénéfique ou pas... De toute façon, comme
les génériques ne permettent pas la spécialisation partielle ou
totale, ce qu'on peut faire avec en manière de programmation
générique est très limité.



disons que la façon d'exprimer les choses est différente.

si on reprend l'eternel exemple de la classe Complex<T>, les operations de
base sur les composantes Re et Im peuvent etre faites par un objet, disons
Algebra.

point de vue type, on découpe un concept autours de ces opérations +-*/, et
ça permet de les "séparer" du type manipulé. moi, j'aime bien , c'est une
bonne propriété...

point de vue efficacité, c'est sur que c'est pas le grand reve ultime pour
l'instant... meme si on gagne du temps sur les algorithmes qui utilisent nos
classes, on aime pas trop avoir ce genre de frein à main levé dans les
briques de base.

Et non! Un foncteur c'est un objet qui se comporte coimme une fonction,
donc qui surcharge (une ou plusieurs fois) l'operateur (). (en plus on
considère souvent que un foncteur doit être copiable, mais c'est unc
concept purement C++ là).



respect. je savais plus que ça portait ce nom la.
Avatar
adebaene
Ambassadeur Kosh wrote:
> Cette approche a très nettement ces limites quand tu fait de la
> programmation générique à coup de templates ou, de manière plus
> limitée, de génériques : Si tu veux manipuler un type "T" inconnu,
> c'est beaucoup plus simple et plus naturel d'imposer comme contrainte
> que T doit définir un operateur (), où être convertible
> (implicitement ou explicitement) dans tel type, plutôt que d'imposer
> qu'il ait une méthode "To_TrucChose" ou "Do_CeciOuCela".

hmmm. c'est typique STL ça.
maintenant si on mets un parametre supplémentaire (type ou valeur) qui fait
le boulot, ça revient au même... je crois meme que ça enrichi le tr uc.
bon faut voir...



Euhh... moi pas comprendre là... Tu peux donner un exemple de ce à
quoi tu penses? Typiquement, avec la STL, tu peux avoir:
std::for_each(iterateur_de_debut, itrateur_de_fin, foncteur);

avec foncteur *n'importe quoi* qui définit operator(). Ce n'importe
quoi peut être un pointeur de fonction, un foncteur, etc...
L'alternative que tu proposes, c'est quoi?

> encore trop si ca va ête bénéfique ou pas... De toute façon, co mme
> les génériques ne permettent pas la spécialisation partielle ou
> totale, ce qu'on peut faire avec en manière de programmation
> générique est très limité.

disons que la façon d'exprimer les choses est différente.

si on reprend l'eternel exemple de la classe Complex<T>, les operations de
base sur les composantes Re et Im peuvent etre faites par un objet, disons
Algebra.

point de vue type, on découpe un concept autours de ces opérations +- */, et
ça permet de les "séparer" du type manipulé. moi, j'aime bien , c'e st une
bonne propriété...



C'est vrai, c'est propre sur le papier, c'est orienté objet, c'est
proprement typé, et tout et tout.... Maintenant comment tu fais pour
faire un Complex<int> ? Tu modifies int pour qu'il satisfasse le
concept "Algebra"? (pour moi Algebra est un concept, pas un type à
proprement parler).

Il y a certainement des tas de choses à faire avec cette notion de
"concept", mais je n'ai pas encore vu de proposition de syntaxe et
d'implémentation satisfaisante sur le sujet (je crois que certains
langages "exotiques" à typage dynamique le supportent, mais je n'ai
plus les références en tête).

Cette notion de "concept" est d'ailleurs présente de partout avec la
programmation générique template C++ (et la STL en particulier). Le
problème, c'est que ces concepts ("copy-constructible", "assignable",
par exemple ...) sont implicites dans le code et qu'il n'y a aucun
formalisme syntaxique pour les exprimer, ce qui est AMHA à la base de
l'illisibilité chronique des messages d'erreurs des compilateurs C++
dès qu'on touche les templates.

point de vue efficacité, c'est sur que c'est pas le grand reve ultime p our
l'instant... meme si on gagne du temps sur les algorithmes qui utilisent nos
classes, on aime pas trop avoir ce genre de frein à main levé dans les
briques de base.


A mon avis, c'est surtout que le modèle des generiques avec
spécification des types "instanciables" rend les messages d'erreurs
beaucoup plus lisibles et facilite grandement l'implémentation du
compilateur (la vérification de la "compatibilité" entre le type
d'isntanciation et les contraintes du template est immédiate, alors
qu'avec le modèle C++ c'est la croix et la bannière - surtout si tu
veux supporter "export" c'est-à-dire fournir un template sous forme de
binaire).

Arnaud
MVP - VC
Avatar
Ambassadeur Kosh
> Euhh... moi pas comprendre là... Tu peux donner un exemple de ce à


[...]
L'alternative que tu proposes, c'est quoi?



j'ai pas compris ce que tu as dis, alors...
je pensais que l'operateur () qu'on cherche dans T peut etre passé dans une
fonction.
je pensais que c'etait un idiome de codage trés STL, mais il n'en est peut
etre rien...
rangeons ça dans le tiroire des "affirmations hatives" pour le moment...

C'est vrai, c'est propre sur le papier, c'est orienté objet, c'est
proprement typé, et tout et tout.... Maintenant comment tu fais pour
faire un Complex<int> ? Tu modifies int pour qu'il satisfasse le
concept "Algebra"? (pour moi Algebra est un concept, pas un type à
proprement parler).



pour l'instant, ça devient un type.

public abstract class Algebraic<T>
{
public abstract T Plus(T x,T y) ;
public abstract T Minus(T x,T y) ;
public abstract T Multiply(T x,T y) ;
public abstract T Divide(T x,T y) ;
...
}

public static class Algebraics
{
public static readonly Algebraic<int> Int = new AlgebraicIntImpl() ;
public static readonly Algebraic<double> Double = new
AlgebraicDoubleImpl() ;
...
private sealed class AlgebraicIntImpl : Algebraic<int> { ... }
private sealed class AlgebraicDoubleImpl: Algebraic<double> { ... }
}

on peut mettre un singleton Algebra<T> dans le Complex<T>, par exemple...

public struct Complex<T>
{
...
private T x ;
private T y ;

public static Algebra<T> algebra ;

public static T operator + (T a,T b)
{
T x = albebra.Plus(a.X ,b.X) ;
T y = albebra.Plus(a.Y ,b.Y) ;

return new Complex<T>(x,y) ;
}
}

mais ça me gene. Complex<T> ne peut pas presumer de l'instance de
Algebra<T>. il peut y avoir plusieurs façons d'ajouter des int. (ici
j'exagere, mais dans un cadre plus vaste...) donc, il faut que algebra soit
amené par "l'exterieur", et c'est pas cool. Le Run du prog, mais bon, aucune
garantie qu'on en oublie pas, et faut connaitre tout le code... Reflection ?
pourquoi pas... comme Comparator<T>, mais c'est un peu violent, et pas trés
type-attitude

public struct Complex<T>
{
public T x ;
public T y ;
}

public ComplexMath<T>
{
ComplexMath<T>(Algebra<T> algebra)

private Algebra<T> algebra ;

public Complex<T> Plus(Complex<T> a,Complex<T> b)
public Complex<T> Minus(Complex<T> a,Complex<T> b)
public Complex<T> Multiply(Complex<T> a,Complex<T> b)
public Complex<T> Divide(Complex<T> a,Complex<T> b)
}

niveau lisibilité, on perd les operators, et des expressions mathematiques
compliquées vont etre chiantes...
mais ça a de bonnes propriétés.

maintenant, c'est pareil, on va pas en creer 1E658 instances, de la
ComplexMath<int>. donc, qui l'instancie ?
soit l'utilisateur a ses singletons sous la main, soit il se les fait
amener. et la boucle est bouclée. au plus haut, on va creer les instances
necessaires. ce que fait le compilo en C++ pour chaque "instance de
template"...

DONC :
on ne touche pas à int (à T), on l'utilise...

Il y a certainement des tas de choses à faire avec cette notion de
"concept", mais je n'ai pas encore vu de proposition de syntaxe et


[...]
veux supporter "export" c'est-à-dire fournir un template sous forme de
binaire).




maintenant, oui, de tout ce foutra de singletons et de genericité, il y'a
peut être bien des mécanismes de "concept" à faire emmerger.
la genericité en C++ amene d'essence à de l'efficacité, mais peut être qu'un
compilateur musclé peut applatir les appels aux Singletons en code inline
(sous certaines conditions)... D. Colnet à bien mis ça en route sur Eiffel,
alors pourquoi pas nous
Avatar
Boris Sargos
> Euhh... moi pas comprendre là... Tu peux donner un exemple de ce à
quoi tu penses? Typiquement, avec la STL, tu peux avoir:
std::for_each(iterateur_de_debut, itrateur_de_fin, foncteur);

avec foncteur *n'importe quoi* qui définit operator(). Ce n'importe
quoi peut être un pointeur de fonction, un foncteur, etc...
L'alternative que tu proposes, c'est quoi?



Voilà, c'est exactement mon souci. Moi je suis un fan de la STL, qui
utilise à outrance les templates et les foncteurs.
Les templates apportés par VS2005 ne me semblent pas encore
satisfaisants par rapport à C++. J'ai déjà du contourner quelques
restrictions. Mais c'est déjà pas mal.
Pour les foncteurs, je n'arrive pas à comprendre pourquoi ça a été
supprimé. Ca me semble fondamental.

Merci à vous pour vos tuyeaux. Je ne savais pas qu'on pouvait indexer
avec deux paramètres :

class BorisList {
double this[string key,string key2] { get {...} set {...}}
}

Je vais donc me débrouiller avec çà.

Boris.
Avatar
adebaene
Ambassadeur Kosh wrote:
> Euhh... moi pas comprendre là... Tu peux donner un exemple de ce à
[...]
> L'alternative que tu proposes, c'est quoi?

j'ai pas compris ce que tu as dis, alors...



Bonjour le dialogue de sourds :-)

Bon, je reprends depuis le début : Supposes que tu écrives une classe
template qui prend en paramètre T un foncteur (un "truc" qu'on peut
"appeler", que ce soit un pointeur de fonction, ou un objet). Il est
beaucoup plus naturel dans le code template d'écrire
T& mon_truc_que_je_peux_appeler = ... //initialisé correctment
//....
mon_truc_que_je_peux_appeler();

plutôt que d'écrire
mon_truc_que_je_peux_appeler.DoIt(); //ou un autre nom de fonction.

Entre autres choses, la 1ere version permet que T soit un pointeur de
fonction brut, pas forcément un objet.

Si tu veux une analogie C#, c'est un peu le même principe que pour
rappeler un delegate : on utilise la forme operator() pour appeler le
delegate, quoi que cet "appel" fasse réellement derrière.

<snip>

> C'est vrai, c'est propre sur le papier, c'est orienté objet, c'est
> proprement typé, et tout et tout.... Maintenant comment tu fais pour
> faire un Complex<int> ? Tu modifies int pour qu'il satisfasse le
> concept "Algebra"? (pour moi Algebra est un concept, pas un type à
> proprement parler).

pour l'instant, ça devient un type.



<snip tout un tas d'essais tordus>

Ouais... comme quoi faire de Algebra un type, ca n'a rien d'évident,
on est bien d'accord ;-)
D'où l'idée de concept, c'est à dire de contrainte que l'on impose
sur un type (genre "il supporte les opérations + et -", "il est
assignable", "il est convertible en bool") mais sans que ces
contraintes soient exprimées sous forme d'héritage.
Au passage, exprimer ces contraintes sous forme d'héritage, c'est
plutôt limite par rapport au principe de substitution de Liskov : dire
qu'un int EST un Algebra, c'est plutot bizarre...

Ne restes donc plus qu'à trouver une syntaxe qui va bien pour exprimer
ces concepts de manière formelle et à la faire implémenter ;-)

Arnaud
MVP - VC
Avatar
Ambassadeur Kosh
> beaucoup plus naturel dans le code template d'écrire
T& mon_truc_que_je_peux_appeler = ... //initialisé correctment
//....
mon_truc_que_je_peux_appeler();
plutôt que d'écrire
mon_truc_que_je_peux_appeler.DoIt(); //ou un autre nom de fonction.



ok, compris. mon propos n'etait pas la. c'etait pour la résolution.

// ça, j'aime pas. le + est résolu à l'arrache
void Job<T>(T x,T y)
{
... T z = x + y ;
}

// ça, je prefere
void Job<T>(T x,T y, BinaryFunction<T,T,T> plus)
{
T z = plus(x,y) ;
}

Entre autres choses, la 1ere version permet que T soit un pointeur de
fonction brut, pas forcément un objet.



ok. j'entend bien. le plus est un delegate, et on l'a notre ecriture
friendly.
c'est pour ça que j'en parlais dans la réponse plus haut ces mots

"pour les foncteurs, si tu entends pointeurs de fonction, cad passer en
parametre un agent chargé de faire un boulot et qui a un contrat precis, tu
as plusieurs solutions : les delegate et les class"

Si tu veux une analogie C#, c'est un peu le même principe que pour
rappeler un delegate : on utilise la forme operator() pour appeler le
delegate, quoi que cet "appel" fasse réellement derrière.



eh

pour l'instant, ça devient un type.


<snip tout un tas d'essais tordus>



mais euuhhhhh :o)

D'où l'idée de concept, c'est à dire de contrainte que l'on impose
sur un type (genre "il supporte les opérations + et -", "il est
assignable", "il est convertible en bool") mais sans que ces
contraintes soient exprimées sous forme d'héritage.




Au passage, exprimer ces contraintes sous forme d'héritage, c'est
plutôt limite par rapport au principe de substitution de Liskov : dire
qu'un int EST un Algebra, c'est plutot bizarre...



NON EEEUUHHH ! Algebra est un utility qui manipule des int.

Ne restes donc plus qu'à trouver une syntaxe qui va bien pour exprimer
ces concepts de manière formelle et à la faire implémenter ;-)



concept Number
{
public Number f(Number other) ;
public int Count { get ; }
public static operator(Number x,Number y) ;
}

class Dummy<T> : object where T satisfy Number
{
...
}

on peut meme imaginer s'en servir de bien d'autres façons...

class Dummy satisfy Number
{
public Dummy f(Dummy other) ;
public int Count { get ; }
public static operator(Dummy x,Dummy y) ;
}

en gros quoi. on peut pas trop mettre des interfaces vu que les static n'ont
pas leur place la dedans.
d'ailleurs cette demande de rendre les static abstract ou virtual, c'est
souvent pour avoir cet effet. dans l'heritage, on toujours les deux choses
fusionées : le contrat, et le polymorphisme, mais en fait, on pourrait les
dissocier avec le concept.

allez, une biere, et on ajoute la mutation de type :o)

Fred
GLP - Dotnet