Partage
  • Partager sur Facebook
  • Partager sur Twitter

constructeur de déplacement

erreur à la compilation

Sujet résolu
8 août 2018 à 19:16:14

Bonjour,

J'ai un souci avec le constructeur de déplacement d'une classe.

J'ai donc une classe principale 'BossMain' (j'ai enlevé quelques fonctions membres sans intérêt) et une classe 'BossEngineDrawer' qui sert à dessiner un sprite à l'écran. Idéalement je crée une classe 'BossEngineDrawer' dans une fonction en chargeant des textures, puis je déplace celle nouvelle classe dans un std::unique_ptr<BossEngineDrawer> qui se trouve dans 'BossMain'. Seulement voilà , je n'arrive pas à résoudre ce problème car le compilateur (g++-7) m'insulte avec un message assez cryptique (j'avoue que j'ai du mal à comprendre le problème, même après une recherche Google/Qwant).

Voici BossMain.h :

class BossMain
{
	private:
		bool isLoadingPerfect;
		unsigned int bossType;
		std::unique_ptr<BossEngine> AerialBoss;
		std::unique_ptr<BossEngineDrawer> EngineDraw;
		std::unique_ptr<LifeCounter> EngineLifeCounter;
		std::vector<SDL_Rect> EngineCollisionBoxes;
        
		std::unique_ptr<GroundBoss> GroundBossStructure;

        unsigned int creditOnDestruction;
        std::array<bool, 3> canPlayerLasersSetDamage;//avoid set damage to quickly du to main fast loop of level

    public:
        BossMain();
        ~BossMain() = default;
        bool wasLoadingPerfect() const;
		void getNewBossEngineDrawer(std::unique_ptr< BossEngineDrawer>& NewBoss);

BossMain.cpp (seulement la partie qui est intéressante):

void BossMain::getNewBossEngineDrawer(std::unique_ptr<BossEngineDrawer>& NewBoss)
{
	EngineDraw.reset( std::move( NewBoss ) );
}

BossEngineDrawer.h

class BossEngineDrawer
{
private:
	bool isLoadingPerfect;
	std::vector<sdl2::SimpleTexture> T_imgReal,
								T_imgShadow,
								T_imgReactor;
	Offset shadowOffset;
	std::vector<Offset> coordBlitReactor;
	unsigned int indexAnimReal,
				 indexAnimShadow,
				 indexAnimReactor,
				 animDelay;
	AccurateTimeDelay AirBossMove;
	TimeDelay AnimBossAir,
			  AnimBossReactor;

public:
	BossEngineDrawer();
	~BossEngineDrawer() = default;
	BossEngineDrawer( const BossEngineDrawer& ) = delete;
	BossEngineDrawer& operator= ( const BossEngineDrawer& ) = delete;
	
	BossEngineDrawer( BossEngineDrawer&& ToMove);

BossEngineDrawer.cpp :

BossEngineDrawer::BossEngineDrawer(BossEngineDrawer&& ToMove ):
	isLoadingPerfect{ std::move( ToMove.isLoadingPerfect ) },
	T_imgReal{ std::move( ToMove.T_imgReal ) },
	T_imgShadow{ std::move( ToMove.T_imgShadow ) },
	T_imgReactor{ std::move( ToMove.T_imgReactor ) },
	coordBlitReactor{ std::move( ToMove.coordBlitReactor ) },
	indexAnimReal{ std::move( ToMove.indexAnimReal ) },
	indexAnimShadow{ std::move( ToMove.indexAnimShadow ) },
	indexAnimReactor{ std::move( ToMove.indexAnimReactor ) },
	animDelay{ std::move( ToMove.animDelay ) },
	AirBossMove{ std::move( ToMove.AirBossMove ) },
	AnimBossAir{ std::move( ToMove.AnimBossAir ) },
	AnimBossReactor{ std::move( ToMove.AnimBossReactor ) }
{
	
}

A noter que la classe sdl2::SimpleTexture à un constructeur de déplacement qui marche très bien (testé ailleurs dans le code).

Et voici l'erreur que m'affiche le compilateur:

BossMain.cpp:83:41: error: no matching function for call to 'std::unique_ptr<BossEngineDrawer>::reset(std::remove_reference<std::unique_ptr<BossEngineDrawer>&>::type)'
  EngineDraw.reset( std::move( NewBoss ) );
                                         ^
In file included from /usr/include/c++/7/memory:80:0,
                 from ../commonFiles/sources/generic/data/sdl2_ptr.h:4,
                 from /media/antoine/projetsLinux/projets/programmation/jeux/Mercenaries/codelite/MercenariesProject/commonFiles/sources/levels/boss/BossMain.cpp:48:
/usr/include/c++/7/bits/unique_ptr.h:371:7: note: candidate: void std::unique_ptr<_Tp, _Dp>::reset(std::unique_ptr<_Tp, _Dp>::pointer) [with _Tp = BossEngineDrawer; _Dp = std::default_delete<BossEngineDrawer>; std::unique_ptr<_Tp, _Dp>::pointer = BossEngineDrawer*]
       reset(pointer __p = pointer()) noexcept
       ^~~~~
/usr/include/c++/7/bits/unique_ptr.h:371:7: note:   no known conversion for argument 1 from 'std::remove_reference<std::unique_ptr<BossEngineDrawer>&>::type {aka std::unique_ptr<BossEngineDrawer>}' to 'std::unique_ptr<BossEngineDrawer>::pointer {aka BossEngineDrawer*}'
game.mk:649: recipe for target 'Release/up_commonFiles_sources_levels_boss_BossMain.cpp.o' failed
make: *** [Release/up_commonFiles_sources_levels_boss_BossMain.cpp.o] Error 1
====1 errors, 3 warnings====

Merci. :)

