Pages

dimanche 22 décembre 2013

Parcourir les arguments d'une fonction variadique

À l'approche de Noël et du déballage de cadeaux, faisons un tour sur le déballage des paramètres variadiques.

La méthode habituelle pour utiliser chaque paramètre est la récursion jusqu'à plus d'argument ou jusqu'à un nombre défini, généralement 1.

Quelque chose comme ça:

#include <iostream>
 
void f1()
{}
 
template<class T, class... Args>
void f1(const T& first, const Args&... others)
{
  std::cout << first << '\n';
  f1(others...);
}
 
 
template<class T>
void f2(const T& first)
{
  std::cout << first << '\n'; 
}
 
template<class T, class... Args>
void f2(const T& first, const Args&... others)
{
  std::cout << first << ", ";
  f2(others...);
}
 
 
int main()
{
 f1(1, 2.4, "plop");
 f2(1, 2.4, "plop");
}

Ce qui a affiche:

1
2.4
plop
1, 2.4, plop

Il existe cependant une autre façon de faire qui n'utilise pas la récursivité. J'ai découvert cette méthode sur le forum de openclassroms (ce sujet, 10ème message), elle est très astucieuse.

Implémentée sous la forme d'une macro cela donne:

#define UNPACK(...) (void(::std::initializer_list<char>{(void((__VA_ARGS__)), '\0')...}))
  • Le premier void permet d'ignorer l'initializer_list créé.
  • (void((__VA_ARGS__)), '\0') évalue l'ensemble de l'expression, ignore le résulat (grâce au void) et retourne un caractère dans l'initalizer_list.
  • Au final l'expression est dépaquetée et l'initializer_list est rempli de 0.

Avec cette macro les 2 fonctions précédentes deviennent:

#include <initializer_list>

#define UNPACK(...) (void(::std::initializer_list<char>{(void((__VA_ARGS__)), '\0')...}))

template<class... Args>
void f1(const Args&... args)
{
  UNPACK(std::cout << args << '\n');
}

template<class T, class... Args>
void f2(const T& first, const Args&... others)
{
  std::cout << first;
  UNPACK(std::cout << ", " << others);
  std::cout << '\n';
}

Ceci simplifie considérablement l'écriture :).

Évidemment, cette macro se trouve depuis dans falcon: CPP1X_UNPACK.

dimanche 20 octobre 2013

Faire un flux qui redirige vers d'autres flux (std::ostream).

Je prend l'exemple type: la création d'un logger. Il est courant de voir ce genre de classe dans un projet souvent utilisé à travers une macro pour y insérer date, fichier et numéro de ligne. L'idée est bonne mais l'implémentation de la classe logger est souvent trop compliquée.

Cette classe manipule généralement les flux std::cerr ou une instance de std::ofstream. Voire même dans certaines implémentations une multitude de flux. En gros, elle fait la passerelle entre des données et le(s) flux où écrire.

En partant de ce constat, on peut imaginer voir un header un peu comme ça:

class Logger {
public:
  switch_stdout(bool);
  switch_stderror(bool);
  //...

  Logger & operator << (int);
  Logger & operator << (const char *);
  //etc

private:
  std::ofstream log {"/tmp/log"};
  std::ofstream trace {"/tmp/trace"};
  bool to_cerr = false;
  bool to_cout = false;
};

Un problème est que cette classe n'est pas du tout compatible avec les flux. La raison en est toute simple, ce n'en est pas un !
Du fait de cette non compatibilité, une utilisation courante comme `Logger() << type_perso()` ne fonctionnera pas s'il n'y a pas la surcharge de type_perso pour Logger ET pour std::ostream

Ce point est facile à corriger en remplaçant toutes les surcharges de << par une template. En plus ça réduit le nombre de lignes :)
Cependant, le Logger n'étant pas un flux, d'autres problèmes peuvent exister.

Le plus gros problème reste l'apparente complexité pour écrire dans les flux. Tout ce mal alors que std::ostream fait déjà presque tout. Et que dire de std::clog qui rien que par son nom semble être fait pour ça ?

Les flux ont en interne un buffer, un std::streambuf pour être plus précis. Sa fonction est de prendre des chaînes de caractères et de les écrire quelque part. Ou inversement, de lire une chaîne.
Difficile de faire plus bas niveau.

Bien évidemment, les buffers d'un flux son interchangeables, il m'est tout à fait possible de rediriger std::cout dans un fichier en quelques lignes de code seulement.

int main()
{
  std::filebuf sbuf;
  if (!sbuf.open("/tmp/out")) {
    //error
    return 1;
  }
  std::cout.rdbuf(&sbuf);
  std::cout << "j'écris dans un fichier\n";
}

Au final, Logger fait la même chose mais sans la gestion d'erreur et sans flux de sortie entièrement personnalisable.

Dans mon idée, logger peut simplement contenir le buffer de log (std::clog.rdbuf() par défaut) et fournir au moins 1 méthode pour fournir un buffer avec la responsabilité de le détruire. Le reste est gérer automatique par l'héritage sur std::ostream. ou via une méthode pour fournir le flux.

void Logger::give_buffer_ownership(std::unique_ptr<std::streambuf> buf)
{
  delete internal_buffer;
  internal_buffer = buf.release();
  internal_ostream.rdbuf(internal_buffer);
}

Éventuellement un moyen de créer un objet intermédiaire qui ajoute la date, nom de fichier, etc dans le constructeur et flush automatiquement à sa destruction (vive le RAII).

mercredi 18 septembre 2013

Tableau dans un std::vector

Prenons le type suivant `std::vector<int[4]>` qui peut se justifier. À priori cela ne cause aucun problème ; et c'est vrai !

