web-dev-qa-db-fra.com

Un modèle de fonction de membre de classe C++ peut-il être virtuel?

J'ai entendu dire que les modèles de fonction de membre de classe C++ ne peuvent pas être virtuels. Est-ce vrai? 

Si elles peuvent être virtuelles, quel est un exemple de scénario dans lequel une telle fonction serait utilisée?

261
WannaBeGeek

Les modèles concernent uniquement le compilateur générant le code à la compilation compile-time. Les fonctions virtuelles concernent le système d’exécution, déterminant quelle fonction appeler à run-time

Une fois que le système d’exécution a compris qu’il aurait besoin d’appeler une fonction virtuelle modélisée, la compilation est terminée et le compilateur ne peut plus générer l’instance appropriée. Par conséquent, vous ne pouvez pas avoir de modèles de fonction de membre virtuel. 

Cependant, il existe quelques techniques puissantes et intéressantes issues de la combinaison du polymorphisme et des modèles, notamment ce que l’on appelle le type effacement

289
sbi

À partir de modèles C++ Le guide complet:

Les modèles de fonctions membres ne peuvent pas être déclarés virtuels. Cette contrainte est imposé parce que l'implémentation habituelle de la fonction virtuelle Le mécanisme d'appel utilise une table de taille fixe avec une entrée par virtuel une fonction. Cependant, le nombre d'instanciations d'une fonction membre le modèle n'est pas corrigé tant que tout le programme n'a pas été traduit . Par conséquent, prendre en charge les modèles de fonction de membre virtuel nécessiterait prise en charge d'un tout nouveau type de mécanisme dans les compilateurs C++ et linkers. En revanche, les membres ordinaires des modèles de classe peuvent être virtuelle parce que leur nombre est fixe lorsqu'une classe est instanciée

104
InQusitive

C++ n'autorise pas les fonctions de membre de modèle virtuel pour le moment. La raison la plus probable est la complexité de sa mise en œuvre. Rajendra donne une bonne raison pour laquelle cela ne peut pas être fait maintenant mais cela pourrait être possible avec des changements raisonnables de la norme. Il est particulièrement difficile de déterminer le nombre d'instanciations d'une fonction basée sur un modèle et de construire la table vtable semble difficile si vous considérez l'emplacement de l'appel de fonction virtuelle. Les gens de standard ont juste beaucoup d'autres choses à faire en ce moment et C++ 1x représente également beaucoup de travail pour les rédacteurs de compilateur.

Quand auriez-vous besoin d'une fonction membre basée sur un modèle? Une fois, j’ai rencontré une telle situation dans laquelle j’essayais de refactoriser une hiérarchie avec une classe de base virtuelle pure. C'était un style médiocre pour la mise en œuvre de différentes stratégies. Je voulais changer l'argument de l'une des fonctions virtuelles en un type numérique et au lieu de surcharger la fonction membre et de surcharger chaque surcharge dans toutes les sous-classes pour lesquelles j'ai essayé d'utiliser des fonctions de modèle virtuel (et je devais trouver qu'elles n'existaient pas). .) 

31
pmr

Tables de fonctions virtuelles

Commençons par quelques informations sur les tables de fonctions virtuelles et leur fonctionnement ( source ):

[20.3] Quelle est la différence entre le virtuel et le non-virtuel les fonctions membres sont appelées?

Les fonctions membres non virtuelles sont résolues de manière statique. C'est le La fonction membre est sélectionnée statiquement (au moment de la compilation) en fonction du fichier type du pointeur (ou référence) sur l'objet.