  • Partager sur Facebook
  • Partager sur Twitter

Mon site web de jeux SDL2 entre autres : https://www.ant01.fr

8 août 2018 à 19:52:24

"reset" prend  un pointeur nu. Cette fonction permet de prendre la responsabilité d'un pointeur nu (non managé).

C'est une simple affectation que tu veux faire :

EngineDraw = std::move( NewBoss );

Hors sujet :

- tes noms ne sont pas consistants, tu as des variables qui commencent avec des minuscules et d'autres avec des majuscules, tu as des variables qui sont préfixées avec "T_"

- "get" pour une fonction qui ne retourne rien et qui prend l'ownership ???

- 1 declaration de variable par ligne

- initializes tes variables lors de la declaration

- ton constructeur par déplacement ne fait rien de plus que celui par defaut a priori. Utilises "= default"

  • Partager sur Facebook
  • Partager sur Twitter
8 août 2018 à 21:04:24

Bonjour gbdivers. :)

Merci pour le tuyau, c'est effectivement une affectation avec std::move(). Pour le nom des variables , oui ça manque de constance je vais modifier tout ça. Par contre, quand tu dis 1 déclaration de variable par ligne, c'est ce que j'ai fait justement , où as tu vu que plusieurs variables sont déclarées sur une même ligne ? o_O

  • Partager sur Facebook
  • Partager sur Twitter

Mon site web de jeux SDL2 entre autres : https://www.ant01.fr

8 août 2018 à 22:09:44

Quand on conseille de declarer une variable a la fois, on ne parle pas simplement de mise en forme.

    unsigned int indexAnimReal,
                 indexAnimShadow,
                 indexAnimReactor,
                 animDelay;

devrait etre :

unsigned int indexAnimReal { 0 };
unsigned int indexAnimShadow { 0 };
unsigned int indexAnimReactor { 0 };
unsigned int animDelay { 0 };




  • Partager sur Facebook
  • Partager sur Twitter
8 août 2018 à 22:13:01

Salut

Par contre, quand tu dis 1 déclaration de variable par ligne, c'est ce que j'ai fait justement , où as tu vu que plusieurs variables sont déclarées sur une même ligne ?

C'est peut-être la façon d'écrire :

    unsigned int indexAnimReal,
                 indexAnimShadow,
                 indexAnimReactor,
                 animDelay;

Au lieu de :

    unsigned int indexAnimReal;
    unsigned int indexAnimShadow;
    unsigned int indexAnimReactor;
    unsigned int animDelay;


Edit : grillé

-
Edité par XxAnoth-ChaxX 8 août 2018 à 22:13:18

  • Partager sur Facebook
  • Partager sur Twitter
9 août 2018 à 16:49:25

gbdivers et XxAnothChaxX ont écrit [...]

Ok. Pour être tout à fait franc avec vous j'ai pensé au code de vos dernier message ce dernier après midi vers 15 h avant de les voir, ça m'est venu comme ça. :-°

Je ne remet pas en cause vos conseils, mais je trouve (ça reste personnel , hein.) que ça alourdit la lisibilité du code. Mais sinon pour le message de gbdivers, il faut donc privilégier le code suivant :

//Fichier.h

class UneClasse
{
private:
    unsigned int entier{0};
    std::string texte{"Code C++"};

public:
    UneClasse() = default;

};

à celui-ci

//fichier.h

class UneClasse
{
private:
    unsigned entier;
    std::string texte;

public:
    UneClasse();

};

Debut fichier source : .cpp

#include <string>
#include "fichier.h"

UneCLasse::UneClasse():
    entier{0},
    texte{"Code C++"}
{}


Ou alors les 2 sont aussi bien l'un que l'autre ?

Edit: je privilégie la liste d'initialisation dans le constructeur autant que faire se peut.

-
Edité par Warren79 9 août 2018 à 16:51:10

  • Partager sur Facebook
  • Partager sur Twitter

Mon site web de jeux SDL2 entre autres : https://www.ant01.fr

9 août 2018 à 17:27:43

>mais je trouve (ça reste personnel , hein.)

Toi, tu t'es pas encore pris la tête avec des mélanges de type plein, de référence, de pointeur, de const etc... sur une seule ligne.

Et t'as pas eu à faire du refactoring de code qui utilisait ton "goût" personnel, car t'aurais très vite changé de "goût", crois-moi.

>que ça alourdit la lisibilité du code

Absolument pas, car on déclare très rarement "beaucoup" de variable en bloc car on les définit là où on les utilise.

Si elles sont très liées, on les regroupe dans des structures, bien plus facilement manipulable.

Il n'y a aucune bonne raison d'avoir votre goût, à moins de vouloir conserver d'autres sales habitudes.

  • Partager sur Facebook
  • Partager sur Twitter
Je recherche un CDI/CDD/mission freelance comme Architecte Logiciel/ Expert Technique sur technologies Microsoft.
9 août 2018 à 18:13:16

Bacelar à écrit [...]

Ok. Les structures je connais, j'en créé très souvent, ne serait-ce que pour diviser des ensembles de choses qui sinon seraient monolithiques. C'est ce que je fais par exemple quand j' essaie d'appliquer l' ISP (Interface Segregation Principal), je divise en interfaces plus petites (struct ou class, c'est selon). Et il y a sans doute pleins d'autres raisons... Je suis très loin d'être un expert du C++ j'en suis parfaitement conscient. ;) Je développe en C++ depuis 5 ans environ et j'apprends souvent des choses en venant sur différents forums et j'ai déjà lu des livres intéressants sur ce langage:

le livre de Koala01 / Philippe Dunski : "développer efficacement" en C++.

"Concurrency in action" d'Anthony Williams basé sur le C++11, surtout la partie multithreading de la STL.

