5
votes

C ++ plusieurs interfaces qui ne diffèrent que par le type de retour?

Faire une expérience de traduction de .NET IL en C ++ d'une manière lisible par l'homme.

Voici le problème: C # vous permet de résoudre plusieurs interfaces avec le même nom de méthode qui ne diffèrent que par type de retour. C ++ ne semble pas prendre en charge cela, cependant, rendant la résolution de deux interfaces impossible à l'aide de vTable (ou est-ce que je me trompe?).

J'ai trouvé un moyen de répliquer l'approche C # en C ++ en utilisant des modèles mais Je me demande s'il existe un moyen qui ne nécessite pas de modèles pour résoudre le même problème? Les modèles sont détaillés et je préfère ne pas les utiliser pour chaque type d'interface si possible.

Voici la version C ++.

class IMyInterface
{
    public: virtual short Foo() = 0;
};

class IMyInterface2
{
    public: virtual int Foo() = 0;
};

class MyClass : public IMyInterface, public IMyInterface2
{
    public: virtual short Foo()
    {
        return 1;
    }

    private: int IMyInterface2::Foo()// compiler error
    {
        return 1;
    }
};

class MyClass2 : public MyClass
{
    public: virtual short Foo() override
    {
        return 2;
    }
};

void InvokeFoo(IMyInterface* k)
{
    k->Foo();
}

void main()
{
    auto a = new MyClass2();
    InvokeFoo(a);
}

Voici la source de référence C # sur laquelle est basée celle C ++. p >

interface IMyInterface
{
    short Foo();
}

interface IMyInterface2
{
    int Foo();
}

class MyClass : IMyInterface, IMyInterface2
{
    public virtual short Foo()
    {
        return 1;
    }

    int IMyInterface2.Foo()
    {
        return 1;
    }
}

class MyClass2 : MyClass
{
    public override short Foo()
    {
        return 2;
    }
}

namespace CSTest
{
    class Program
    {
        static void InvokeFoo(IMyInterface k)
        {
            k.Foo();
        }

        static void Main(string[] args)
        {
            var a = new MyClass2();
            InvokeFoo(a);
        }
    }
}