En revanche, les fonctions de membre virtuel sont résolues dynamiquement (à l'exécution ). C'est-à-dire que la fonction de membre est sélectionnée dynamiquement (à l'exécution ) En fonction du type de l'objet et non du type de pointeur/référence à cet objet. Ceci est appelé "liaison dynamique" La plupart des compilateurs utilisent une variante de la technique suivante: si le objet a une ou plusieurs fonctions virtuelles, le compilateur place un .__ caché. pointeur dans l'objet appelé "pointeur virtuel" ou "pointeur v." Ce v-pointer pointe vers une table globale appelée "table virtuelle" ou "v-table."

Le compilateur crée une v-table pour chaque classe ayant au moins un fonction virtuelle. Par exemple, si la classe Circle a des fonctions virtuelles pour draw (), move () et resize (), il y aurait exactement une v-table associé à la classe Circle, même s'il y avait un million de cercle les objets, et le v-pointeur de chacun de ces objets Circle indiquerait au cercle v-table. La table des v lui-même a des pointeurs sur chacun des fonctions virtuelles dans la classe. Par exemple, le cercle v-table serait avoir trois pointeurs: un pointeur sur Circle :: draw (), un pointeur sur Circle :: move (), et un pointeur sur Circle :: resize ().

Lors de l'envoi d'une fonction virtuelle, le système d'exécution suit le pointeur v de l'objet sur la table v de la classe, puis suit le emplacement approprié dans la v-table au code de la méthode.

Les frais d’encombrement de la technique ci-dessus sont minimes: un supplément pointeur par objet (mais uniquement pour les objets devant faire une liaison dynamique ), plus un pointeur supplémentaire par méthode (mais uniquement pour les méthodes virtuelles ). Les frais généraux liés au temps sont également relativement minimes: comparés à un appel de fonction normal, un appel de fonction virtuel nécessite deux extra récupère (un pour obtenir la valeur du pointeur v, un second pour obtenir l'adresse de la méthode). Aucune de ces activités d'exécution ne se produit avec fonctions non virtuelles, car le compilateur résout .__ non virtuel. fonctionne exclusivement au moment de la compilation en fonction du type du fichier aiguille.


Mon problème ou comment je suis arrivé ici

J'essaie d'utiliser quelque chose comme ceci maintenant pour une classe de base cubefile avec des fonctions de chargement optimisées par modèle qui seront implémentées différemment pour différents types de cubes (certains stockés par pixel, d'autres par image, etc.).

Un code:

virtual void  LoadCube(UtpBipCube<float> &Cube,long LowerLeftRow=0,long LowerLeftColumn=0,
        long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
virtual void  LoadCube(UtpBipCube<short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
        long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
virtual void  LoadCube(UtpBipCube<unsigned short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
        long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;

Ce que j'aimerais que ce soit, mais cela ne se compilera pas à cause d'un combo virtuel basé sur un modèle:

template<class T>
    virtual void  LoadCube(UtpBipCube<T> &Cube,long LowerLeftRow=0,long LowerLeftColumn=0,
            long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;

J'ai fini par déplacer la déclaration de modèle au niveau de la classe. Cette solution aurait obligé les programmes à connaître certains types de données à lire avant de les lire, ce qui est inacceptable.

Solution

warning, ce n'est pas très joli mais cela m'a permis de supprimer le code d'exécution répétitif

1) dans la classe de base

virtual void  LoadCube(UtpBipCube<float> &Cube,long LowerLeftRow=0,long LowerLeftColumn=0,
            long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
virtual void  LoadCube(UtpBipCube<short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
            long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;
virtual void  LoadCube(UtpBipCube<unsigned short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
            long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1) = 0;

2) et dans les classes d'enfants

void  LoadCube(UtpBipCube<float> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
        long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1)
{ LoadAnyCube(Cube,LowerLeftRow,LowerLeftColumn,UpperRightRow,UpperRightColumn,LowerBand,UpperBand); }

void  LoadCube(UtpBipCube<short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
        long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1)
{ LoadAnyCube(Cube,LowerLeftRow,LowerLeftColumn,UpperRightRow,UpperRightColumn,LowerBand,UpperBand); }

void  LoadCube(UtpBipCube<unsigned short> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
        long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1)
{ LoadAnyCube(Cube,LowerLeftRow,LowerLeftColumn,UpperRightRow,UpperRightColumn,LowerBand,UpperBand); }

template<class T>
void  LoadAnyCube(UtpBipCube<T> &Cube, long LowerLeftRow=0,long LowerLeftColumn=0,
        long UpperRightRow=-1,long UpperRightColumn=-1,long LowerBand=0,long UpperBand=-1);

Notez que LoadAnyCube n'est pas déclaré dans la classe de base. 


Voici une autre réponse de débordement de pile avec une solution de contournement: besoin d'une solution de contournement de membre de modèle virtuel

16
Mark Essel

Non, ils ne peuvent pas. Mais:

template<typename T>
class Foo {
public:
  template<typename P>
  void f(const P& p) {
    ((T*)this)->f<P>(p);
  }
};

class Bar : public Foo<Bar> {
public:
  template<typename P>
  void f(const P& p) {
    std::cout << p << std::endl;
  }
};

int main() {
  Bar bar;

  Bar *pbar = &bar;
  pbar -> f(1);

  Foo<Bar> *pfoo = &bar;
  pfoo -> f(1);
};

a le même effet si tout ce que vous voulez faire est d’avoir une interface commune et de reporter l’implémentation aux sous-classes.

11
Tom

Le code suivant peut être compilé et s'exécute correctement à l'aide de MinGW G ++ 3.4.5 sur Windows 7:

#include <iostream>
#include <string>

using namespace std;

template <typename T>
class A{
public:
    virtual void func1(const T& p)
    {
        cout<<"A:"<<p<<endl;
    }
};

template <typename T>
class B
: public A<T>
{
public:
    virtual void func1(const T& p)
    {
        cout<<"A<--B:"<<p<<endl;
    }
};

int main(int argc, char** argv)
{
    A<string> a;
    B<int> b;
    B<string> c;

    A<string>* p = &a;
    p->func1("A<string> a");
    p = dynamic_cast<A<string>*>(&c);
    p->func1("B<string> c");
    B<int>* q = &b;
    q->func1(3);
}

et le résultat est:

A:A<string> a
A<--B:B<string> c
A<--B:3

Et plus tard j'ai ajouté une nouvelle classe X:

class X
{
public:
    template <typename T>
    virtual void func2(const T& p)
    {
        cout<<"C:"<<p<<endl;
    }
};

Quand j'ai essayé d'utiliser la classe X dans main () comme ceci:

X x;
x.func2<string>("X x");

g ++ signale l'erreur suivante:

vtempl.cpp:34: error: invalid use of `virtual' in template declaration of `virtu
al void X::func2(const T&)'

Il est donc évident que:

  • virtual member function peut être utilisé dans un modèle de classe. Il est facile pour le compilateur de construire vtable
  • Il est impossible de définir une fonction membre d'un modèle de classe comme virtuelle, comme vous pouvez le constater, il est difficile de déterminer la signature de la fonction et d'allouer des entrées vtable.
11
Brent81

Non, les fonctions de membre de modèle ne peuvent pas être virtuelles. 

4
dirkgently

Pour répondre à la deuxième partie de la question:

Si elles peuvent être virtuelles, quel est un exemple de scénario dans lequel une telle fonction serait utilisée?

Ce n'est pas une chose déraisonnable à vouloir faire. Par exemple, Java (où chaque méthode est virtuelle) n’a aucun problème avec les méthodes génériques.

Un exemple en C++ de vouloir un modèle de fonction virtuelle est une fonction membre qui accepte un itérateur générique. Ou une fonction membre qui accepte un objet de fonction générique.

La solution à ce problème consiste à utiliser le type erasure avec boost :: any_range et boost :: function, ce qui vous permettra d'accepter un itérateur ou un foncteur générique sans qu'il soit nécessaire de transformer votre fonction en modèle.

3
exclipy

Il existe une solution de contournement pour la «méthode de modèle virtuel» si un ensemble de types pour la méthode de modèle est connu à l'avance.

Pour illustrer cette idée, dans l'exemple ci-dessous, seuls deux types sont utilisés (int et double).

Là, une méthode modèle 'virtuelle' (Base::Method) appelle la méthode virtuelle correspondante (une parmi Base::VMethod) qui, à son tour, appelle la mise en œuvre de la méthode modèle (Impl::TMethod).

Il suffit d’implémenter la méthode template TMethod dans les implémentations dérivées (AImpl, BImpl) et d’utiliser Derived<*Impl>.

class Base
{
public:
    virtual ~Base()
    {
    }

    template <typename T>
    T Method(T t)
    {
        return VMethod(t);
    }

private:
    virtual int VMethod(int t) = 0;
    virtual double VMethod(double t) = 0;
};

template <class Impl>
class Derived : public Impl
{
public:
    template <class... TArgs>
    Derived(TArgs&&... args)
        : Impl(std::forward<TArgs>(args)...)
    {
    }

private:
    int VMethod(int t) final
    {
        return Impl::TMethod(t);
    }

    double VMethod(double t) final
    {
        return Impl::TMethod(t);
    }
};

class AImpl : public Base
{
protected:
    AImpl(int p)
        : i(p)
    {
    }

    template <typename T>
    T TMethod(T t)
    {
        return t - i;
    }

private:
    int i;
};

using A = Derived<AImpl>;

class BImpl : public Base
{
protected:
    BImpl(int p)
        : i(p)
    {
    }

    template <typename T>
    T TMethod(T t)
    {
        return t + i;
    }

private:
    int i;
};

using B = Derived<BImpl>;

int main(int argc, const char* argv[])
{
    A a(1);
    B b(1);
    Base* base = nullptr;

    base = &a;
    std::cout << base->Method(1) << std::endl;
    std::cout << base->Method(2.0) << std::endl;

    base = &b;
    std::cout << base->Method(1) << std::endl;
    std::cout << base->Method(2.0) << std::endl;
}

Sortie:

0
1
2
3

NB: Base::Method est en réalité un surplus pour du code réel (VMethod peut être rendu public et utilisé directement) . Je l'ai ajouté pour qu'il ressemble à une méthode de modèle 'virtuelle' réelle.

2
sad1raf

Au moins avec gcc 5.4, les fonctions virtuelles peuvent être membres de modèles mais doivent être elles-mêmes. 

#include <iostream>
#include <string>
class first {
protected:
    virtual std::string  a1() { return "a1"; }
    virtual std::string  mixt() { return a1(); }
};

class last {
protected:
    virtual std::string a2() { return "a2"; }
};

template<class T>  class mix: first , T {
    public:
    virtual std::string mixt() override;
};

template<class T> std::string mix<T>::mixt() {
   return a1()+" before "+T::a2();
}

class mix2: public mix<last>  {
    virtual std::string a1() override { return "mix"; }
};

int main() {
    std::cout << mix2().mixt();
    return 0;
}

Les sorties

mix before a2
Process finished with exit code 0
0
Maxim Sinev

Dans les autres réponses, la fonction de modèle proposée est une façade et n'offre aucun avantage pratique.

  • Les fonctions de modèle sont utiles pour écrire du code une seule fois en utilisant Différents types. 
  • Les fonctions virtuelles sont utiles pour avoir une interface commune pour différentes classes.

Le langage n'autorise pas les fonctions de modèle virtuel, mais avec une solution de contournement, il est possible d'avoir les deux, par exemple. un modèle d'implémentation pour chaque classe et une interface commune virtuelle.

Il est toutefois nécessaire de définir pour chaque combinaison de type de modèle une fonction de wrapper virtuel factice:

#include <memory>
#include <iostream>
#include <iomanip>

//---------------------------------------------
// Abstract class with virtual functions
class Geometry {
public:
    virtual void getArea(float &area) = 0;
    virtual void getArea(long double &area) = 0;
};

//---------------------------------------------
// Square
class Square : public Geometry {
public:
    float size {1};

    // virtual wrapper functions call template function for square
    virtual void getArea(float &area) { getAreaT(area); }
    virtual void getArea(long double &area) { getAreaT(area); }

private:
    // Template function for squares
    template <typename T>
    void getAreaT(T &area) {
        area = static_cast<T>(size * size);
    }
};

//---------------------------------------------
// Circle
class Circle : public Geometry  {
public:
    float radius {1};

    // virtual wrapper functions call template function for circle
    virtual void getArea(float &area) { getAreaT(area); }
    virtual void getArea(long double &area) { getAreaT(area); }

private:
    // Template function for Circles
    template <typename T>
    void getAreaT(T &area) {
        area = static_cast<T>(radius * radius * 3.1415926535897932385L);
    }
};


//---------------------------------------------
// Main
int main()
{
    // get area of square using template based function T=float
    std::unique_ptr<Geometry> geometry = std::make_unique<Square>();
    float areaSquare;
    geometry->getArea(areaSquare);

    // get area of circle using template based function T=long double
    geometry = std::make_unique<Circle>();
    long double areaCircle;
    geometry->getArea(areaCircle);

    std::cout << std::setprecision(20) << "Square area is " << areaSquare << ", Circle area is " << areaCircle << std::endl;
    return 0;
}

Sortie: 

La zone carrée est 1, la zone du cercle est 3.1415926535897932385

Essayez-le ici

0
andreaplanet