Et malheureusement j'ai commencé avec le livre qui est le pendant du cours C++ d'OpenClassrooms: ça c'est se tirer une balle dans le pied, mais j'en suis sorti, ouf:-° (je me suis demandé si je devais écrire 'Hérésie' au feutre rouge sur ce bouquin d'ailleurs, lol).

-
Edité par Warren79 9 août 2018 à 18:13:51

  • Partager sur Facebook
  • Partager sur Twitter

Mon site web de jeux SDL2 entre autres : https://www.ant01.fr

10 août 2018 à 9:09:54

Si tu dois prendre un objet déplaçable mais non copiable prend le par valeur, le prendre par référence non-const t'empêchera de prendre des variables temporaires. Exemple:

void Object::take(std::unique_ptr<Foo> f)
{
    // variable membre
    foo_ = std::move(f);
}



  • Partager sur Facebook
  • Partager sur Twitter

git is great because Linus did it, mercurial is better because he didn't.

10 août 2018 à 10:52:37

markand a écrit [...]

Bonjour,

Jusqu'ici , je pensais qu'on ne pouvait pas copier un std::unique_ptr<T> (en le passant par valeur ici, par exemple). J'ai du mal à comprendre la suite :

"le prendre par référence non-const t'empêchera de prendre des variables temporaires". Est-ce que tu peux détailler s'il te plaît ? :)

  • Partager sur Facebook
  • Partager sur Twitter

Mon site web de jeux SDL2 entre autres : https://www.ant01.fr

10 août 2018 à 10:54:55

Justement, si tu fais un std::move des deux côté il n'y a aucune copie.

Pour la référence non-const, ce code ne compile pas (certaines versions de MSVC le permettent mais c'est une erreur).

struct foo {
    int x, y;
};

void check(foo& f)
{
}

int main()
{
    foo f1;
    check(f1); // ok lvalue

    check(foo{123, 456}); // erreur
}



  • Partager sur Facebook
  • Partager sur Twitter

git is great because Linus did it, mercurial is better because he didn't.

10 août 2018 à 11:02:51

Ah, ok je comprends , c'est du même acabit  que le code avec une référence const de std::string qui permet de placer une string créée directement en tant que paramètre de fonction, alors que la référence non-const ne le permet pas. Maintenant, ça me revient, Ksass' Peuk avait mis un lien vers une page de blog de Lynnix qui disait que depuis la sémantique de déplacement on était plus obligé de tout passer par référence constante/non constante systématiquement (lorsque c'est possible bien entendu).

Merci de ton explication.

-
Edité par Warren79 10 août 2018 à 11:03:32

  • Partager sur Facebook
  • Partager sur Twitter

Mon site web de jeux SDL2 entre autres : https://www.ant01.fr

10 août 2018 à 12:28:46

C'est exact, d'ailleurs pour les constructeurs je conseille aussi de toute prendre par valeur et déplacer dans les variables membres si besoin, ça évite de nombreuses copies et en plus tu peux mettre dans la plupart des cas ton constructeur noexcept.

class element {
private:
    std::string category_;
    std::string title_;

public:
    element(std::string category, std::string title) noexcept
        : category_(std::move(category))
        , title_(std::move(title))
    {
    }
};

int main()
{
    element e("images", "Cute Cat"); // aucune copie.
}



-
Edité par markand 10 août 2018 à 12:29:16

  • Partager sur Facebook
  • Partager sur Twitter

git is great because Linus did it, mercurial is better because he didn't.

10 août 2018 à 14:16:50

L'article de Lynix en question : https://sirlynixvanfrietjes.be/2018/02/25/cpp-moderne-le-passage-dobjets-par-argument/ 

Il explique bien pourquoi "passage par copie" est incorrect. (Abus langage qui n'etait pas grave avant le deplacement)

  • Partager sur Facebook
  • Partager sur Twitter
10 août 2018 à 14:36:23

@Warren, tu n'as pas compris exactement ce qu'est l'ISP. la ségrégation en interface plus petite ne signifie pas utilisée des objets plus petits, ça c'est plus en rapport avec le SRP (IMHO).

L'ISP te dit de splitter tes interfaces. Prenons par exemple une interface IModem :

class IModem {
public:
    virtual void send(PacketToSend) const = 0;
    virtual void receive(PacketToReceive) = 0;
};

Si tu donnes ton modem a un client, qui lui, ne souhaite qu'envoyer des données, il a aussi la possibilité de recevoir des données alors qu'il s'en fou. La segrégation en interface te dirait plutôt de faire ceci :

class ITransmitter {
public:
    virtual void send(PacketToSend) const = 0;
};

class IReceiver {
public:
    virtual void receive(PacketToReceive) = 0;
};

// Note ici la double présence du publique qui est obligatoire et que tu ne peux pas avoir avec ta façon de déclarer les variables 
class IModem : public ITransmitter, public IReceiver {

};

De cette manière, si un client attend un ITransmitter, tu n'auras pas besoin de lui donner de "IReceiver"/


  • Partager sur Facebook
  • Partager sur Twitter
http://cpp-rendering.io : Vous trouverez tout ce dont vous avez besoin sur Vulkan / OpenGL et le rendu 3D !
10 août 2018 à 14:45:32

(c'est moche tous ces heritages, on dirait du java)
  • Partager sur Facebook
  • Partager sur Twitter
10 août 2018 à 14:47:15

Comment définirais tu l'ISP gbdivers :)?
  • Partager sur Facebook
  • Partager sur Twitter
http://cpp-rendering.io : Vous trouverez tout ce dont vous avez besoin sur Vulkan / OpenGL et le rendu 3D !
10 août 2018 à 15:53:28

L'heritage et les fonctions virtuelles ont un coût. En Java, osef, tout est objet et heritage, et les interfaces sont un usage courant. En C++, on peut éviter ce cout. Si ce cout n'est pas critique, on peut s'amuser a implementer l'ISP comme ca. Sinon, on peut l'implementer via template et des classes de traits.

template<typename Modem>
foo(Modem&& modem) {
    static_assert(is_modem_v<Modem>)
    ...
}

template<typename Receiver>
foo(Receiver&& receiver) {
    static_assert(is_receiver_v<Receiver>)
    ...
}

(Ou sont les conceptssssssss !!!)

Voila, c'etait juste pour faire mon vieux raleur de C++ien, pour rappeler qu'on n'est pas obligé de copier toutes les betises du Java :D

  • Partager sur Facebook
  • Partager sur Twitter
10 août 2018 à 15:56:54

Sur le fait je suis d'accord ^^, après je sais pas trop si parler de template est une bonne chose pour des débutants..., après c'est sûre que plus tôt on y est confronté et on en saisit l'intérêt mieux c'est. Mais sur le principe je suis d'accord avec toi :).

Après le soucis avec de genre d'astuce via template, c'est difficile d'avoir une collection d'objet. Alors pour un modem c'est normal, mais pour des objets qui sont destinés à être placé dans une collection, c'est pas simple ^^

-
Edité par Qnope 10 août 2018 à 16:00:07

  • Partager sur Facebook
  • Partager sur Twitter
http://cpp-rendering.io : Vous trouverez tout ce dont vous avez besoin sur Vulkan / OpenGL et le rendu 3D !
10 août 2018 à 16:46:16

Ce n'est pas une "astuce" dans le sens où l'approche via template n'est pas moins "naturelle" en C++ que l'approche via héritage. Ca l'est uniquement par rapport a l'approche tout-objet (dont le Java est un representant, mais ce n'est pas sur pas le Java la source du problème)

D'ailleurs, le problème que tu signales est souvent aussi basé sur la meme approche tout-objet. Bien sur que pour des problèmes d'aliasing, on ne peut pas mettre directement des objets de taille memoire differents sans une meme collection. Mais on a tendance a vouloir tout mettre dans la meme collection, meme quand ce n'est pas necessaire.

En quoi par exemple avoir 1 collection de pointeurs sur IModem serait mieux que d'avoir 2 collections d'objets (non pointeur) de Receiver et Transmitter ? En quoi avoir une collection de pointeurs sur Value (qui serait dérivé en IntValue, RealValue, StringValue et BoolValue) serait mieux que d'avoir 1 tableau pour chaque classe ?

C'est le principe des ECS : remplacer un héritage pour les entités, par plusieurs composants dans leur propre collections.

On peut parler aussi des pool objects, dans lequel on va mettre des objets de taille differentes.

On peut remplacer une collection de pointeurs sur Point (qui est dérivé en Point2D et Point3D) en ayant une collection de float et des point2d_view et point3d_view.

En fait, le problème du tout-objet, c'est qu'on a trouvé ca tellement genial (ce qui est un peu vrai) qu'on s'est mis des oeillères et on a arreté de voir les autres approches possibles. Et on a aussi enseigné que ça pendant des années. Ce qui est cool, parce que c'est quelque chose qui est facilement transposable d'un langage a un autre (donc plus facile d'enseigner). Mais a force de faire des choses transposables, on oublie ce qui fait la qualité et la spécificité de chaque langage.

  • Partager sur Facebook
  • Partager sur Twitter
10 août 2018 à 17:42:58

Je ne dis pas que ce n'est pas mieux d'avoir 2 tableaux plutot qu'un seul, attention :).

Ce que je dis, c'est que comme tu l'as dis, c'est facile à transposer d'un langage à un autre. J'adore les templates, mais je pense (je n'affirme pas) que quelque choses comme ça :

class ContainerOfBase {
public:
    void append(std::unique_ptr<Base> base) {
        m_bases.push_back(std::move(base));
    }

private:
    std::vector<std::unique_ptr<Base>> m_bases;
};

est plus générique que celui ci :

class Container {
    void append(ConcreteB b);
    void append(ConcreteA a);
private:
    std::vector<ConcreteB> m_Bs;
    std::vector<ConcreteA> m_As;
};

Les deux sont valides, le deuxième est plus efficace que le premier, mais le premier est générique.
Maintenant il est possible de rendre le second générique avec quelque chose proche de ceci :

template<typename T>
class MonoContainer {
    void append(T t);
    void process() {
        for(T &t : m_container) {
            process(t);
        }
    }
    std::vector<T> m_container;
};

template<typename ...Ts>
class Container : public MonoContainer<Ts>... {
public:
    using MonoContainer<Ts>::append...;

    void process() {
        (MonoContainer<Ts>::process())...;
    }
};

void process(A&);
void process(B&);

int main() {
    Container<A, B> container;
    container.append(A{});
    container.append(B{});
    container.process();
    return 0;
}


Je n'ai pas testé le code précédent, mais l'idée est présente. Selon toi, on devrait enseigner plutôt ce genre de pratique lorsque l'on est professeur de C++ plutôt que l'approche objet? Je pense que la première approche est plus simple à comprendre pour un étudiant, mais après je me trompe peut être, j'aimerais juste avoir ton avis sur la question?

Peut être que la façon dont j'ai essayé de rendre générique le truc ne te plaît tout simplement pas, dans ce cas, qu'est ce qui ne te plaît pas et pourquoi?  Et surtout comment aurais tu fait?

Penses tu réellement que l'approche template est aussi légitime que l'approche objet en C++, et que c'est dommage que l'approche template ne soit pas autant mise en valeur que l'approche objet (qui est sûrement vrai, dans la boîte où je bosse les gens qui aiment les template ne sont pas légion, malheureusement...)

-
Edité par Qnope 10 août 2018 à 17:47:27

  • Partager sur Facebook
  • Partager sur Twitter
http://cpp-rendering.io : Vous trouverez tout ce dont vous avez besoin sur Vulkan / OpenGL et le rendu 3D !
10 août 2018 à 20:18:18

L'approche objet (classe de base + heritage) n'est pas du tout "universelle". C'est une approche qui est liée a une semantique particulière (polymorphisme d'héritage). Le problème est que vous souvent des solutions qui vont imposer cette semantique, meme quand cela n'a pas de sens.

Un exemple concret pour expliquer. Si on reprend l'exemple des points 2d et 3d et que l'on souhaite calculer la distance euclidienne par rapport a l'origine. On pourra ecrire :

struct point { virtual double distance() = 0; };
struct point2d: point { double distance() override; };
struct point3d: point { double distance() override; };

vector<unique_ptr<point>> v;
double distance { 0.0 };
for (auto const& p: v) {
    distance += p->distance();
}

Ici, on est bien dans un comportement polymorphique. Quand on manipule le vector de points, on manipule réellement des points. Dans ce cas, "point" est une vraie abstraction d'un point, pas une simple classe fourre-tout qui ne sert qu'a mettre des objets dans une collection. Cela a un vrai sens semantique.

Maintenant, si on veut afficher ces points dans un canvas (2d) ou un contexte (opengl, 3d). On va alors ecrire les classes suivantes par exemple :

struct point { virtual void draw(???) = 0; };
struct point2d: point { void draw(canvas &) override; };
struct point3d: point { void draw(context &) override; };

On peut bien sur réussir a faire cela avec un visitor/double dispatch. Mais au niveau sémantique, quel sens donner a ce que l'on veut faire ? On a des objets de nature différentes, sur les quels on applique des traitements différents. Quel sens donner a la classe qui va contenir les 2 ?

On voit quand même qu'il y a quelque chose de commun. On a un "truc" pour dessiner et on veut dessiner des "machins" dedans. Cela a du sens de vouloir écrire du code generique, c'est a dire du code qui n'est pas specifique aux types, mais qui sera une abstraction du traitement que l'on veut faire. Mais on peut faire tout simplement avec des templates. (Et sans avoir besoin de ton conteneur qui contient 2 conteneurs).

template<typename Point, typename Surface>
void draw(vector<T> const& v, Surface & s) {
    for (auto const& p: v) {
        s.draw(p);
}

Pour bien comprendre, la meme chose avec polymorphisme :

void draw(vector<unique_ptr<Point>> const& v, unique_ptr<Surface> & s) {
    for (auto const& p: v) {
        s->draw(p);
}

Honnetement, je ne pense pas que l'une des solutions est plus difficile a comprendre, pour un etudiant, que l'autre. Ou que l'un des deux est plus generique. Dans les 2 cas, le traitement que l'on souhaite faire (parcourir les points et les afficher) est clairement exprimé.

Et au niveau du code appelant :

vector<point2d> points2d;
vector<point3d> points3d;
points2d.push_back({1, 2});
points2d.push_back({1, 2});
points3d.push_back({1, 2, 3});
points3d.push_back({1, 2, 3});
draw(points2d, canvas2d);
draw(points3d, context3d);

et la version objet :

vector<unique_ptr<point>> points;
points.push_back(make_unique<point2d>({1, 2}));
points.push_back(make_unique<point2d>({1, 2}));
points.push_back(make_unique<point3d>({1, 2, 3}));
points.push_back(make_unique<point3d>({1, 2, 3}));
draw(points, canvas2d);
draw(points, context3d);

En termes de syntaxe, d'abstraction et de compréhension, je pense que c'est équivalent.

En termes d'apprentissage, il me semble assez simple d'expliquer aux etudiants qu'ils pensent "convertir" une fonction classique en fonction generique, en utiliser des templates. (Je parle bien de fonctions generiques ici, pas de meta programmation. Si c'etait le cas, effectivement, ca sera plus complexe a enseigner).

A mon sens, aucune des 2 approches est meilleure dans l'absolue. Chacune est lié a une semantique particuliere, une approche différente de la genericité, des avantages et defauts.

Qnope a écrit:

Selon toi, on devrait enseigner plutôt ce genre de pratique lorsque l'on est professeur de C++ plutôt que l'approche objet? Je pense que la première approche est plus simple à comprendre pour un étudiant, mais après je me trompe peut être, j'aimerais juste avoir ton avis sur la question?

Pour des enseignants, c'est intéressant d'enseigner ce qui est commun entre les langages. Ca permet de ne pas devoir perdre de temps a devoir reexpliquer a des etudiants qui ont appris le java, le python ou autre chose, un concept specifique au langage que tu vas utiliser dans ton enseignement. Quand tu as des etudiants qui viennent de differentes formations, ou que les profs n'utilisent pas tous le meme langage dans leurs enseignement, ca a du sens.

Pour certaines entreprises, cela peut aussi avoir du sens. Si tu es une societe de service et que tes developpeurs vont devoir bosser sur des langages différents a chaque mission, tu ne peux pas te permettre de refaire une formation a chaque changement de mission. Donc on se concentre sur les synthaxes et connaissances qui sont reutilisables.

Mais si tu veux enseigner ou utiliser ce qui fait la specificité d'un langage, tu ne peux pas te contenter de ca.

Du coup, cela va dépendre des objectifs de ton enseignement. Si le but est d'apprendre le C++ comme support pour un enseignement, alors cela peut avoir du sens de ne pas aborder les specificités du C++. (Mais dans ce cas, pourquoi le C++ ?) Si c'est pour les performances ou le typage fort compile time, alors il faut entrer dans les spécificité du C++.

Mais dans tous les cas, il faut bien etre conscient que le tout-objet est un mode de pensé limité. On se ferme forcement des portes en C++ en pensant comme ca. (Vu que le comité de normalisation du C++ a aussi fait cet erreur, on ne peut pas trop reproche aux gens de la faire aussi :D )

Qnope a écrit:

Peut être que la façon dont j'ai essayé de rendre générique le truc ne te plaît tout simplement pas, dans ce cas, qu'est ce qui ne te plaît pas et pourquoi?  Et surtout comment aurais tu fait?

Juste : pourquoi ?

Pourquoi faire un conteneur pour les 2 autres conteneurs ? Et pas appliquer ton traitement generique sur les 2 conteneurs directement ?

Je n'ai rien contre aucune solution, si elle est justifiée.

Qnope a écrit:

Penses tu réellement que l'approche template est aussi légitime que l'approche objet en C++, et que c'est dommage que l'approche template ne soit pas autant mise en valeur que l'approche objet (qui est sûrement vrai, dans la boîte où je bosse les gens qui aiment les template ne sont pas légion, malheureusement...)

Oui. (Et je parle bien de genericité, pas de meta prog)

Quand tu apprends a ecrire une fonction (par exemple une addition), tu vas apprendre a ecrire :

int add(int lhs, int rhs) {
    return lhs + rhs;
}

Une question qui peut naturellement venir, c'est si on veut faire la meme chose sur des reels. Donc on va ecrire la meme fonction avec des doubles. Et on peut alors naturellement parler des fonctions template pour ecrire des fonctions generique, pour lequelles on n'a pas besoin de specifier le type.

En fait, c'est meme l'inverse qui se passera probablement : c'est le fait d'ecrire une fonction qui est specifique pour un type qui embetera les etudiants. De nos jours, la grande majorité des langages n'ont pas de typage fort (js, php, python, etc) et ecrire un code non generique leur semblera etrange.

(Dans mon cours, je presente les fonctions template directement apres le chapitre sur les fonctions. Donc bien avant la POO).

  • Partager sur Facebook
  • Partager sur Twitter
11 août 2018 à 1:40:18

Salut.

Tout d'abord je te remercie de ta réponse très détaillées comme d'habitude.

Je me permet de répondre à ta question "Pourquoi?" sur le code que j'ai posté précédemment. Il se trouve que lorsque l'on décide de rajouter des classes, si on doit se trimballer un vecteur par type, on risque, à un moment où à un autre, d'utiliser le vecteur d'objet que l'on a crée, c'est pour celà qu'on a besoin de généricité.

Prenons le cas d'un ray tracer, j'aime beaucoup cet exemple je sais.

Si on fait ça avec des fonctions virtuelles :

struct Shape {
    virtual ~Shape() = default;
    virtual std::optional<Distance> intersect(Ray ray) = 0;
};

struct Sphere {
    virtual std::optional<Distance> intersect(Ray ray) override;
};

int main() {
    std::vector<Shape*> shapes = getShapes();
    Ray ray = getRayFromCamera();
    for(auto shape : shapes) {
        if(auto dist = shape->intersect(ray)) {
            // On a l'intersection, on peut faire quelque chose
        }
    }
}

 Si jamais ici on veut rajouter une Shape (un plan, un triangle, un mesh), il n'y a pas besoin de toucher à la boucle.

Si maintenant tu tiens à faire un tableau par type sans aucune généricité, à chaque nouvelle classe, tu devras rajouter cette boucle, ce qui t'oblige à faire beaucoup de copier coller et ce n'est pas bien du tout. Alors qu'avec un container comme je l'ai fais précédemment, tu devras juste rajouter le type dans les template variadiques, et c'est finis, le mécanisme s'occupe de tout pour chacun des types.

Ou alors je n'ai pas compris ta remarque.

Pour le reste je suis globalement d'accord avec toi :).

  • Partager sur Facebook
  • Partager sur Twitter
http://cpp-rendering.io : Vous trouverez tout ce dont vous avez besoin sur Vulkan / OpenGL et le rendu 3D !
11 août 2018 à 1:58:54

Rien n'empêche d'avoir une fonction capable de boucler sur chaque conteneur qui dispatche le traitement. Cela limite grandement les modifications: 1 vector en plus = une boucle en plus dans la fonction. L'extérieur n'a plus qu'à donner une lambda à la fonction pour l'appliquer sur chaque élément.

Une autre approche est de passer par un type union tel que std::variant. Un seul conteneur capable de prendre n'importe quel type connu du variant. C'est encore une autre approche.

  • Partager sur Facebook
  • Partager sur Twitter
11 août 2018 à 2:29:06

@jo_link_noir j'aime bien ton approche, en plus ça peut se faire en utilisant mon code de toute à l'heure :).

L'approche pour les sum-types (variant) c'est bien lorsque tu ne rajoutes que rarement des types, mais que tu rajoutes souvent des "fonctions". C'est orthogonal à l'objet :).

  • Partager sur Facebook
  • Partager sur Twitter
http://cpp-rendering.io : Vous trouverez tout ce dont vous avez besoin sur Vulkan / OpenGL et le rendu 3D !
11 août 2018 à 13:20:44

Qnope a écrit:

c'est pour celà qu'on a besoin de généricité.

[...]

il n'y a pas besoin de toucher à la boucle.

A force de repeter les regles "qui vont bien dans les cas generiques", on en oublie de reflechir a la pertinence de ces regles.

Imaginons que tu n'as et n'auras que 2 types d'objets : des boules et des triangles (les autres formes se decomposant en triangles). Alors tu pourrais ecrire :

struct Sphere {
    std::optional<Distance> intersect(Ray ray);
};

struct Triangle {
   std::optional<Distance> intersect(Ray ray);
};

int main() {
    std::vector<Sphere> spheres = getSpheres();
    Ray ray = getRayFromCamera();
    for(auto sphere : spheres) {
        if(auto dist = sphere.intersect(ray)) {
            // On a l'intersection, on peut faire quelque chose
        }
    }

    std::vector<Triangle> triangles = getTriangles();
    for(auto triangle : triangles) {
        if(auto dist = triangle.intersect(ray)) {
            // On a l'intersection, on peut faire quelque chose
        }
    }
}

Quel est l'impact en termes de conception ? Est-ce que cette approche pose des problemes de maintenance ? De validation du code ? Au pire, on met la boucle dans une fonction template, si on veut eviter le copier coller (ou dans une macro ! :D).

Quel est le gain, dans ce cas la, d'un conteneur qui va pouvoir contenir les 2 types d'objets ? Conteneur qu'il faudra valider aussi.

En termes de conception, dans le cas ou l'on a que 2 types d'objets, aucune des 2 approches n'est meilleure que l'autre.

(Note : dans ce cas precis, semantiquement, cela a un sens d'avoir une classe de base Shape. On n'est pas dans le cas d'une semantique qu'on aurait artificiellement utilise sur des classes pour les mettre dans un meme conteneur)

(Note2 : au dela de la conception, la solution avec les pointeurs et la classe de base pose un gros probleme de performances, en ajoutant des indirections, des fonctions virtuelles, et perte de la memoire contigue. C'est pour cela que je prefere largement la seconde solution dans ce cas)

-----------------------------------------------------------

Prenons un autre exemple. Imaginons qu'on veut implementer des widgets a la Qt. On va avoir des centaines de classes differentes.

Dans ce cas, on imagine tres mal d'avoir des centaines de vecteur dans chaque objets pour contenir les widgets enfants. Alors que la grande majorite des objets aurons 0 ou peu de widgets enfants. Le cout d'avoir des conteneurs differents est enorme et inutile.

-----------------------------------------------------------

La genericite a du sens quand on ne sait pas comment le code va etre utilise. Donc en particulier dans les libs reutilisables : lib standard, boost, etc.

Mais dans le code metier, tu sais comment tu vas utiliser ton code, ce qu'il va contenir, le nombre de types que tu peux avoir, la taille des donnees, etc. Tu n'as pas besoin de faire de la genericite pour la genericite. C'est un outil qui doit etre evaluer dans le contexte precis de son utilisation, par rapport aux benefices et defauts des differentes approches.

Bien sur, j'ai pris des exemples un peu particulier. Si on n'a que 2 types possibles, l'approche par heritage est moins interessante (a mon sens). Mais si on en a 3 ? 5 ? 10 ? A quel moment on va faire le changement d'une approche a une autre ?

Cette question est tres difficile. Ne pas faire le changement, c'est accumuler de la dette technique. Faire le changement demande un travail supplementaire pour concevoir, implementer et tester. Faire le changement trop tot, c'est prendre le risque de faire un travail inutile. 

Je n'ai pas de reponse a cette question. C'est tres fortement lie a ce qu'on veut faire exactement. Dans certains cas, cela aura du sens de prendre le temps d'implementer des le depart une solution generique. Dans d'autres cas, on pourra se permettre de ne pas utiliser de solution generique, meme si on a 10 ou 20 cas differents.

Peu importe la solution choisie. Ce que je critique surtout (et pas que moi) avec le tout-objet, c'est d'avoir une solution generique qu'on utilise dans tous les cas, sans reflechir sur les alternatives et les avantages et defauts. (L'exemple des shapes pour un ray caster me semble un tres bon exemple de mauvaise utilisation, si on veut des performances)

-----------------------------------------------------------

Une remarque sur variant. Si tu as un variant<char, int, double> par exemple, le sizeof du variant sera le sizeof max des types manipules. Ce qui veut dire que si tu as un char, cela prendra autant de memoire qu'un double. Tu te retrouves avec une memoire qui est fragmenter.

Pas top.

-----------------------------------------------------------

Pour donner mon point de vue sur la solution que je choisirais pour ce probleme de Shape : j'utiliserais des vues sur un conteneur de points.

En gros : toutes les formes sont des ensembles de points, un gros tableau pour tous les points, chaque groupe de points est interprete comme etant une forme particuliere.

using point2d = array_view<float, 2>;
using point3d = array_view<float, 3>; // ou 4, si tu as w
using triangle3d = array_view<point3d, 3>;
using mesh = vector_view<point3d>;

vector<float> all_points;

Pour l'indexation des elements : soit un tableau de formes dans n'importe quel ordre, soit les formes sont regroupes ensemble.

// cas ou tous les elements sont melanges
struct shape_view {
    vector_view<point3d> data;
    int type { 0 }; // ou enum
};

vector<shape_view> shapes;

// cas ou tous les elements sont regroupes
struct shapes_view {
    vector_view<shape_view> data;
    int type { 0 }; // ou enum
};

vector<shapes_view> shapes;

Detais :

- swap plutot que insertion

- pas de reallocation de vector

- possible de mapper directement le tableau sur le gpu

  • Partager sur Facebook
  • Partager sur Twitter
11 août 2018 à 13:44:52

Dans un ray tracer tu as aussi les hyperboloide à une nappe, les paraboloides etc. En gros tout ce qui est quadrique.

Je suis d'accord d'un point de vue personne, la base objet est mauvaise dans le cas d'un ray tracer. Par contre le fait d'avoir un container (qui en contient d'autre) l'est beaucoup moins je pense.

Comme tu l'as dis, y a pas de solution qui fait tout, tout dépend du cas par cas, et de ce que l'on pense qu'il va se passer à l'avenir. Et quand on développe un logiciel et qu'on connaît les problèmes métier, c'est le cas.

Tu dis a un moment que le fait d'avoir des containers différents est énorme et inutile. Si ils sont vide ça ne devrait pas poser de problèmes, si? 

Petite note sur les variants : Ils font la taille du plus gros objet + un identifiant pour savoir quel type il contient ;).

Décidément, faut je me renseigne de plus en plus sur ces vues, j'utilise pratiquement jamais string_view alors que je suis sûre que ça peut être utile ^^.

  • Partager sur Facebook
  • Partager sur Twitter
http://cpp-rendering.io : Vous trouverez tout ce dont vous avez besoin sur Vulkan / OpenGL et le rendu 3D !
11 août 2018 à 22:24:45

Qnope a écrit:

Dans un ray tracer tu as aussi les hyperboloide à une nappe, les paraboloides etc. En gros tout ce qui est quadrique.

Je prenais l'exemple d'un cas simple a 2 shapes pour simplifier et expliquer le probleme du choix de structures. Si tu as un nombre petit de formes differentes, connus a l'avance et que tu n'ajouteras pas (ou peu souvent) de nouvelles formes, il peut etre quand meme interessant de ne pas utiliser un heritage.

Et ce qui compte, ce n'est pas le nombre de types, mais le nombre de facon d'interpreter les types. Si tu as des carres, des rectangles, des spheres, etc. mais qu'ils sont tous manipules via un ensemble de triangles, ca ne serait pas forcement une bonne idee d'utiliser un heritage pour representer ces triangles. Ou il est possible de separere la logique correspond a la forme et la logique correspondant aux triangles. (Avec des structures de donnees differentes).

C'est a dire passer de ca :

class Shape { 
    virtual void draw() = 0;
};

class Sphere : public Shape {};
class Cube : public Shape {};
class Tor : public Shape {};

Par :

// structures for meshes
vector<triangle> meshes;

struct mesh_view {
    vector<triangle>::iterator first, last;
};

void draw(vector<triangle> const& meshes);

// structures pour l'heritage d'objets
class Shape {
    mesh_view mesh;
};

class Sphere : Public Shape {};
class Cube : Public Shape {};
class Tor : Public Shape {};

Qnope a écrit:

Par contre le fait d'avoir un container (qui en contient d'autre) l'est beaucoup moins je pense.

A mon sens, le probleme est la fragmentation de la memoire, qui ne va pas etre cache-friendly. Donc tres mauvais niveau perfs.

Qnope a écrit:

Comme tu l'as dis, y a pas de solution qui fait tout, tout dépend du cas par cas, et de ce que l'on pense qu'il va se passer à l'avenir. Et quand on développe un logiciel et qu'on connaît les problèmes métier, c'est le cas.

Comme je l'ai dit, ce qui me semble poser probleme, c'est l'apprentissage d'une solution unique, comme si c'etait le seule approche possible, sans recul sur les autres approches. Ce qui a du sens dans certains langages, qui ne permettent pas d'implementer des solutions plus performantes bas niveau. Mais c'est aussi une perte en C++, ou l'on peut faire ce genre d'approches.

Qnope a écrit:

Tu dis a un moment que le fait d'avoir des containers différents est énorme et inutile. Si ils sont vide ça ne devrait pas poser de problèmes, si? 

Je parlais d'une solution comme ca par exemple :

class Sphere {
    vector<Sphere> sphereChildren;
    vector<Cube> cubeChildren;
    vector<Tor> torChildren;
};

class Cube {
    vector<Sphere> sphereChildren;
    vector<Cube> cubeChildren;
    vector<Tor> torChildren;
};

class Tor {
    vector<Sphere> sphereChildren;
    vector<Cube> cubeChildren;
    vector<Tor> torChildren;
};

Tu as le cout des vector vides. Et si on parles de widgets avec plusieurs centaines de types, cette approche serait completement absurde.

Qnope a écrit:

Décidément, faut je me renseigne de plus en plus sur ces vues, j'utilise pratiquement jamais string_view alors que je suis sûre que ça peut être utile ^^.

Quand je parle de view, je parle surtout du concept. C'est a dire un pointer-like sur une collection, pour exposer un type different. Pas forcement de string_view ou span (qui sont const). Et array_view et vector_view n'existent pas.

Par exemple :

vector<float> data;

template<typename T>
struct array_view {
    vector<float>::iterator first, last; // ou juste "first"
    T& operator[](int i) { return *(first+i); }
};

struct point {
    array_view<float> coordinates;
    
    float& x() { return coordinates[0]; }
    float& y() { return coordinates[1]; }
    float& y() { return coordinates[2]; }
};

struct triangle {
    array_view<point> points;

    point& x() { return points[0]; }
    point& y() { return points[1]; }
    point& y() { return points[2]; }
};

struct mesh {
    array_view<triangle> triangles;

    triangle& x() { return triangles[0]; }
triangle& y() { return triangles[1]; }
triangle& y() { return triangles[2]; }
};

L'idee est de ne pas manipuler directement des tableaux de floats, mais des types intermediaires, qui ont une vraie semantique, et qui ne possedent pas les donnees.

-
Edité par gbdivers 11 août 2018 à 22:25:25

  • Partager sur Facebook
  • Partager sur Twitter
12 août 2018 à 0:35:49

Hello !

Justement dans ma façon de voir le container tu n'as pas de fragmentation mémoire étant donné que tu as un vecteur pour chaque type de shape :-). De cette manière tu iteres sur chacune de tes formes qui sont stockées de manière contigu et non fragmenté en mémoire :-)

Si tu représentes tout par des triangles en effet je serai passé par une sorte de builder qui remplit un vecteur de triangle aussi :-)

  • Partager sur Facebook
  • Partager sur Twitter
http://cpp-rendering.io : Vous trouverez tout ce dont vous avez besoin sur Vulkan / OpenGL et le rendu 3D !
12 août 2018 à 0:54:52

Qnope a écrit:

Justement dans ma façon de voir le container tu n'as pas de fragmentation mémoire étant donné que tu as un vecteur pour chaque type de shape :-). De cette manière tu iteres sur chacune de tes formes qui sont stockées de manière contigu et non fragmenté en mémoire


Alors il y a un truc que tu as mal explique ou que je n'ai pas compris. Parce que le code tu as donne :

struct Shape {
    virtual ~Shape() = default;
    virtual std::optional<Distance> intersect(Ray ray) = 0;
};
 
struct Sphere {
    virtual std::optional<Distance> intersect(Ray ray) override;
};
 
int main() {
    std::vector<Shape*> shapes = getShapes();
    Ray ray = getRayFromCamera();
    for(auto shape : shapes) {
        if(auto dist = shape->intersect(ray)) {
            // On a l'intersection, on peut faire quelque chose
        }
    }
}

La memoire est fragmente dans ce cas. Et l'utilisation d'un heritage et de fonctions virtuelles va difficilement etre cache-friendly.

Et j'avais compris que tu avais justement 1 seul conteneur pour tous tes types. (Au contraire de la solution que je presentais, ou tu avais plusieurs conteneurs)

-
Edité par gbdivers 12 août 2018 à 0:55:16

  • Partager sur Facebook
  • Partager sur Twitter