Ajoutons maintenant un élément à notre vector avec push_back et patatra rien ne va plus, il y a 2 erreurs.
La première concerne la construction du tableau et la seconde sa destruction car un tableau n'a ni constructeur ni destructeur.

La manière la plus facile pour éliminer ces erreurs de compilation est de mettre un wrapper sur le tableau. Écrire ce wrapper n'est pas très compliqué et Oh joie, Oh bonheur, il existe std::array.

Mais si l'on tient vraiment à notre tableau (pour d'obscures raisons satanique :D) il est toujours possible de modifier le comportement du vector pour qu'il comprenne les tableaux. Ceci à travers l'allocator, le second paramètre template d'un vector ; celui jamais utilisé, toujours oublié `std::vector<T, là_ici>`.

La technique consiste à remplacer l'allocator par un spécialisé pour les tableaux. Le plus simple est d'hériter d'un std::allocator et de redéfinir les 2 méthodes problématiques: construct. et destroy.
Et ne surtout pas oublier rebind qui permet de recréer l'allocateur avec un type interne différent (c'est utilisé par les containers :)).

(Un précédent article permet de comprendre les mécanismes utilisés par un allocateur.)

L'implémentation de construct et destroy est vraiment bateau, il suffit d'appeler le constructeur ou le destructeur pour chaque élément du tableau.
Mais pour faire au minimum bien les choses et supporter l'allocation de tableau de tableau (int[3][2]) on utilise array_allocator::construct/destroy si les cellules sont des tableaux (ce qui fait 2 fonctions récursives) ou std::allocator::construct/destroy dans le cas contraire.
À noter que la récursivité peut être éliminée en utilisant les propriétés d'alignement des tableaux (int[3][2] -> int[3*2]).

template<typename T>
class array_allocator;

//c'est juste pour les tableaux
template<typename T, std::size_t N>
class array_allocator<T[N]>
: public  std::allocator<T[N]>
{
public:
  using value_allocator = typename std::conditional<
    std::is_array<T>::value,
    array_allocator<T>,
    std::allocator<T>
  >::type;

public:
  template<typename U>
  struct rebind
  { typedef array_allocator<U> other; };

  template<typename U>
  void construct(U* p, const T(&val)[N])
  {
    value_allocator alloc;
    for (std::size_t n = 0; n < N; ++n) {
      alloc.construct(&(*p)[n], val[n]);
    }
  }

  template<typename U>
  void destroy(U* p)
  {
    value_allocator alloc;
    for (std::size_t n = 0; n < N; ++n) {
      alloc.destroy(&(*p)[n]);
    }
  }
};

Dans l'idéal il faudrait un `construct` avec un nombre variable d'arguments ce qui permet d'utiliser emplace_back.
Toutefois, cette version est fonctionnelle avec push_back :).

std::vector<int[3], array_allocator<int[3]> > v;
int a[3]{1,2,3};
v.push_back(a);
v.push_back({4,5,6});
(La lib falcon (dont je suis le seul développeur actuellement ^^) dispose d'un allocateur de ce style: generic_allocator. Comme son nom l'indique, l'allocateur couvre un spectre un peu plus large et gère en plus les types POD.)

samedi 20 juillet 2013

Utilisation de swap et des fonctions utilitaires en général

Quitte à trouver un meilleur nom, ce que j'appelle fonction utilitaire est toute fonction libre de conversion (to_string, etc) ou d'accesseur externe à une classe (get, begin, end, etc).

Ce sont toutes des fonctions qu'on peut trouver en C++11. La seule fonction utilitaire qui me vient à l'esprit en C++03 est swap (fonction qui échange le contenu de 2 variables). Pour info, l'en-tête de swap est passé de <algorithm> en C++03 à <utility> en C++11.

Il arrive qu'un jour ou l'autre on veuille faire une surcharge de swap pour un objet particulier. À ce moment 2 choix s'offrent: en faire une fonction libre dans le namespace où se trouve la classe ou dans le namespace std. Évidemment, le meilleur choix est le premier.

Et là, pour toutes les personnes qui ont en horreur le `using namespace std` ou travaillant dans les .h, un problème va se poser. Comme swap se trouve dans le namespace de la std, il est logique de faire std::swap(...). Mais à ce moment, la fonction swap spécialisée dans le namespace de la classe n'est pas utilisée...

#include <iostream>
#if __cplusplus >= 201103L
# include <utility>
#else
# include <algorithm>
#endif

namespace my {
  struct A{};
  void swap(A&, A&)
  { std::cout << "ok\n"; }
}

int main()
{
  my::A a, b;
  std::swap(a,b);
}

N'affiche rien... :/.

Par contre ce qui suit affiche "ok".

int main()
{
  my::A a, b;
  swap(a,b);
}

Comme une fonction swap prenant des my::A existe elle est utilisé. Cela est possible car la fonction swap fait partie du namespace my et que les variables a et b sont des instances d'une classe du même namespace.

Mais ceci ne compile pas.

int main()
{
  int a, b;
  swap(a,b);
}

Donc, d'un côté on a un swap générique dans la std et de l'autre un swap spécialisé dans my.
Et surtout, 2 syntaxes différentes.

Dans un contexte générique (typiquement des templates) et de maintient de code ; 2 formes n'est pas acceptable.
Il faudrait que std::swap soit utilisé si aucun swap spécialisé n'existe.

Pour ce, on 'déplace' std::swap dans le scope courant avec using et grâce l'ADL (Argument-dependent lookup) le compilateur appellera la bonne fonction.

int main()
{
  using std::swap;
  {
    my::A a, b;
    swap(a,b); //affiche ok
  }
  {
    int a, b;
    swap(a,b); //compile
  }
}

Et ceci s'applique pour toutes les fonctions libres.

L'article suivant présente une solution pour éviter l'utilisation systématique de using.

mercredi 26 juin 2013

make sans Makefile, utilisation des règles implicites

La commande GNU make possède énormément de commandes implicites. En fait, tout est regroupé dans un Makefile 'par défaut' avec plein de règles. Celui-ci est visible en tapant `make -pf /dev/null` dans un terminal.

Rien qu'avec ça, on peux compiler des fichiers C, C++, archive, latex, etc. Il y a de quoi faire en fait.

Par exemple, je crée un fichier C nommé test.c

int main(int ac, char ** av) {
  return ac;
}

compilation: `make test`
Ceci crée l’exécutable test. Mais si on veut avoir des fichiers objets, il faut le demander explicitement: `make test.o test`.

Maintenant on remplace test.c par test.cpp et on inclut iostream.

compilation: `make test`
avec un objet: `make test.o test`.

Ah ! En fait non, le second ne fonctionne pas.
test.o est compilé avec cc et comme iostream est linker avec une lib, l'implémentation du constructeur std::ios_base::Init n'est pas trouvée. L'idéal serait d'utiliser g++.

Si ce n'est qu'une question de compilateur, changeons-le !

Tout d'abord, la règles de compilation d'un .o vers un exécutable:

$ make -pf/dev/null | grep '%: %.o' -A3
%: %.o
#  commands to execute (built-in):
        $(LINK.o) $^ $(LOADLIBES) $(LDLIBS) -o $@

Ça utilise pour compiler la variable LINK.o. Soit, qu'est ça valeur ?

$ make -pf/dev/null | g 'LINK.o ='
LINK.o = $(CC) $(LDFLAGS) $(TARGET_ARCH)

Qui fait référence à CC. On s'approche, vérifions:

$ make -pf/dev/null | g 'CC ='
CC = cc

Bingo \o/. Il suffit donc de changer la valeur de CC.

On recommence: `make CC=g++ test.o test` et ça fonctionne :).

