Code C ++ multiplateforme et en-tête unique – implémentations multiples

J’ai entendu dire qu’un moyen d’écrire du code Cross Platform c ++ est de définir les classes comme suit (par exemple, une classe Window):

window.h window_win32.cpp window_linux.cpp window_osx.cpp 

puis choisissez le fichier d’implémentation en conséquence. Mais que faire si j’ai des membres de cette classe qui sont relatifs à l’OS? Comme un membre HWND pour l’implémentation Win32. Je ne peux pas le mettre dans le window.h ou quand window.h de le comstackr, disons, Linux, cela générerait une erreur du compilateur.

#ifdef -je besoin de #ifdef cela? J’ai déjà posé une question similaire, mais celle-ci est davantage axée sur ce problème particulier.

Il existe plus de moyens de résoudre ce problème – chacun a ses avantages et ses inconvénients.

1.) Utilisez les macros #ifdef, #endif

 // Note: not sure if "WINDOWS" or "WIN32" or something else is defined on Windows #ifdef WINDOWS #include  #else // etc. #endif class MyClass { public: // Public interface... private: #ifdef WINDOWS HWND m_myHandle; #else // etc. #endif }; 

Avantages:

  • Vitesse maximale du programme.

Les inconvénients:

  • Pire lisibilité. Avec de nombreuses plates-formes, cela peut devenir vraiment compliqué.
  • Inclure des inclusions spécifiques à la plate-forme pourrait casser quelque chose windows.h définit beaucoup de macros avec des noms normaux.

2.) Comme il y avait déjà été écrit, vous pourriez utiliser le polymorphism:

 // IMyClass.h for user of your class: class IMyClass { public: virtual ~IMyClass() {} virtual void doSomething() = 0; }; // MyClassWindows.h is implementation for one platform #include  #include "IMyClass.h" class MyClassWindows : public IMyClass { public: MyClassWindows(); virtual void doSomething(); private: HWND m_myHandle; }; // MyClassWindows.cpp implements methods for MyClassWindows 

Avantages:

  • Beaucoup, beaucoup plus propre code.

Les inconvénients:

  • L’utilisateur ne peut pas créer directement d’instances de votre classe (surtout pas sur la stack).
  • Vous devez fournir une fonction spéciale pour la création: par exemple, declare IMyClass * createMyClass (); et le définir dans MyClassWindows.cpp et d’autres fichiers spécifiques à la plate-forme. Dans ce cas (enfin, dans tout ce cas de polymorphism), vous devez également définir une fonction qui détruit les instances – afin de conserver l’idiome “celui qui l’a créé doit également détruire”.
  • Peu de ralentissement en raison de méthodes virtuelles (pratiquement insignifiantes de nos jours, sauf dans des cas très très spéciaux).
  • Remarque: l’allocation peut poser problème sur les plates-formes avec une mémoire limitée en raison de problèmes de fragmentation de la RAM. Dans ce cas, vous pouvez résoudre ce problème en utilisant une sorte de pool de mémoire pour vos objects.

3.) idiome PIMPL.

 // MyClass.h class MyClass { public: MyClass(); void doSomething(); private: struct MyClassImplementation; MyClassImplementation *m_impl; } // MyClassWindows.h #include  #include "MyClass.h" struct MyClassImplementation { HWND m_myHandle; void doSomething(); } 

Dans ce cas, MyClassImplementation conserve toutes les données nécessaires (au moins spécifiques à une plate-forme) et implémente ce qui est nécessaire (à nouveau, spécifique à une plate-forme). Dans MyClass.cpp, vous incluez l’implémentation spécifique à la plate-forme (les méthodes peuvent être en ligne), dans le constructeur (ou plus tard, si vous voulez – soyez prudent) vous allouez l’implémentation et dans le destructeur, vous la détruisez.

Avantages:

  • L’utilisateur peut créer des instances de votre classe (y compris sur la stack) (ne vous inquiétez pas des pointeurs non supprimés / supprimés).
  • Vous n’avez pas besoin d’inclure des en-têtes spécifiques à la plate-forme dans MyClass.h.
  • Vous pouvez simplement append un compteur de références et implémenter le partage de données et / ou la copie sur écriture, ce qui permet facilement d’utiliser votre classe comme valeur de retour, même si elle conserve une grande quantité de données.