Cette méthode C ++ ne fonctionne pas ci-dessous mais j'aurais aimé qu'elle le fasse (c'est plus ce que je cherche).

template<typename T>
class IMyInterface
{
    public: short (T::*Foo_IMyInterface)() = 0;
};

template<typename T>
class IMyInterface2
{
    public: int (T::*Foo_IMyInterface2)() = 0;
};

class MyClass : public IMyInterface<MyClass>, public IMyInterface2<MyClass>
{
    public: MyClass()
    {
        Foo_IMyInterface = &MyClass::Foo;
        Foo_IMyInterface2 = &MyClass::IMyInterface2_Foo;
    }

    public: virtual short Foo()
    {
        return 1;
    }

    private: int IMyInterface2_Foo()
    {
        return 1;
    }
};

class MyClass2 : public MyClass
{
    public: virtual short Foo() override
    {
        return 2;
    }
};

void InvokeFoo(IMyInterface<MyClass>* k)
{
    (((MyClass*)k)->*k->Foo_IMyInterface)();
}

void main()
{
    auto a = new MyClass2();
    InvokeFoo(a);
}


15 commentaires

Votre classe d'interface n'est pas vraiment une interface car elle n'est pas virtuelle et définie sur 0.


Oui je sais (je ne peux pas en C ++ est le problème). Si je fais cela, j'obtiens des erreurs de compilation en C ++ car C ++ n'a aucun moyen de résoudre deux classes de base avec la même signature de méthode qui ne diffère que par le type de retour comme C #.


Il semble que vous ne soyez pas autorisé à modifier uniquement le type de retour en cas de surcharge: tutorialspoint.com/cplusplus/ cpp_overloading.htm


C'est à dire. vous cherchez quelque chose comme class MyClass {short IMyInterface :: foo () override; int IMyInterface2 :: foo () override; } ? Peut-être devrions-nous proposer cela pour C ++ 20 ou version ultérieure?


@fstam Ce n'est pas le problème ici - nous héritons de MyClass à la fois short IMyInterface :: foo () et int IMyInterface2 :: foo () et peuvent tous deux utiliser légalement (bien que nécessitant une qualification appropriée). Le problème est: nous ne pouvons pas remplacer car nous ne pouvons pas spécifier lequel remplacer ...


@Aconcagua N'est-ce pas causé par le problème de surcharge?


@Aconcagua Oui, vous avez raison. Si C ++ 20 prenait en charge quelque chose comme celui-ci, similaire à l'exemple d'interface C #, serait utile. C ++ est un langage à héritage multiple et cet exemple correspond (pour moi) très bien à ce que j'attendrais possible en C ++.


@fstam Hm, à y regarder de plus près - vous avez peut-être raison ... Au début, cela ne semble que quelque peu lié, mais toujours différent - la classe dérivée C peut hériter de deux versions de foo (même signature, type de retour différent) de A (en supposant void ) et de B (en supposant int ). Seulement, nous ne pouvons pas simplement écrire C c; c.foo (); , comme l'appel est ambigu, nous devons écrire c.A :: foo (); int n = c.B :: foo (); . Mais maintenant, essayer de remplacer A :: foo crée apparemment une surcharge ( invalide ) pour B :: foo (ou vice versa) - si le terme 'surcharge' est correct dans ce scénario spécifique, pas tout à fait sûr de ...


@Aconcagua a pu le trouver sur la pile: stackoverflow.com/questions/2807840/... et plus encore ici: stackoverflow.com/questions/442026/...


@fstam Suite aux liens de la question citée, il y en a une plus intéressante , mais les deux ne couvrent pas entièrement le cas ici. La question intéressante est: Quand crée-t-on des surcharges? Le simple fait d'hériter de deux versions de foo ( A :: foo et B :: foo ) ne peut pas être considéré comme une 'surcharge', comme si c'était le cas, Le compilateur devrait refuser si les deux ne diffèrent que par le type de retour - mais ce n'est pas le cas. Remplacer maintenant l'une de ces deux versions de foo (c'est-à-dire échanger le pointeur de fonction dans vtable) - est-ce en même temps surcharger l'autre? Semble , au moins.


@Aconcagua Regardez-le du point de vue de l'objet que nous créons. Si vous n'héritez que d'un seul foo , l'objet qui hérite ne surcharge pas, il ne contient qu'une seule définition de foo . Si vous héritez ensuite de l'autre foo , l'objet en question est en surcharge et a maintenant 2 définitions de foo . C'est à ce moment que nous surchargons. Comment cela fonctionne dans la vtable, je ne sais pas. Je suis un développeur C # alors je suppose que je prends tout cela avec un grain de sel :)


@fstam Ha, en C # vous avez aussi des vtables. C'est la norme de facto lors de la mise en œuvre du polymorphisme. Seulement, les gens parlent de beaucoup moins qu'avec C ++ ... Nous arrivons au point où nous devons définir ce qu'est réellement la surcharge (eh bien, nous devrions regarder comment le standard définit!). S'il interdit obligatoirement les surcharges qui ne diffèrent que par le type de retour, un tel héritage ne peut être une surcharge. De plus, les surcharges ne peuvent pas être sélectionnées via la résolution de la portée ( cC :: bar (); - laquelle, si j'en ai deux?), Mais les fonctions héritées peuvent ( cA :: foo , cB :: foo , mais l'appel n'est plus virtuel).


@Aconcagua Je viens de l'essayer en C #. Vous ne pouvez hériter des deux définitions de foo dans une classe sans dire explicitement de quelle interface elles proviennent (appelée implémentation d'interface explicite ). Les implémentations d'interface explicites ne peuvent pas être publiques . Cela vous permet de définir de manière non implicite l'une des définitions foo, vous pouvez la rendre publique. Cette façon d'objecter n'expose jamais l'un ou l'autre des toto. Si vous essayez la même chose sans héritage dans le mix, vous obtenez l'erreur suivante: Le type 'testclass' définit déjà un membre appelé 'bar' avec les mêmes types de paramètres


continuons cette discussion dans le chat .


En remarque C ++ / CLI prend en charge quelque chose de similaire à C # pour le code managé. Voir stackoverflow.com/questions/ 11536808 /… .


4 Réponses :


5
votes

J'ai une solution qui pourrait fonctionner. Ce n'est pas parfait, mais c'est un moyen de contourner le problème si vous portez.

Au lieu d'appeler int foo () , vous pouvez appeler void foo (int & out) de cette façon, vous mettez le type de retour dans la partie appelante de la fonction.


3 commentaires

Hmm intéressant car cela compile et corrige le problème. Je ne sais pas si c'est mieux par rapport aux modèles (peut-être). Gardera cela à l'esprit pour savoir quelle direction prendre à coup sûr.


@ zezba9000 Au moins, vous n'avez pas à écrire vos propres vtables (ce que font effectivement vos templates ...).


Cela empêcherait un code comme bar (foo ()); , cependant, nous serions obligés d'utiliser une variable intermédiaire - si la première est du `` bon style '', bien un autre problème ...



8
votes

Le problème est que vous ne pouvez pas surcharger uniquement en fonction du type de retour.

Voir

Le dernier thread stackoverflow indique que la surcharge est possible en utilisant des opérateurs.

class my_interface
{
public: 
    virtual short foo_short() = 0;
};

class my_interface2
{
public: 
    virtual int foo_int() = 0;
};

class my_class : public my_interface, public my_interface2
{
public: 
    short foo_short() override
    {
        return 1;
    }

    int foo_int() override
    {
        return 1;
    }
};

class my_class2 : public my_class
{
public: 
    virtual short foo_short() override
    {
        return 2;
    }
};

void InvokeFoo(my_interface* k)
{
    short result = k->foo_short();
    std::cout << result << std::endl;
}

void main()
{
    auto a = new my_class2();
    InvokeFoo(a);
}

Cependant, vous ne pouvez pas les nommer, cela se transformerait rapidement en désordre avec lequel travailler.

La liste d'arguments doit changer. Victor Padureau a suggéré d'utiliser des types de retour void et de passer le type de valeur comme référence à définir sur une valeur dans la méthode, cela fonctionnera. Vous pouvez également modifier le nom de la méthode pour les différents types.

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
}