Cette petite excursion permet mine de rien de découvrir pas mal de variables. Au passage, il est plus intéressant de les utiliser que d'en recréer des nouvelles. Idem dans les règles de dépendances. S'il faut les personnaliser, autant garder les mêmes commandes à exécuter.

Par exemple, pour compiler un .cpp en .o:

%.o: %.cpp
#  commands to execute (built-in):
        $(COMPILE.cpp) $(OUTPUT_OPTION) $<

Généralement les sources sont également dépendantes de fichier d'en-tête. Si on fait une règle qui prend en compte cela, autant garder la même commande:
(petite parenthèse pour dire que gcc possède l'option -MM pour lister les dépendances entre les fichiers.)

bidule.o: bidule.cpp bidule.h machin.h
        $(COMPILE.cpp) $(OUTPUT_OPTION) $<

Ainsi, quand CXXFLAGS sera modifié, la règle le prendra en compte car COMPILE.cpp = COMPILE.cc
Et COMPILE.cc = $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c

À noter que la variable CPPFLAGS fait référence à la commande cpp (ou option -E de gcc) et concerne le C-preprocessor.

Bien sûr, la technique du Makefile seul a des limites. Par exemple, s'il y a plusieurs fichiers C, je peux créer tous les fichiers objets avec `make *.o` mais l’exécutable ne sera dépendant que d'un fichier. Au final les règles par défaut ne permettront pas de créer l’exécutable, il faudra faire `gcc *.o` ou un Makefile avec au minimum a.out: *.o; $(LINK.o) $^ $(LOADLIBES) $(LDLIBS) -o $@.

Voilà, ce fut un petit post pour découvrir l’existence des règles implicites et des variables prédéfinies :).

manual de make

mercredi 29 mai 2013

optimisation de script bash en limitant l'ouverture de processus

Ce qui prend du temps pour un script shell est le nombre de programmes appelés et donc, le nombre de processus créés.

Le meilleur moyen d’accélérer un script est de passer par les builtins et limiter les boucles ouverture/fermeture de programme.
En fait, dans certain cas, on pourrait avoir une commande qui lit sur l'entrée standard et retourne un résultat ; un peu comme bc. C'est là que les coprocess viennent à la rescousse :).

Un coprocess est un sous-shell exécuté de façon asynchrone et fournissant les tubes d'entrée/sortie. Ceux-ci sont accessibles via COPROC[1] et COPROC[2].

Dû coup, avec bc, la méthode est d'écrire dans un tube et lire dans l'autre. Comme la lecture est bloquante, le script va attendre que bc retourne le résultat.
Et voilà l'ouverture d'un programme remplacé par une lecture/écriture.

Par exemple, un petit programme qui lit un fichier contenant des opérations mathématiques et les affiches suivies du résultat.

#!/bin/bash
[ -z "$1" ] && echo $0: file >&2 && exit 1

#coprocess de bc avec la sortie d'erreur redirigée vers la sortie standard
coproc bc 2>&1

while read l; do
 echo "$l" >&${COPROC[1]} # écrire dans le coprocess
 read result <&${COPROC[0]} # réception du résultat
 echo $l = $result
done < "$1"

Sans coproc, la boucle est plus simple mais un processus sera ouvert pour chaque ligne du fichier, ce qui est particulièrement coûteux.

#sans coproc
while read l; do
 echo "$l" = `echo "$l" | bc 2>&1`
done < "$1"

À tester avec par exemple un fichier de calcul comme celui-ci

2+3
2+3*9
23*9
23s*9
3*9

mardi 21 mai 2013

