Wednesday, December 30, 2015

Je peux améliorer mon C++

Grande différence entre mon "nouveau" boulot (depuis Mars 2014) et mon ancien poste universitaire: ici, il y a des revues de code. Et mes collègues "Hergé et Jigé" ont un sacrément haut niveau en C++ comparé au mien. Alors autant profiter de mes deux semaines de "Super Papa Bros" pour essayer de remanier le code de mon moteur de jeu, le rendre plus fiable, plus lisible, et peut-être plus efficace.

J'avais introduit un mécanisme de gestion de mémoire inspiré du cours "compilateurs" : le "tank", avec un seul bloc de mémoire qui est découpé progressivement en sous-blocs qui auront tous la même durée de vie. L'ennui principal, c'est que ce "tank" n'a aucun moyen de retenir quels objets ont été créés ni d'appeler les destructeurs en fin de cycle. Du coup, tout objet "standard" présent dans les morceaux du tank sont une fuite de mémoire potentielle.


Parmi les "nouveaux trucs" appris cette année qui pourront m'être utiles, il y a la fonction "foreach", les fonctions template (et en particulier leur utilisation pour faire de la programmation assertive), les namespaces anonymes, et les structures-internes-pour-masquer-l'implémentation.


Let me collate a few C++ tricks I practiced this year and hope to use in my hobby tools/game engine to improve them.
If it make sense to have a function applied on all members of a collection, foreach can help:

- for (vector<Tire>::iterator it = wheels.begin(), e = wheels.end(); it != e; it++) {
- checkPressure(*it);
- }
+ for_each(wheels.begin(), wheels.end(), checkPressure);


Template function do exist. Template functions do not need their template argument to be specified when it can be inferred from function arguments. E.g.

template<typename T>
void assert(T a, T b, const std::string msg) {
  if (a != b) throw AssertException(msg);
}

can be invoked as
+ assert(myCar, TimeTravellingDelorean, "timed' out");
-assert<Car>(myCar, TimeTravelling ...);;

Template integers exist too. If you want something to behave completely differently depending on whether you're on a 32-bit or 64-bit system, you might consider the following function that can be invoked as getLibraryPath<sizeof(int)>():


template<int> path getLibraryPath();
template<> inline path getLibraryPath<4>() {
  return "/usr/lib32";
}
template<> path getLibraryPath<8>() {
  return "/usr/lib/x86_64-linux-gnu";
}

And as we're talking about templates stack overflow's question on puzzling template error messages can help.

You don't need to declare your functions static to avoid interference with other translation units of the program. Simply put them in an anonymous namespace.

You don't need to explicitly track the "object setup sequence" with an init_level if you can do it with contents of the regular members of the objects,


Car::~Car() {
- switch(init_level) {
- case TIRES_MOUNTED: RecycleTires();
- case ENGINE_INSTALLED: RecycleEngine();

- // FIXME: what do you do for default: ?
- }

+ if (tires!=UNDEF) RecycleTires();
+ if (engine!=UNDEF) RecycleEngine();

PS: UNDEF could just be 0 for pointers to components.

You can have compact structure initialization with (optionally-)named fields but it must be *trivial*, e.g.

  • you may not swap the order of components;
  • you may not omit a field if there are other field after it
  • but you *can* omit items at the tail of the description

If you want a class/struct to look more like a first-class citizen, think about
  • copy constructor : Car(const Car &that) : engine(that.engine), tires(that.tires) {}
  • comparison operator : bool operator==(const Car &that) { return that.tires==tires && that.engine==engine; }
  • ostream-compatibility: this requires a additional std::ostream& operator<<(std::ostream& os, const Car& that) { os << "powered by " << engine << " on " << tires; return os; } function. Note that it is *not* a member of the Car struct/class and that it will need to be declared friend of the Class in case of a class.


Something thrown as throw new std::runtime_error(..) is caught by catch (...) { releaseResources(); throw; }, but not by catch(const exception& e). That latest one only catch stack-allocated exceptions, e.g. throw std::runtime_error(...);. Reading more on this I should.

I should remember that namespace ds = PPPTeam::LibGEDS is the way to say import PPPTeam.LibGEDS as ds. And that ostream & operator << (ostream &out, const Complex &c) is the way to tell how the class 'Complex' should be printed.

Oh, and I shouldn't use std::unique_ptr on stack-allocated object. ever. unique_ptr will eventually call free on the pointer it holds.

 

In case of doubt on performance, remember that Quick-Bench.com does exist.

2 comments:

Vincent said...

Super intéressant, je vais pouvoir utiliser ça pour mon projet aussi
- nominé aux Vincent's Awards.

PypeBros said...

https://medium.com/@nonuruzun/overloading-input-output-operators-in-c-a2a74c5dda8a