4 commentaires

C'est une bonne idée car cela résoudrait toutes les situations prises en charge par C #. Rendrait le résultat final détaillé mais fonctionne. 99% du code ne se heurte pas à cet exemple de problème qui est le mauvais nom du 1%, mais pourrait être le meilleur.


@ zezba9000 Je pense que l'approche des références est bonne aussi, moins verbeuse. C'est vraiment une question de clarté à ce stade, ce qui est le plus lisible, le plus agréable à travailler, etc. Beaucoup de préférence personnelle.


Hm, je n'aurais pas envisagé différentes surcharges d'opérateurs de cast - à la place, je considérerais l ' opérateur X () et l' opérateur Y () aussi différents que void Fonctions foo () et void bar () ...


@Aconcagua Je suppose qu'ils sont si différents. Je ne l'utiliserais pas, que se passe-t-il lorsque vous utilisez ces opérateurs avec auto? De plus, le problème demeure que vous ne pouvez pas placer les définitions dans des structures séparées (elles sont dans la même structure dans l'exemple) et les hériter. Vous rencontrez toujours le même problème de surcharge.



1
votes

Le mieux est: de ne pas avoir les mêmes noms de fonction dans les deux interfaces , la création de paramètres de sortie des valeurs de retour est certainement une alternative intéressante. Par souci d'exhaustivité: supposons que nous ne pouvons pas changer les interfaces des classes de base, peut-être parce qu'elles proviennent de bibliothèques différentes - et nous devons encore remplacer.

Eh bien, d'abord: hériter des deux interfaces pourrait être une conception discutable, c'est très vous violerez probablement le principe de responsabilité unique .

Maintenant supposons que nous en ayons besoin de toute façon. Une solution de contournement possible alors (cela ne correspond pas à ma propre définition de 'élégance' - mais au moins ...) sont des classes intermédiaires:

class DerivedA : public BaseA
{
public:
    void foo() final // better than override:
                     // prevents breaking the pattern again
    { fooA() };
protected:
    virtual void fooA() = 0;
};
class DerivedB : public BaseB
{
public:
    int foo() final { return fooB() };
protected:
    virtual int fooB() = 0;
};

Maintenant Derived peut servir à la fois de BaseA et BaseB et a toujours remplacé les deux (dans ce cas) variantes de foo, bien qu'indirectement. p>

Si vous avez l'intention d'hériter davantage de Derived , en permettant toujours de remplacer l'une ou l'autre variante de la fonction foo , alors chaque remplacement dans DerivedA et DerivedB appellerait lui-même une (nouvelle) fonction virtuelle fooA et fooB respectivement:

class BaseA { virtual ~BaseA(); virtual void foo(); };
class BaseB { virtual ~BaseB(); virtual int foo(); };

class DerivedA : public BaseA { void foo() override; };
class DerivedB : public BaseB { int foo() override; };

class Derived : public DerivedA, public DerivedB { };
Cette même astuce permet aux remplacements d'utiliser les membres des deux bases et est certainement l'approche la plus propre pour faire avancer les choses, cependant, elle s'accompagne d'un compromis car les appels de fonctions virtuelles ne sont pas gratuits (recherche de vtable, puis appel de fonction réel) - donc si les performances sont plus importantes (pensez bien si elles le sont vraiment !), vous pouvez simplement implémenter les remplacements dans DerivedA et DerivedB directement.


1 commentaires

Si l'on a l'intention de dériver des classes de Derived et que l'on veut remplacer soit foo , alors généralement DerivedA :: foo appellerait virtuel foo_A < / code> (idem pour B) et la dérivation ultérieure utiliseraient et remplaceraient foo_A ou foo_B



0
votes

Si vous voulez un code C ++ qui serait essentiellement similaire à votre exemple C #, je vous recommande d'ajouter essentiellement une classe intermédiaire supplémentaire MyClass_IMyInterface2 dans le seul but de renommer les méthodes implémentées explicitement en C # code.

Ainsi vous auriez quelque chose comme:

class IMyInterface2
{
public: 
    virtual int Foo() = 0;
};

class MyClass_IMyInterface2 : public IMyInterface2
{
protected:
    int Foo() final { return IMyInterface2_Foo(); }
    virtual int IMyInterface2_Foo() = 0; 
};

class MyClass : public IMyInterface, public MyClass_IMyInterface2...

Au fait, vous n'avez besoin d'ajouter la classe supplémentaire que si vous avez des conflits et que vous n'avez besoin que de renommer si c'est le cas.

En supposant que le but serait d'imiter le code C #, je le ferais probablement chaque fois qu'une méthode d'interface est explicitement implémentée.

Vous pouvez également ajuster la visibilité du membres comme vous le souhaitez.


0 commentaires