printf vs cout vs boost::format vs tinyformat

Après diverses recherches et comparaisons entre printf et cout, j'ai décidé de faire mon propre benchmark en prenant en compte:

  • La norme utilisée (c++03, c++11)
  • La synchronisation des flux c++ avec ios_base::sync_with_stdio().
  • Le multithreading (option -pthread et inclusion de <thread> ou <pthread.h>).

J'en profite également pour tester et comparer avec boost::format et tinyformat.

Mes tests sont minimalistes et ne couvrent qu'une très petite partie des possibilités mais:

  • La norme utilisée et les threads n'influencent pas les résultats
  • Le fait de désynchroniser cout des streams C fait gagner en performance et ce de façon significative pour certains types.
  • Les performances varient selon le type. Par exemple, les flottants sont plus lents avec les flux c++ mais c'est l’inverse pour les caractères ou les chaînes.
  • boost::format est très lent mais possède l'avantage (ou pas) de mettre une à une les variables à formater. À vrai dire je n'ai pas regardé ce que proposait cette lib.
  • tinyformat est un peu plus lent que les flux C++ (tout dépend du format) mais est une alternative à printf. Tinyformat étant basé sur les flux du C++ la surcharge des flux fonctionne d'office.

Au final la différence entre printf et cout n'est pas importante et varie selon les types de variable, ce qui en rend délicat les mesures.
Cependant, l’avantage d'ostream est la surchargeable qui rend aisée l'ajout de nouveau type à écrire.
Et l'avantage de printf est la possibilité de fournir un format ce qui permet un meilleur contrôle dans l'affichage (ordre des paramètres, internationalisation, ...). Défaut de ostream qui peut être pallié via des libs spécialisées comme tinyformat ou boost::format.

Ci-dessous un tableau un peu brut qui donne des résultats en secondes de la médiane de 10 tests.
Plus lisible ici et mieux étaler ici. Pour finir, des graphiques en bars.
Code source est données: printf_vs_cout.tar.gz (~151 Ko) ou printf_vs_cout.zip (~223 Ko).