Les inconvénients:

  • Vous devez allouer un object d’implémentation. La piscine d’objects peut aider.
  • Lors de l’appel d’une méthode, deux sont appelées à la place et un pointeur déréférencé. Pourtant, aujourd’hui ne devrait pas être un problème.

4.) Définissez un type neutre, suffisamment grand pour conserver vos données. Par exemple long long int.

 // MyClass.h class MyClass { public: MyClass(); void doSomething(); private: typedef unsigned long long int MyData; MyData m_data; }; 

Dans la mise en œuvre (par exemple, MyClassWindows.cpp), vous devez toujours transtyper (réinterpréter la conversion) entre MyClass :: MyData et les données réelles stockées.

Avantages:

  • Aussi vite que le premier moyen mais vous évitez les macros.
  • Éviter l’atsortingbution si non nécessaire.
  • Éviter les appels de méthode multiples.
  • Eviter d’inclure des en-têtes spécifiques à la plateforme dans MyClass.h.

Les inconvénients:

  • Vous devez être absolument sûr à 110% que la taille de MyClass :: MyData est toujours au moins identique à celle des données stockées.
  • Si différentes plates-formes stockent des données de tailles différentes, vous perdez de la place (si vous utilisez quelques éléments, c’est correct, mais avec des millions …), sauf si vous utilisez des macros. Dans ce cas, ce ne sera pas si compliqué.
  • C’est un travail de bas niveau avec les données – peu sûr, pas de POO. Mais rapide.

Utilisez donc celui qui convient le mieux à votre problème … et à votre personnalité: 3 car avec le pouvoir actuel, ils sont tous quatre plus ou moins relativement égaux en termes de vitesse et d’espace.

L’intérêt d’une telle approche est que vous encapsuliez des données spécifiques au système d’exploitation dans le fichier spécifique au système d’exploitation. Si vous devez contourner un HWND, vous pouvez reconsidérer la conception de votre object. Le sens d’une telle structure dépend de la taille de votre code spécifique à un système d’exploitation. Vous ne voulez pas vraiment compresser toutes les classes possibles dans un seul fichier.

D’autre part, il existe des bibliothèques pour les interfaces graphiques qui font exactement cela – encapsulant les composants spécifiques au système d’exploitation dans une bibliothèque telle que QT ou wxWidgets ou autres. Si vous séparez correctement l’interface graphique du code principal, vous n’aurez peut-être même pas besoin de cette approche.

J’utilise une telle structure dans mon projet pour prendre en charge différentes versions de xerces, sans que le code principal ne soit encombré par #ifdefs . Cependant, dans mon cas, le code affecté est plutôt petit. Je peux imaginer qu’un framework d’interface graphique prend beaucoup plus d’espace.

J’appendais seulement un 5) point à la réponse de @Laethnes

5) Ecrivez un en-tête vide qui inclut, au moment de la compilation, l’en-tête de la plate-forme et masquez la classe spécifique à la plate-forme sous une typedef

 // MyClass.hpp #if defined(WINDOWS) # include "WINMyClass.hpp" typedef WINMyClass MyClass #elif defined(OSX) # include "OSXMyClass.hpp" typedef OSXMyClass MyClass ... // keep going #endif 

Avantages:

  • c’est seulement un typedef, très simple
  • bonne lisibilité
  • la vitesse

Les inconvénients:

  • c’est seulement un typedef

Une méthode courante consiste à utiliser le polymorphism. Vous fournissez une interface qui résume vos fonctionnalités sans aucun égard pour une plate-forme spécifique:

 class thread { virtual ~thread() {} virtual void run() = 0; /* whatever */ }; 

et vous pouvez ensuite hériter de cette classe à l’aide de fonctionnalités spécifiques à la plate-forme:

 class posix_thread : thread; 

au moment de la compilation, vous choisissez avec #ifdef classe que vous incluez et instanciez.

La plupart du temps, vous évitez de telles choses dans l’en-tête. Cependant, il y a des moments où vous voulez les exposer. Dans de tels cas, ma solution a toujours été d’utiliser quelque chose comme:

 #include dependentInclude(syst,MyInclude.lhh) 

dependInclude étant une macro qui étend le chemin du fichier dont j’ai besoin, en fonction de la valeur de syst (définie sur la ligne de commande).