6 votes

Tableau de pointeurs de concepts

Je suis en train d'essayer de comprendre si je peux utiliser des concepts comme une sorte d'interface pour les classes sans nécessiter le surdébit d'une table virtuelle. J'ai mis en place un exemple qui fonctionne plus ou moins, mais je dois stocker les instances de ma classe dans un tableau défini par leur héritage commun plutôt que leur concept commun. Je ne vois rien de discuté dans les messages à propos des tableaux de concepts, mais g++ 6.3.0 ne semble pas le permettre. L'erreur est :

$ g++ -fconcepts -std=c++1z custom_concept.cpp 
custom_concept.cpp: In function ‘int main()’:
custom_concept.cpp:37:20: error: ‘shapes’ déclaré comme tableau de ‘IShape*’
    IShape* shapes[2] = {&square, &rect};  // ne fonctionne pas 
                    ^
custom_concept.cpp:39:25: error: ‘shapes’ n'a pas été déclaré dans cette portée
    for (IShape* shape : shapes ) 
                         ^~~~~~

Si je change le tableau de IShape* en un tableau de Rectangle* (comme dans la ligne de commentaire en dessous de celle qui a causé la première erreur), le programme compile et s'exécute comme prévu.

Pourquoi le tableau de pointeurs de concept n'est-il pas autorisé ? Est-ce probable que cela soit autorisé dans une version future de c++ ?

(Mon exemple inclut des fonctions virtuelles et de l'héritage, même si mon but était de les éliminer. Je les ai inclus seulement pour faciliter le fonctionnement de la version Rectangle*. Si je réussis à faire fonctionner la version IShape*, je prévois de supprimer les fonctions virtuelles et l'héritage.)

Voici le code :

#include 

template 
concept bool IShape = requires (T x, T z, int y)
{
    { T() } ;
    { T(x) }  ;
    { x = z } -> T& ;
    { x.countSides() } -> int ;
    { x.sideLength(y) } -> int ;
};

struct Rectangle
{
    Rectangle() {};
    Rectangle(const Rectangle& other) {};
    Rectangle& operator=(Rectangle& other) {return *this; };
    virtual std::string getName() { return "Rectangle"; }

    int countSides() {return 4;}
    virtual int sideLength(int side) { return (side % 2 == 0) ? 10 : 5; }
};

struct Square : public Rectangle
{
    Square() {};
    Square(const Square& other) {};
    Square& operator=(Square& other) {return *this; };
    std::string getName() override { return "Square"; }
    int sideLength(int side) override { return 10; }
};

int main()
{
    Square square;
    Rectangle rect;
    IShape* shapes[2] = {&square, &rect};  // ne fonctionne pas
//  Rectangle* shapes[2] = {&square, &rect}; // fonctionne
    for (IShape* shape : shapes )
    {
        for (int side = 0 ; side < shape->countSides() ; ++side )
        {
            std::cout << shape->getName() << " side=" << shape->sideLength(side) << "\n";
        }
    }

    return 0;
};

Merci à @Yakk pour l'idée d'utiliser le tuple. G++ 6.3.0 n'avait pas pleinement mis en œuvre le fichier include pour inclure apply() tel que le standard C++17 le définit, mais il était disponible dans std::experimental. (Je pense qu'il peut être ajouté à dans une version ultérieure de g++.) Voici ce avec quoi j'ai fini :

#include 
#include 
#include 

template 
concept bool IShape = requires (T x, T z, int y)
{
   { T() } ;
   { x = z } -> T& ;
   { T(x) }  ;
   { x.countSides() } -> int ;
   { x.sideLength(y) } -> int ;
};

struct Rectangle
{
   Rectangle() {};
   Rectangle(const Rectangle& other) {};
   Rectangle& operator=(Rectangle& other) {return *this; };

   std::string getName() { return "Rectangle"; }
   int countSides() {return 4;}
   int sideLength(int side) { return (side % 2 == 0) ? 10 : 5; }
};

struct Square
{
   Square() {};
   Square(const Square& other) {};
   Square& operator=(Square& other) {return *this; };  

   std::string getName() { return "Square"; }
   int countSides() {return 4;}
   int sideLength(int side) { return 10; }
};

void print(IShape& shape)
{
   for (int side = 0 ; side < shape.countSides() ; ++side )
   {
      std::cout << shape.getName() << " side=" << shape.sideLength(side) << "\n";
   }
};

int main()
{
   Square square;
   Rectangle rect;
   auto shapes = std::make_tuple(square, rect);
   std::experimental::apply([](auto&... shape) { ((print(shape)), ...); }, shapes) ;

   return 0;
};

4voto

Yakk Points 31636

Cela ne peut pas être fait.

Je veux dire que vous pouvez implémenter votre propre effacement de type qui remplace les tables de fonctions virtuelles. Et cela pourrait éventuellement être plus performant qu'une table virtuelle dans votre cas spécifique, car vous pouvez l'adapter à votre problème exact.