Comparaison (en secondes) de différentes APIs de manipulation de flux
format ostream printf boost::format tinyformat
format ostream printf boost::format tinyformat
cpp03
'%d' 0.078679 0.108698 0.464650 0.142462
'%d\n' 0.105090 0.113880 0.489037 0.159989
'%f' 0.463047 0.271212 0.993986 0.494874
'%f\n' 0.501310 0.278252 0.998719 0.505266
'%+6.4f\n' 0.437482 0.264044 0.999680 0.474117
'%c' 0.032680 0.010411 0.423434 0.101393
'%c\n' 0.065180 0.067260 0.444380 0.118883
'%s (chaîne constante)' 0.033918 0.022545 0.473357 0.107968
'%s (chaîne constante)\n' 0.034260 0.022663 0.474171 0.108528
'%s' 0.038505 0.102462 0.442303 0.112725
'%s\n' 0.038247 0.100166 0.440523 0.112081
'%20s\n' 0.199725 0.130075 0.478601 0.289536
'%d blah blah: %s%f\n' 0.637055 0.415993 2.256252 0.762320
cpp03.no_sync
'%d' 0.067002 0.109601 0.476751 0.147007
'%d\n' 0.100137 0.115022 0.492545 0.163944
'%f' 0.397226 0.271981 1.026970 0.503956
'%f\n' 0.441220 0.277866 1.043543 0.514723
'%+6.4f\n' 0.397340 0.262398 0.991949 0.493908
'%c' 0.025976 0.010431 0.447651 0.107648
'%c\n' 0.053353 0.067874 0.457830 0.114286
'%s (chaîne constante)' 0.027245 0.022667 0.457423 0.111958
'%s (chaîne constante)\n' 0.027467 0.022671 0.454262 0.112207
'%s' 0.035850 0.101713 0.450900 0.118588
'%s\n' 0.034884 0.100841 0.454078 0.118410
'%20s\n' 0.091354 0.130033 0.493873 0.188529
'%d blah blah: %s%f\n' 0.617792 0.415237 2.232278 0.799673
cpp03.thread
'%d' 0.076335 0.111163 0.483686 0.143798
'%d\n' 0.105287 0.115094 0.496408 0.160223
'%f' 0.417944 0.272958 0.987420 0.496637
'%f\n' 0.461985 0.279903 1.033756 0.510683
'%+6.4f\n' 0.390526 0.263052 0.980551 0.481614
'%c' 0.032338 0.010354 0.440483 0.103588
'%c\n' 0.065121 0.067937 0.455177 0.120682
'%s (chaîne constante)' 0.034495 0.022692 0.482209 0.110065
'%s (chaîne constante)\n' 0.034517 0.022687 0.479707 0.109049
'%s' 0.039811 0.102205 0.458460 0.113883
'%s\n' 0.039298 0.100060 0.455681 0.111497
'%20s\n' 0.198438 0.130106 0.493480 0.286004
'%d blah blah: %s%f\n' 0.614676 0.418298 2.101005 0.796899
cpp03.thread.no_sync
'%d' 0.068209 0.110208 0.484866 0.145173
'%d\n' 0.100651 0.115347 0.501651 0.161192
'%f' 0.380537 0.274508 0.980840 0.495117
'%f\n' 0.423242 0.279306 0.994300 0.512564
'%+6.4f\n' 0.381993 0.263379 0.963846 0.483052
'%c' 0.025535 0.010461 0.451770 0.106700
'%c\n' 0.051531 0.067833 0.461615 0.114352
'%s (chaîne constante)' 0.026337 0.022717 0.467188 0.110280
'%s (chaîne constante)\n' 0.026454 0.022715 0.468853 0.111218
'%s' 0.035278 0.102782 0.449293 0.117769
'%s\n' 0.034852 0.100985 0.454568 0.117806
'%20s\n' 0.089904 0.129334 0.528948 0.190800
'%d blah blah: %s%f\n' 0.604321 0.416814 2.101355 0.814668
cpp11
'%d' 0.076249 0.109847 0.468822 0.143676
'%d\n' 0.105597 0.116130 0.490844 0.159679
'%f' 0.461125 0.270193 1.009484 0.504168
'%f\n' 0.498769 0.277011 0.999355 0.521951
'%+6.4f\n' 0.422417 0.263603 0.951426 0.489394
'%c' 0.032439 0.062792 0.439045 0.101254
'%c\n' 0.064846 0.067247 0.457943 0.118911
'%s (chaîne constante)' 0.034257 0.100141 0.429901 0.108771
'%s (chaîne constante)\n' 0.034244 0.100071 0.423239 0.108540
'%s' 0.037396 0.102511 0.433076 0.109352
'%s\n' 0.040426 0.100593 0.432691 0.111921
'%20s\n' 0.200948 0.129657 0.457259 0.288542
'%d blah blah: %s%f\n' 0.612473 0.414612 2.107274 0.765568
cpp11.no_sync
'%d' 0.066652 0.110400 0.473127 0.146732
'%d\n' 0.100151 0.115625 0.497890 0.161555
'%f' 0.405104 0.271282 1.008395 0.511858
'%f\n' 0.441258 0.278531 1.023761 0.520053
'%+6.4f\n' 0.395064 0.263529 0.994490 0.503324
'%c' 0.027678 0.062949 0.441796 0.105975
'%c\n' 0.053462 0.067833 0.452007 0.113988
'%s (chaîne constante)' 0.030517 0.099791 0.438610 0.116545
'%s (chaîne constante)\n' 0.030432 0.099902 0.452633 0.116247
'%s' 0.032077 0.102299 0.435142 0.114287
'%s\n' 0.034866 0.100304 0.442736 0.117459
'%20s\n' 0.090916 0.130642 0.482727 0.187818
'%d blah blah: %s%f\n' 0.617891 0.416206 2.125498 0.787507
cpp11.thread
'%d' 0.079597 0.111012 0.490221 0.144572
'%d\n' 0.105356 0.116541 0.509951 0.161204
'%f' 0.419433 0.274043 1.062142 0.500665
'%f\n' 0.459403 0.280520 1.068277 0.516433
'%+6.4f\n' 0.387856 0.264889 1.016386 0.478026
'%c' 0.032373 0.062918 0.468587 0.102139
'%c\n' 0.065148 0.068547 0.503620 0.118624
'%s (chaîne constante)' 0.034375 0.099740 0.452058 0.108760
'%s (chaîne constante)\n' 0.034234 0.100634 0.442354 0.108973
'%s' 0.036803 0.102868 0.447704 0.110269
'%s\n' 0.039003 0.100901 0.457537 0.112071
'%20s\n' 0.198806 0.130172 0.474586 0.289830
'%d blah blah: %s%f\n' 0.604971 0.418086 2.188021 0.812509
cpp11.thread.no_sync
'%d' 0.067131 0.108851 0.479392 0.146475
'%d\n' 0.100783 0.115027 0.503999 0.163044
'%f' 0.388417 0.273822 1.025720 0.483775
'%f\n' 0.419818 0.280391 1.030705 0.493496
'%+6.4f\n' 0.382446 0.264789 0.988382 0.467702
'%c' 0.026136 0.062212 0.461026 0.106458
'%c\n' 0.052622 0.067718 0.464902 0.113045
'%s (chaîne constante)' 0.030520 0.100459 0.443730 0.117889
'%s (chaîne constante)\n' 0.030301 0.100305 0.443197 0.117772
'%s' 0.032202 0.102627 0.448466 0.114538
'%s\n' 0.034557 0.100783 0.449142 0.117577
'%20s\n' 0.091444 0.130085 0.486534 0.188111
'%d blah blah: %s%f\n' 0.609628 0.417566 2.116151 0.818492

mercredi 15 mai 2013

Référence constante sur référence

Une petite note sur les références et le qualificatif const en commençant par un exemple :).

typedef int& reference;
int i = 0;
const reference r = i;
r = 3; //possible car r est en réalité un int&

Comme le montre l'exemple, r est modifiable alors que const est présent. En fait, ajouter const sur une référence ne fait rien. Pareil pour volatile cela dit.

J'aurais pu mettre `int & const r` mais comme const ne peut s'appliquer directement sur une référence, j'ai dû ruser ;).

Si on considère les références comme des pointeurs constants non nuls (car c'est ce qu'elles sont) et en transformant le premier exemple, on obtient:

typedef int * const pointeur_constant;
int i = 0;
const pointeur_constant r = &i;

Donc
decltype(r) = pointeur_constant const = int * const const = int * const = pointeur_constant

À ce moment on se rend compte que le type de r n'est autre que pointeur_constant. const n'a fait qu'ajouter une information déjà présente, il n'a donc aucune utilité.

lundi 22 avril 2013

Placement new, allocateur et container

New est généralement utilisé pour allouer un bloc mémoire et −où il diffère de malloc(),− appelle le constructeur de la classe demandée (si constructeur il y a).
New fait donc deux choses en une.

En fait, new fait une troisième chose: il lance une exception std::bad_alloc si l'espace mémoire est insuffisant.
Pour qu'un pointeur nul soit retourné, il existe le type std::nothrow_t et la variable std::nothrow (tous 2 dans <new>) qui, conjointement avec new s'utilise ainsi:

Machin * machin = new (std::nothrow) Machin(/*params...*/);

Voici ce qui clôt son utilisation courante et voyons comment le faire en deux étapes au moins.

Allouer de la mémoire

Comme on veut juste réserver un espace mémoire, malloc() peut suffire mais prenons les bonnes habitudes, utilisons la fonction ::operator new() !

void * p = ::operator new(sizeof(Machin));

Ou la version sans exception.

#include <new>
//...
void * p = ::operator new(sizeof(Machin), std::nothrow);

Maintenant qu'on a un joli espace mémoire tout fraîchement alloué, construisons l'objet.

Placement new

Le placement new permet de construire un objet dans une zone mémoire prédéfinie et appelle le constructeur.

Machin * machin = new (p) Machin(/*params...*/); // machin == p (même zone mémoire)

Et maintenant que c'est construit, on détruit :D

Destructeur

Un appel explicite au destructeur et le tour est joué.

machin->~Machin();

Étape inutile pour les types scalaires. De toute façon ils n'ont pas de destructeur.

Évidemment, si machin possède un destructeur virtuel et est en réalité un objet héritant de Machin, alors c'est le destructeur de la classe fille qui est appelé.

Il ne reste plus qu'à libérer la mémoire.

Libération de la mémoire

Comme pour ::operator new (), il existe un ::operator delete() auquel il suffit de donner notre pointeur.

::operator delete (p);

Allocation de tableau

::operator new[] et ::operator delete[] fonctionnent de la même façon et sont plus sûrs qu'une gestion manuelle avec leurs homologues sans crochets. Ne serait-ce que pour éviter les fuites mémoire.

void * p = ::operator new [] (sizeof(Machin) * n); //identique que la version sans crochet
Machin * machin = new (p) Machin[n]/*{params...}*/; //si un des constructeurs jettes une exception alors le destructeur des objets construit sera automatiquement appeler.

Surcharge de new et delete

Toutes les formes de new et delete sont surchargeables de façon locale ou globale. Local quand l'opérateur est implémenté à l'intérieur d'une classe (son prototype sera implicitement statique) et global lorsqu'implémenté dans le namespace global.

De plus, comme le new peux prendre des paramètres, il est possible de les personnaliser et d'en ajouter.

#include <new>
struct A{
 A(int i=0)
 {std::cout << "A("<<i<<")" << std::endl;}

 void * operator new (std::size_t size, int x, int y) throw(std::bad_alloc)
 {
  std::cout << "new A " << x << ' ' << y << std::endl;
  return ::operator new (sizeof(A));
 }
};

new(1,2) A;//affiche "new A 1 2 A(0)"

L'alignement mémoire

L'alignement mémoire est une histoire à part entière, je n'en parle donc pas ^^.
Toutefois, renseignez-vous dessus, des variables non alignées peuvent faire chuter les performances et planter certaines architectures de processeur.
Présent dans boost et la dernière norme du C++, il existe aligned_storage et co pour aider dans l'alignement.

Allocateurs

Les allocateurs sont des objets qui s'occupent de faire tout ce qui a été dit auparavant à travers des méthodes comme allocate/desallocate, construct/destroy, address et max_size mais sans faire de surcharge. En fait tous les containers de la std utilisent un std::allocator.

Ce qui m’amène au dernier point, les containers.

Allocateurs et containers

Chaque container (ou presque), que ce soit des std::string, des std::vector ou encore des std::list possèdent tous un allocateur. Son type se définit en dernier paramètre de template.
Évidemment, l'allocateur peut être personnalisable et dans certaines circonstances permet un gain de performance en évitant l'allocation/dés-allocation répéter.

Par exemple, il y quelques semaines, j'ai fait un algorithme qui faisait au total 2'100'000 new pour au final ne garder que 100'000 objets. Donc 2'000'000 de delete.
Dans le pire des cas, il y avait une suite de 25 objets à supprimer. Avec un allocateur qui ne vide pas la mémoire mais garde un tableau des pointeurs alloués je n'avais plus qu'à faire 25 dés-allocations au lieu de 2'000'000 et le nombre de new effectuées descendait quant à lui à 100'025.
Seul le nombre d'appels au destructeur et au placement new restait inchangé. Respectivement 2'000'000 et 2'100'000.

Au final l'algorithme était quand même 30% plus rapide :).

Ce type d'optimisation reste toutefois exceptionnel et n'est pas adapté à tous les containers. Par exemple, std::vector se prête mal à ce genre d'exercice car il demande toujours une allocation d'au moins la taille du nombre d'éléments qu'il possède.
Par contre, les containers comme std::list ou std::map, qui allouent toujours un seul élément à la fois sont un meilleur choix pour utiliser ce type d'allocateur.
Cependant, comme les containers retournent une copie de leur allocateur, il sera difficile de supprimer de manière simple la mémoire non utilisée par l'allocateur du container.

En ce moment, j'ajoute plusieurs allocateurs dans falcon/memory, même si celui dont je viens de parler n'est pas encore présent car son implémentation était vraiment basique et spécifique, il fait quand même partie de ma todo-list.

samedi 23 mars 2013

Nouveau mode pour fopen

Depuis le temps que j’essayais de me rappeler ce nouveau mode. Je ne regardais juste pas la bonne doc. Mais le tir est corrigé, maintenant il est aussi là \o/.

Mais quelle est donc ce nouvel arrivant tout droit sorti du c11 ? Un simple inconnu, le mode "x" :D.
Ce nouveau mode (qui se met en dernier) permet quand il est avec le mode "w" de créer le fichier s'il n'existe pas et d'échouer dans le cas contraire.
Ça fonctionne comme les flags O_CREAT et O_EXCL de la fonction open.