Pour obtenir de l'aide du compilateur afin de ne pas avoir à écrire de code de base, vous auriez besoin de la prise en charge de la réflexion et de la réification aux côtés des concepts.

Si vous faisiez cela, cela ressemblerait à ceci :

 ShapePtr shapes[2] = {&square, &rect};

ou

 ShapeValue shapes[2] = {square, rect};

Maintenant, cela ne fera pas tout ce que vous espérez en termes de performances ; l'effacement de type va toujours passer par des pointeurs de fonction. Et avoir un surcoût de stockage par objet ou vue. Vous pouvez cependant échanger plus de stockage contre moins d'indirection.

L'effacement de type manuel ici revient essentiellement à implémenter un modèle d'objet en C, puis l'envelopper pour le rendre joli en C++. Le modèle d'objet par défaut de C++ était juste une approche possible, et les programmes C implémentent de nombreuses alternatives.

Vous pourriez également prendre du recul et remplacer le tableau par un tuple. Les tuples peuvent stocker des types non uniformes, et avec un peu de travail, vous pouvez les parcourir :

 auto shapes = make_IShapePtr_tuple(&square, &rect);

foreach_elem( shapes,[&](IShape* shape )
{
    for (int side = 0 ; side < shape->countSides() ; ++side )
    {
        std::cout << shape->getName() << " côté=" << shape->sideLength(side) << "\n";
    }
});

où le lambda obtient le type non effacé.

Rien de tout cela ne nécessite de concepts :

 auto shapes = std::make_tuple(&square, &rect);

foreach_elem( shapes,[&](auto* shape )
{
    for (int side = 0 ; side < shape->countSides() ; ++side )
    {
        std::cout << shape->getName() << " côté=" << shape->sideLength(side) << "\n";
    }
});

ce qui précède peut être écrit en c++14.

Un c++17 foreach_elem ressemble à ceci :

 template
void foreach_elem( T&& t, F&& f ) {
  std::apply( [&](auto&&...args){
    ( (void)f(decltype(args)(args)), ... );
  }, std::forward(t) );
}

en c++14 la ligne dans le lambda est plutôt :

    using discard=int[];
    (void)discard{ 0,((void)f(decltype(args)(args)),0)... };

ce qui est un peu plus obscur et nécessite une implémentation de std::apply.

En c++11, vous devriez écrire une structure à l'extérieur qui imite le lambda c++14.

0voto

Je vois ce que vous essayez de faire, mais cela n'a pas de sens pour votre cas d'utilisation. Les concepts sont des moyens d'imposer une interface au moment de la compilation, généralement pour les fonctions de modèles. Ce que vous voulez ici est une interface abstraite - une classe de base avec quelques fonctions membres virtuelles pures.

template 
bool collide(S s, U u)
{
    // les types concrets de S et U sont connus ici
    // vous pouvez également utiliser d'autres méthodes, et imposer d'autres concepts sur les types
}

Une interface abstraite impose une interface au moment de l'exécution - vous ne connaissez pas directement le type concret, mais vous pouvez travailler avec les méthodes fournies.

bool collide(ShapeInterface& s, ShapeInterface& u)
{
    // les types concrets de S et U sont inconnus
    // seules les méthodes des interfaces sont disponibles
}

En passant, peut-être que cela n'était qu'un exemple fabriqué de toutes pièces, mais un carré n'est certainement pas un rectangle dans le sens orienté objet. Un exemple simple est que quelqu'un pourrait inclure une méthode appelée stretch dans la classe de base du rectangle, et vous devriez l'implémenter dans votre carré. Bien sûr, dès que vous étirez un carré dans n'importe quelle dimension, ce n'est plus un carré. Soyez prudent.

0voto

NoSenseEtAl Points 2342

La réponse de Yakk est correcte, mais je pense qu'elle est trop compliquée. Vos exigences sont erronées dans le sens où vous essayez d'obtenir "gratuitement" quelque chose que vous ne pouvez pas obtenir gratuitement :

J'essaie de déterminer si je peux utiliser des concepts comme une sorte d'interface pour les classes sans avoir besoin de la surcharge d'une table virtuelle.

La réponse est non. Et ce n'est pas parce que la surcharge de la table virtuelle est un coût inutile. Si vous voulez avoir un tableau de formes pour les utiliser, vous devez stocker des informations sur des instances spécifiques. La machinerie virtuelle le fait pour vous (la manière la plus simple de penser à cela est un membre enum caché pour chaque instance qui dit au compilateur à l'exécution quelles fonctions membres appeler), et si vous le souhaitez, vous pourriez le faire manuellement, mais vous devez le faire d'une manière ou d'une autre (par exemple, vous pourriez utiliser std::variant).

Si vous ne le faites pas, un tableau de pointeurs vers des formes est aussi bon qu'un tableau de pointeurs vers void. Vous ne savez pas ce que pointent vos pointeurs.

note : si vous avez vraiment du mal avec les performances en raison de la surcharge virtuelle, envisagez d'utiliser Boost polly_collection

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X