Que de complexité, que de souffrance et que de temps perdu résumés en un simple "x"...

Par contre, je n'ai pas trouvé l'équivalent en C++ pour std::fstream.

dimanche 17 mars 2013

Ne pas empêcher la NRVO

La NRVO et la RVO sont des optimisations des compilateurs pour retourner un objet sans le copier. Je renvoie directement sur une partie de la FAQ C++ de développez.com: d'ici jusqu'à la fin.

Cependant ces optimisations ne s'appliquent pas lorsqu'un appel de méthode qui retourne son instance est directement retourné (une référence sur elle-même).
Ceci:

S f()
{
  S ret;
  return ret.action(); //S& action() { /*...*/ return *this; }
}

Ou cela:

iterator operator+(const iterator& other, int n)
{ return iterator(other) += n; }

Dans ces 2 exemples l'objet est copié inutilement. Pour éviter cette copie, il faut décomposer le code pour que le retour de la fonction ne soit pas lié à une méthode: il faut retourner explicitement l'objet. Ainsi la variable de retour est une variable nommée et la NRVO s'applique.

S f()
{
  S ret;
  ret.action();
  return ret;
}

Et cela:

iterator operator+(const iterator& other, int n)
{
  iterator ret(other);
  ret += n;
  return ret;
}

C'est une optimisation facile à faire et il est tout aussi facile de passer à côté ;).

samedi 9 mars 2013

Petite curiosité de volatile

Voici une petite curiosité du mot clef volatile.

#include <iostream>

struct S{};

void f(void*) { std::cout << 'p'; }
void f(bool) { std::cout << 'b'; }

int main()
{
    S s;
    volatile S * p1 = &s;
    S * p2 = &s;
    f(p1);
    f(p2);
}

Résultat: bp

Le pointeur volatile passe dans la fonction demandant un booléen. Ce n'aurait pas été le cas si un prototype avec volatile void * ou templater sur un _T* existait.

lundi 4 mars 2013

Sqlite, reconstruire la bdd pour l'alléger.

Je ne vais pas par 4 chemins : VACUUM.

Cette commande permet de reconstruire une table et ainsi éliminer les lignes vides et réorganise les index (c'est mieux dit dans la doc ^^).
Certains logiciels se servent de sqlite ; utiliser cette commande de temps en temps sur les tables est une bonne idée.
La première fois que je l'ai fait pour firefox (fichier ~/.mozilla/firefox/nom-du-profil/*.sqlite sur linux) j'ai gagné ½ giga :).

Voici un petit script qui va permettre de le faire sur tous les fichiers sqlite du système (du moins, ceux indexés) et connaître la taille totale avant et après utilisation:

#!/bin/sh
tmpf=/tmp/sqlite_file_path
locate \.sqlite \
| xargs -d'\n' mimetype \
| grep 'application/x-sqlite3$' \
| sed 's/:\s*application\/x-sqlite3\s*$//' \
> $tmpf
xargs --arg-file $tmpf -d'\n' du -hc | tail -n1
while read f ; do
 sqlite3 "$f" 'VACUUM;'
 [ $? -ne 0 ] && echo "\tfor $f"
done < $tmpf
xargs --arg-file $tmpf -d'\n' du -hc | tail -n1
rm $tmpf

Une petite version pour cibler les fichiers:

#!/bin/sh
for f in "$@" ; do sqlite3 "$f" 'VACUUM;'; done

mercredi 27 février 2013

Faites parler votre compilateur

Voici la liste de mes alias pour compiler des codes C, C11, C++ et C++11 et ainsi éviter de nombreuses erreurs de codage et avoir un maximum d'info sur les potentielles erreurs. Les flags les plus importants étant quand même -Wall, -Wextra et -Werror pour transformer les avertissements en erreurs.

alias gcc1x='gcc -std=c1x'
alias g0x='g++ -std=c++0x'
alias g11='g++-4.7 -std=c++11'

alias colorgcc1x='colorgcc -std=c1x'
alias colorg0x='colorg++ -std=c++0x'
alias colorg11='colorg++-4.7 -std=c++11'

flag='-Wall -Wextra -Wundef -Wcast-align -Wformat-security -Wunreachable-code -Wformat=2 -Werror-implicit-function-declaration -Wfloat-equal -Wshadow -Wpointer-arith -Wconversion -Wmissing-declarations -Wmissing-noreturn -Wmissing-format-attribute -Wpacked -Wredundant-decls -Winline -Wdouble-promotion -Winit-self -Wcast-qual -pedantic'
cflag=$flag' -Wstrict-prototypes -Wbad-function-cast -Wmissing-prototypes -Wnested-externs -Waggregate-return -Wwrite-strings'
cppflag=$flag' -Wold-style-cast -Woverloaded-virtual -Wnon-virtual-dtor'
unset flag
alias gwcc="gcc $cflag -Wlong-long"
alias gwcc1x="gcc $cflag -std=c1x"

alias colorgwcc="colorgcc $cflag -Wlong-long"
alias colorgwcc1x="colorgcc $cflag -std=c1x"

unset cflag

alias gw++="g++ $cppflag -Wlong-long"
alias gw0x="g++ $cppflag -std=c++0x"
alias gw11="g++-4.7 $cppflag -std=c++11"

alias colorgw++="colorg++ $cppflag -Wlong-long"
alias colorgw0x="colorg++ $cppflag -std=c++0x"
alias colorgw11="colorg++-4.7 $cppflag -std=c++11"

unset cppflag

alias gfcc='gcc -Wfatal-errors'
alias gf++='g++ -Wfatal-errors'
alias gfcc1x='gcc1x -Wfatal-errors'
alias gf0x='g0x -Wfatal-errors'
alias gfwcc='gwcc -Wfatal-errors'
alias gfwcc1x='gwcc1x -Wfatal-errors'
alias gfw0x='gw0x -Wfatal-errors'

alias colorgfcc='colorgcc -Wfatal-errors'
alias colorgf++='colorg++ -Wfatal-errors'
alias colorgfcc1x='colorgcc1x -Wfatal-errors'
alias colorgf0x='colorg0x -Wfatal-errors'
alias colorgfwcc='colorgwcc -Wfatal-errors'
alias colorgfwcc1x='colorgwcc1x -Wfatal-errors'
alias colorgfw0x='colorgw0x -Wfatal-errors'

Ma version de gcc et la 4.6 par défaut mais la 4.7 (que j'ai aussi) a un meilleur support du C++11.

colorgcc se trouve dans les paquets (d'ubuntu) mais il faudra un patch pour avoir colorg++ (facilement trouvable sur le net).

Je compile généralement mes tests en ajoutant -Wno-missing-declarations pour permettre d'éliminer les warnings lorsque la fonction n'a pas de prototype ou n'est pas inline.

vendredi 22 février 2013

Nouveauté sur les unions

Avant le C++11 les types mis dans les unions se limitaient aux objets POD.
Maintenant plus de limite, faut juste bien appeler le destructeur de l'objet construit dans l'union au risque de jolie fuite mémoire :).

union U{
  int i;
  std::string s;

  U():i(){} //constructeur obligé car s n'est pas un objet POD
  ~U(){} //destructeur qui ne fait rien mais aussi obligé

  void construct_s(const std::string& str)
  { new (&s) std::string(str); } //inclure <memory>

  void destroy_s()
  { s.~basic_string(); } //note string = basic_string<char>
};

Comme tel, c'est un peu limité mais c'est pour montrer...

dimanche 17 février 2013

Différence entre $@, $*, "$@" et "$*"

Après avoir vu l'utilisation de $@ dans un script shell, je me suis demandé la différence avec $*.
Pour rappel, ces variables représentent les arguments de la ligne de commande.

$* est une variable ce qu'il y à de plus normale et ne diffère pas d'une autre variable. Cependant, le comportement des variables diffère en fonction du shell, notamment sur zsh (j'y reviens après).
$@ est une variable au comportement différent entre les shells basés sur sh (bash,dash,...) et les autres (ksh, zsh).

Sur sh aucune différence entre $@ et $*. Par contre sur ksh/zsh cela représente le `tableau` d'argument. Il faut savoir qu'une variable sans guillemet revient à créer autant d'arguments qu'il y a de mots. Les mots sont séparés en fonction des caractères de $IFS (la variable contient : espace, tabulation et saut de ligne). C'est pour ça qu'il est conseillé d'entourer ces variables de guillemets doubles.

Un petit exemple pour comprendre :).

$ a='a b c'; for v in $a ; do echo $v ; done
> a
> b
> c

Le résultat est 3 lignes pour 1 paramètre, la chaîne 'a b c' contenant des espaces s'est fait couper. Ce n'est pas le cas pour zsh (raison historique, j'y reviens à la fin).

Maintenant il reste "$*" et "$@" qui ne diffère pas entre les shells.
"$*" correspond à une seul chaîne, tout est gérer en un seul bloc.
"$@" représente les paramètres réel. C'est identique à $@ avec ksh et zsh.

Petit tableau récapitulatif avec le même type de boucle qu'au-dessus et un appel avec deux paramètres.

./test 'a b' c
var/shell sh/bash ksh zsh
$*
> a
> b
> c
> a
> b
> c
> a b
> c
$@
> a
> b
> c
> a b
> c
> a b
> c
"$*"
> a b c
> a b c
> a b c
"$@"
> a b
> c
> a b
> c
> a b
> c

Pour que zsh boucle sur des mots, il faut ajouter un flag à la variable (${=*}), comme ci-dessous.

$ a='a b c'; for v in ${=a} ; do echo $v ; done

mercredi 6 février 2013

Appel explicite aux membres d'une classe

Ce qui suit permet d'appeler les attributs/fonctions cachés du fait d'un héritage ou faire appel à la fonction virtuelle d'un parent même si celle-ci est redéfinie dans un fil.

Ce principe est plus connu à l'intérieur d'une classe est utilisé avec l’héritage:

struct A
{
 virtual int f() { return 3; }
 virtual ~A(){}
};

struct B : A
{
 virtual int f()
 { return A::f() + 3; } // Utilise la fonction f() du parent
};

Mais il peut aussi s'utiliser à l'extérieur d'une classe. Le A::f() fait référence à une fonction de l'objet lié au pointeur this. Donc this->A::f() est syntaxiquement valide. Ce qui veut dire qu'on peut faire référence à n'importe quel membre contenu dans l'objet.

struct A { int n = 2; };
struct B : A { int n = 3; };

B b;
b.n == 3
b.A::n == 2
struct C { virtual int f(){ return 2; } };
struct D : C { int f(){ return 3; } };

D d;
b.f() == 3
b.C::f() == 2

Cette technique est utilisé pour par exemple changer le buffer d'un fstream. La méthode std::ios::rdbuf(std::streambuf*) étant cacher par std::ofstream::rdbuf() c'est un moyen plus simple que le cast en une référence sur std::ios.

std::ofstream os("...");
//...
static_cast<std::ios&>(os).rdbuf(buf);
//ou
os.std::ios::rdbuf(buf);