jeudi 30 août 2018
Le blog déménage
lundi 4 avril 2016
Comment se passer de std::forward.
Actuellement en pleine écriture d'une fonction match (petit projet de 200 lignes actuellement), je me retrouve, pour respecter le perfect forwarding, avec une armée de std::forward utilisée à chaque appel de fonction. J'en compte 21 pour un total de 6 niveaux d'imbrications. Autant dire qu'oublier de le mettre est plutôt facile.
Pour réduire son utilisation et alléger le code, je me suis fait un petit wrapper qui sauvegarde le type de référence. Comme l'objet créé a pleinement connaissance du type de variable qu'il contient et que traverser une chaîne de fonctions ne fait pas disparaître cette information, il n'y a plus besoin de std::forward. Le dernier maillon peut alors extraire une lvalue ou une rvalue via -par exemple- une fonction `get()`.
Pour illustrer, voilà ce que cela donne:
template<class T>
struct forwarder
{
T & x;
T && get() const { return static_cast<T&&>(x); }
};
template<class T>
struct forwarder<T&>
{
T & x;
T & get() const { return x; }
};
#include <iostream>
template<class T> void print(T &&) { std::cout << "&&\n"; }
template<class T> void print(T &) { std::cout << "&\n"; }
// dernier niveau d’imbrication avant l'appel de la vrai fonction
// note: le coup d'une copie est nulle pour le type forwarder
template<class T> void f1(T x) { print(x.get()); }
// pas besoin de std::forward
template<class T> void f2(T x) { f1(x); }
// premier maillion, création du contexte
template<class T> void foo(T && x) { f2(forwarder<T>{x}); }
int main() {
int i = 1;
int const ci = 2;
foo(i); // &
foo(ci); // &
foo(3); // &&
foo(std::move(i)); // &&
foo(std::move(ci)); // &&
}
Avec 2 fonctions d'accès par référence en plus (ref et cref pour la version const) on possède une petite classe qui simplifie pas mal la vie.
lundi 26 octobre 2015
[C++] Filtrage par type
Exécuter du code dépendant du type des entrées est le principe même de la surcharge. La procédure est plutôt simple pour les types concrets, mais devient rapidement compliquée quand des templates interviennent avec des priorités de concept.
Pour exemple, une fonction foo() spécialisée dans l'ordre pour les types flottants, signés et non-signés requière 3 prototypes de la forme:
enable_if_t< is_floating_point_v<T> > foo(T);
enable_if_t< ! is_floating_point_v<T> && is_signed_v<T> > foo(T);
enable_if_t< ! is_floating_point_v<T> && ! is_signed_v<T> > foo(T);
Comme is_signed inclut également les nombres flottants, il faut les exclure pour empêcher tout appel ambigü, ce qui tend le prototype à grandir inexorablement.
Une première approche consiste à la création d'une hiérarchie de fonctions.
- is_floating_point foo() - ! is_floating_point foo() \- is_signed foo_with_integral() \- ! is_signed foo_with_integral()
Ce qui accroît le nombre de fonctions et n'est pas vraiment plus lisible.
Les autres solutions passent par les tags, la spécialisation de template et d'autres que j'oublie peut-être. Au final, la problématique est très proche de l'article précédent concernant static_if.
La solution aussi :D.
Le plus simple consiste en un pattern chaîne de responsabilité appliqué aux protypes de fonction.
Pour rappel, ce pattern permet d'exécuter la première fonction qui répond à une demande. Ici, notre demande sera: cette fonction peut-elle être appelée avec tel type de variable ?
// foo(T xxx)
select(
xxx,
if_<std::is_floating_point>([](auto & x) { ... }),
if_<std::is_unsigned>([](auto & x) { ... }),
if_<std::is_signed>([](auto & x) { ... }),
// et plus
[](std::string const & x) { ... },
[](auto & x) -> decltype(x.foo()) { ... },
// ...
[](auto & x) { static_assert(decltype(void(x),1){},
"Oups, il n'y a pas de correspondance :/"
); }
);
// Implémentation (présentation de la méthode: ici)
template<template<class> class Tpl, class F>
struct case_tpl
{
F f;
template<class T>
auto operator()(T && x, std::enable_if_t<Tpl<std::decay_t<T>>{}>* = 0) const
{ return f(std::forward<T>(x)); }
};
template<template<class> class Tpl, class F>
case_tpl<Tpl, F> if_(F && f) {
return {f};
}
template<class T>
void select_impl(int, T &) {}
template<class T, class Fn, class... Fns>
auto select_impl(int, T & x, Fn & fn, Fns & ...)
-> decltype(fn(x)) {
return fn(x);
}
template<class T, class Fn, class... Fns>
auto select_impl(unsigned, T & x, Fn &, Fns & ... fns) {
return select_impl(1, x, fns...);
}
template<class T, class... Fns>
auto select(T && x, Fns && ... fns) {
return select_impl(1, x, fns...);
}
On peut même faire une implémentation qui utilise la surcharge et ainsi avoir une garantie forte que seule une fonction existe pour un type donné.
Avec les inconvénients concernant les appels ambigüs ;).
template<class T, class Fn>
auto match(T & x, Fn f) {
return f(x);
}
template<class T, class Fn1, class Fn2, class... Fns>
auto match(T & x, Fn1 && fn1, Fn2 && fn2, Fns && ... fns) {
struct F : Fn1, Fn2 {
using Fn1::operator();
using Fn2::operator();
F(Fn1 f1, Fn2 f2) : Fn1(f1), Fn2(f2) {}
};
return match(F{fn1,fn2}, fns...)
}
lundi 21 septembre 2015
[c++] static_if
Le but de `static_if` est de ne pas vérifier la validité d'un code dans la phase de compilation. Le code doit être syntaxiquement valide, mais n'a pas l'obligation de pouvoir être compilé.
Plutôt étrange n'est-ce pas ? Cette propriété se révèle pourtant fort pratique dans les fonctions templates. Prenons comme exemple relativement bidon une fonction `foo(T & array)` qui vérifie si `array.size()` est au moins à 4 et, dans le cas contraire, agrandit le container avec `array.resize(4)`.
// pour T[N]
if (std::is_array<T>{})) {
static_assert(std::extent<T>{} >= 4, "");
}
// pour std::vector
else {
array.resize(4);
}
Le problème ici: `static_assert` est toujours exécuté et `resize()` toujours évalué. Résultat, le code ne compile jamais, car `resize()` n'existe pas pour les tableaux et `size()` n'est pas `constexpr` pour `std::vector`.
Évidemment, `static_if` résout le problème. Dit comme cela, on pourrait croire que c'est un nouveau mot clef du langage, mais non. (Il eut une proposition dans ce sens, mais celle-ci fut rejetée.) Alors comment faire ?
Alternatives au static_if
En réalité, il existe plusieurs alternatives utilisées, et ce, bien avant C++11.
Trait et tag
Parmi celles qui font le sujet d'un de mes articles: les 'tags', souvent associés à un type_trait pour l'extraction.
Sur le principe, un type catégorie est extrait d'un paramètre et propagé à des fonctions surchargées sur la catégorie désirée. Malheureusement, cette approche disperse le code et rend la lecture difficile pour des besoins spécifiques et ponctuels. L'autre inconvénient est la transmission du scope de variable qui s'avère délicat (c'est une union (dans le sens mathématique) de toutes les variables utilisées par l'ensemble des surcharges).
Spécialisation de template
Sans plus m'étendre sur cette technique, le principe est exactement le même que pour les tags, sauf que l'on remplace la surcharge de fonction par de la spécialisation de template.
Classes internes sélectionnées avec std::conditional
La classe interne (comme utilisée dans l'article précédent) permet un code monolithique, mais n'a pas de conséquence sur le scope de variable, il faut toujours le propager. Sa lourdeur d'écriture est aussi un peu rédhibitoire.
Lambda
Les lambdas permettent de capturer le scope et se définissent n'importe où. Mais un problème survient, comment empêcher l'évaluation de la lambda si la condition n'est pas respectée ?
Depuis C++14, il existe les lambdas polymorphes. Celles-ci permettent l'usage de template à travers un type `auto`. Comme les templates ne sont évaluées qu'à leur appel, on peut faire du branchement conditionnel à la compilation en utilisant de la surcharge sur `std::true_type` et `std::false_type`. Au final, static_if devient un appel de fonction.
#include <type_traits>
class dummy { dummy(){} void operator()() const {} };
template<class TrueFn, class FalseFn = dummy>
auto static_if(std::true_type, TrueFn yes_fn, FalseFn const & = FalseFn{}) {
return yes_fn(std::true_type{});
}
template<class TrueFn, class FalseFn>
auto static_if(std::false_type, TrueFn const &, FalseFn no_fn) {
return no_fn(std::false_type{});
}
template<class T>
void foo(T & array) {
static_if(std::is_array<T>{}, [&](auto){
static_assert(std::extent<T>{} >= 4, "");
},
/*else*/[&](auto){
array.resize(4);
});
}
#include <vector>
int main()
{
{
std::vector<int> a;
foo(a); // ok
}
{
int a[5];
foo(a); // ok
}
{
int a[2];
foo(a); // static_assert
}
}
J'ai mis une valeur arbitraire pour le paramètre envoyé aux lambdas, on pourrait aussi le rendre optionnel en utilisant la méthode présentée dans cet article.
L'implémentation de static_if ne se prête pas très bien à l'imbrication. Une autre manière de faire, passe par une classe et du chaînage `static_if(...).else_if(...)`. C'est un peu plus compliqué si on veut un type de retour et on ne pourra pas utiliser de AAA sans appeler explicitement une fonction `eval()`.
auto xxx = static_if(..., []{ return 1; }).else_([]{ return 2; });
// decltype(xxx) != int (définit par l'implémentation de static_if)
auto xxx = static_if(..., []{ return 1; }).else_([]{ return 2; }).eval();
// decltype(xxx) = int
Je pense qu'il vaut alors mieux se tourner vers une forme proche du switch.
static_switch(xxx
, case_<std::is_array>() = [&](auto){ static_assert(std::extent<T>{} >= 4, ""); }
, case_<yyy>() | case_<zzz>() = [&]{ ... }
, default_ = []{ ... }
);
Que je laisse à la discrétion de chacun ;)
mardi 28 juillet 2015
Paramètres de fonction nommés en C++
Cet article est la démonstration de l'article précédent. La problématique présentée est la suivante: "Comment, dans une fonction avec plusieurs paramètres optionnels, initialiser un paramètre précis sans indiquer les valeurs optionnels qui précèdent ?"
La fonction de référence sera la suivante:
void draw_rect(
unsigned w, unsigned h
, char border_top = '-', char border_bottom = '-'
, char border_left = '<', char border_right = '>'
, char fill = '#'
) {
std::cout << std::setfill(border_top) << std::setw(w+2) << "" << "\n";
while (h--) {
std::cout << border_left << std::setfill(fill) << std::setw(w) << "" << border_right << "\n";
}
std::cout << std::setfill(border_bottom) << std::setw(w+2) << "" << "\n";
}
Comment faire un appel proche de `draw_rect(4,3, fill='@')` ?
Création d'un paramètre nommé.
La première étape consiste à créer un type par paramètre optionnel. Comme je n'ai pas envie de me compliquer la vie, la syntaxe `fill='@'` qui demande plus de code sera remplacer par un simple appel de constructeur `fill{'@'}`.
La définition des types devient alors véritablement simpliste:
struct border_top { char value; };
struct border_bottom { char value; };
struct border_left { char value; };
struct border_right { char value; };
struct fill { char value; };
Adapter draw_rect.
Au lieu d'adapter draw_rect, je vais passer par une surcharge, ceci n'impactera pas le résultat.
La nouvelle fonction doit pouvoir prendre les nouveaux types, mais pas forcement tous et de préférence dans un ordre indéfini.
On pourrait faire toutes les surcharges possibles, il n'y a "que" plus d'une centaine de possibilités après tous... Solution rejetée, évidemment ;).
Une variadique template fera l'affaire.
template<class... Ts>
void draw_rect(unsigned w, unsigned h, Ts... params);
Il reste maintenant à associer chaque type de `params` avec le paramètre de notre premier prototype de draw_rect.
Distribution des paramètres.
C'est là qu'intervient le magasin de type de l'article précédant (toujours pas trouvé de meilleur nom).
Le principe est simple, toutes les valeurs de params sont regroupées sous une même enseigne appelé ici 'pack'. On vérifie si le pack est convertible en un type voulu et dans le cas contraire, on utilise une valeur par défaut.
Notre pack ressemble à ça:
struct Pack : Ts... {
Pack(Ts... args) : Ts(args)... {}
} pack{params...};
Et la distribution des paramètres se fait ainsi:
draw_rect(w, h
, getval<border_top>(pack, '-')
, getval<border_bottom>(pack, '-')
, getval<border_left>(pack, '<')
, getval<border_right>(pack, '>')
, getval<fill>(pack, '#')
);
Pour les plus attentifs (il m'a fallu 2 jours pour le réaliser xD), rien n'empêche d'envoyer des paramètres inutiles, on peut l'empêcher grâce à un static_assert avec une condition style `std::is_convertible_t<border_top>() + std::is_convertible<border_bottom>() + /*etc*/ == sizeof...(Ts)`.
Dans l'histoire, bien que largement surmontable, getval est la fonction la plus compliquée. Si Pack est convertible en T alors Get::get() est utilisé, sinon Default::get().
template<class T, class Pack>
char getval(Pack & pack, char default_) {
struct Get { static char get(T item, char ) { return item.value; } };
struct Default { static char get(Pack &, char default_) { return default_; } };
return std::conditional<std::is_convertible<Pack, T>::value, Get, Default>::type
::get(pack, default_);
}
Conclusion
Je trouve la technique du 'pack' très adapté pour les paramètres optionnel. Contrairement à l'alternative hyper lourdingue via fonctions récursives et 2 tuples (un des paramètres reçus + un des paramètres triés), getval() est plutôt limpide. Aspect suffisamment rare dans le domaine de la méta-programmation pour être souligné ^^.
Les choses se compliquent quand on veut récupérer un type partiel d'un pack. Un std::vector<T> par exemple. Actuellement, je n'ai pas de solution aussi simple qu'utiliser is_convertible. J'espère dans les prochains jours trouver une solution et l'ajouter à Falcon.Store.
Pour aller plus loin dans la voie des paramètres nommés, il existe Boost.Parameters.
jeudi 2 juillet 2015
Implémentation d'un magasin de type
Ce que j'appelle ici un magasin de type n'est autre qu'un std::tuple où les types ne sont présents qu'une seule fois. Une espèce de set version tuple en somme.
Je me suis servi de ce type de structure à 2 reprises.
Une fois pour manipuler de façon similaire des types hétérogènes sans la lourdeur de std::tuple. Il faut dire aussi que j'étais en C++11 et que dans cette norme std::get<Type>() n'existe pas.
L'autre fois dans une fonction variadique qui distribue les valeurs vers différentes fonctions. Le but étant de ne pas se soucier de l'ordre des paramètres, certains étant optionnels.
std::tuple fait plutôt bien le boulot, mais possède plusieurs inconvénients pour ce cas de figure.
- Aucune erreur de compilation si un type est présent 2 fois (et c'est normal pour un tuple).
- Prend beaucoup de mémoire et de temps de compilation (osef !)
Je pourrais faire l'apologie de RapidTuple histoire de me faire mousser (le projet contient un tuple_set), mais en fait non, on peut faire encore plus simple en 10 lignes de code :).
Bon ok, 3 lignes.
Mais 10 pour rendre pratique ;).
Planter la compilation quand un type est en doublon.
Le C++ dispose déjà d'un mécanisme interne qui vérifie et hurle au scandale si un type doublon existe. J'ai nommé l'héritage.
Seulement, un héritage direct n'est pas possible avec les types scalaires, il faut un intermédiaire.
template<class T> struct item { T x; };
template<class... Ts>
struct store : item<Ts>... {};
Avec cette implémentation, des petits malins pourraient faire de la pseudo-duplication de type en y ajoutant des qualificatifs, store<int, int const> par exemple.
On peut être tolérant ou devenir un tyran sans pitié en empêchant cela.
template<class... Ts>
struct tyrannical_store_impl : store<std::remove_cv_t<Ts>...> {
using type = store<Ts...>;
};
template<class... Ts>
using tyrannical_store = typename tyrannical_store_impl<Ts...>::type;
Le store tyrannique est construit en 2 étapes, car un alias direct sur un store épuré ne permet pas de garder les qualificatifs.
Piocher dans le magasin.
Piquer un élément du magasin est une affaire de cast. Un simple static_cast.
store<int, char> my_store;
static_cast<item<int>&>(my_store).x;
En mettant des opérateurs de cast dans la classe item, plus besoin de préciser cette dernière avec le static_cast.
template<class T> struct item {
explicit operator T & () noexcept { return x_; }
explicit operator T const & () const noexcept { return x_; }
private:
T x_;
};
store<int, char> my_store;
static_cast<int&>(my_store);
Petit bémol toutefois, cela ne permet pas d'enlever l'ambiguïté pour un type qui diffère uniquement par son qualificatif.
store<int, int volatile> my_store;
static_cast<int volatile&>(my_store); // 'store<int, volatile int>' to 'volatile int&' is ambiguous
Ce qu'il manque
- Les constructeurs, évidemment.
- Une fonction
get<Type>()pour un parallèle avec la STL. - Une fonction pour boucler sur chaque item (apply_from_store ?).
- Et sûrement d'autres.
J'ai mis tout ça dans un repo au nom provisoire (falcon.store).
samedi 16 mai 2015
Délégation d'appel de fonctions utilitaires.
Cet article fait suite à l'utilisation de swap et des fonctions utilitaires en général.
L'article précédemment cité montre pourquoi il faut exporter avec using la fonction utilitaire généraliste avant de l'utiliser. La nécessité de "déplacer" les fonctions dans le scope est lourd et facile à oublier.
L'idéal serait la présence d'une unique fonction en charge d'appeler la bonne surcharge. Heureusement, une solution existe. Elle consiste en la création d'un namespace dans lequel la fonction générale est exportée et où une fonction intermédiaire l'appelle. Comme la règle d'adl s'applique, la fonction surchargée sera appelée si existante.
#include <algorithm>
namespace fn {
namespace {
using std::swap;
template<class T>
void swap_impl(T & a, T & b)
{ swap(a,b); }
}
template<class T>
void swap(T & x, T & y)
{ ::fn::swap_impl(x,y); }
}
Cependant, cette solution souffre de l'effet inverse: la fonction ne peut pas être déplacée dans le même scope qu'une fonction généraliste. Il y aurait 2 prototypes identiques, le compilateur ne pourrait pas lever l’ambiguïté. Comme il est très courant de voir `using namespace std;`, un `using fn::swap` dans le même scope rentrerait en conflit avec std::swap lors de l'appel (aucun problème si dans un sous-scope).
#include <iostream>
namespace my {
struct A{};
void swap(A&, A&)
{ std::cout << "ok\n"; }
}
int main()
{
{
my::A a, b;
fn::swap(a,b); // affiche ok
}
{
int a, b;
fn::swap(a,b); // compile
}
{
using fn::swap;
using std::swap;
int a, b;
swap(a,b); // ambiguïté
}
{
using std::swap;
{
using fn::swap;
my::A a, b;
fn::swap(a,b); // affiche ok
}
}
}
dimanche 30 novembre 2014
Appel conditionnel de fonction selon la validité d'une expression.
Il existe une méthode à base de trait pour changer le comportement d'une fonction. Je l'ai déjà présenté dans un article plus ancien en prenant comme exemple les itérateurs et la fonction std::advance: "taguer vos classes, cataloguées-les".
L'approche suivante consiste non plus à définir des types internes ou des traits mais vérifier qu'une fonction (membre ou statique) est appelable. De manière plus générale, la méthode présentée ici s'applique à toutes expressions.
Appeler T::sort si possible, sinon std::sort(begin(T), end(T))
L'exemple va se faire sur la classe std::list qui n'est pas triable avec std::sort mais possède une fonction membre sort(). Ainsi que sur std::vector qui, inversement, n'a pas de fonction membre sort() mais est triable avec std::sort.
La méthode est simple et consiste à créer 2 fonctions: une pour vérifie si une expression est valide (ici x.sort()) et une autre en cas d'échec.
Seulement, qui dit 2 fonctions dit 2 prototypes. Leur prototype doit être légèrement différent mais compatible avec les mêmes valeurs d'entrée pour appeler la seconde si la première échoue.
Pour vérifier l'expression, seul 2 mots clef existent: sizeof et decltype. Cette procédure est donc possible avant C++11, même si sizeof requière un peu d'enrobage.
#include <algorithm>
//avec decltype
template<class Container>
auto dispatch_sort(int, Container & c)
-> decltype(void(c.sort())) //force decltype au type void
{ c.sort(); }
template<class Container>
void dispatch_sort(unsigned, Container & c)
{
using std::begin;
using std::end;
std::sort(begin(c), end(c));
}
template<class Cont>
void sort(Cont & c)
{ dispatch_sort(1,c); }
La fonction sort appel dispatch_sort avec un int (la valeur n'importe pas, seul le type compte). Comme la seule différence des 2 fonctions dispatch_sort est le premier paramètre, le prototype avec un int correspond parfaitement.
Si une fonction membre sort existe alors l'expression dans decltype est valide et la fonction est appelé. Dans le cas contraire, le compilateur cherche une fonction avec des paramètres pouvant être convertit. Le int pouvant être convertit en unsigned, le compilateur se rabat sur le second prototype qui fait appel à std::sort.
Le point clef étant de mettre toutes les informations dans le prototype. J'aurais par exemple put mettre decltype dans un paramètre initialisé avec une valeur par défaut (R f(int, decltype(xxx)* = 0); Mais il faudra probablement ajouter std::remove_reference (un pointeur sur une référence n'est pas permis))).
Programme de test:
#include <iostream>
#include <vector>
#include <list>
int main()
{
std::vector<int> v({2,6,4});
std::list<int> l({2,6,4});
sort(v);
sort(l);
for ( auto i : v ) std::cout << i << ' ';
std::cout << '\n';
for ( auto i : l ) std::cout << i << ' ';
}
Résultats:
2 4 6
2 4 6
J'ai indiqué qu'il été possible d'utiliser sizeof à la place de decltype. Voici comment:
template<std::size_t, class T = void>
struct dispatch_result_type
{ typedef T type; };
template<class T>
T declval();
template<class Container>
typename dispatch_result_type<sizeof(void(declval<Container&>().sort()),1)>::type
dispatch_sort(int, Container & c)
{ c.sort(); }
Le ,1 de sizeof(xxx,1) peut dérouter mais est requis si l'expression xxx retourne void. Comme void n'est pas vraiment un type, il ne fonctionne pas avec sizeof et il faut donc lui fournir autre chose. Il faut bien comprendre qu'ici xxx,1 est une seule expression et non pas 2 paramètres.
Bien que très peu probable, si j'ai mit void(yyy), c'est pour prévenir la surcharge de l'opérator ',' sur le type de retour retourné par yyy (car cet opérateur peut lui-même retourner un void).
sizeof ne donne pas l'information sur le type de retour mais une valeur, il est couplé à dispatch_result_type qui prend en second paramètre template le type de retour (void par défaut). Quand à declval, c'est le même principe que celui de la sl.
samedi 28 juin 2014
Réduire l'empreinte mémoire d'une agglomération de types
Un petit article pour parler d'optimisation mémoire (si on peut appeler ça comme ça) avec comme exemple la structure de donnée utilisée par std::unique_ptr.
Petit rappel, unique_ptr prend 2 paramètres en template: T et Deleter (qui est par défaut std::default_delete<T>).
Naïvement, l'implémentation serait:
template<T, Deleter = std::default_delete<T>>
class unique_ptr {
T * m_pointer;
Deleter m_deleter;
//...
};
Rien d'extraordinaire.
Cependant, même si le Deleter est une classe sans attribut, sa taille est de 1 octet.
À partir d'ici je considère que Deleter est toujours la valeur par défaut, ce qui donne:
- sizeof(T*) == 4
- sizeof(Deleter) == 1
- sizeof(unique_ptr<T>) == 8
Ouille, méchant padding alors que seule 4 octets sont vraiment utilisés.
La solution ? Une classe qui contient le pointeur et hérite de Deleter. Les attributs de la classe dérivée vont se mettre après ceux de Deleter, et s'il n'en a pas, les attributs se positionnent au début de la classe.
Cette optimisation se nomme Empty Base Class Optimization (EBCO). Merci Guillaume.
template<T, Deleter = std::default_delete<T>>
class unique_ptr {
struct Pointer : Deleter {
T * m_pointer;
//...
};
//...
};
- sizeof(unique_ptr<T>) == 4
Mieux, non ?
Et si le Deleter est une référence ? Bien entendu, l'héritage ne fonctionne pas, il faut se rabattre sur la première forme (celle naïve ^^).
Avec des traits et un code volumineux cela est "facile".
En allant encore plus loin, on peut utiliser un Deleter personnalisé final.
(Mais qui utilise des classes finales... ?)
À ce moment, évidemment, même remarque que les références, l'héritage ne compile pas et l'utilisation de la première forme s'impose.
Avec un trait is_final c'est possible, mais ce dernier n'existe pas et son implémentation est impossible... On est coincé !
Vraiment ? Non, car d'irréductibles développeurs de compilos ont créé __is_final, une fonction non-standard utilisée notamment par std::tuple.
De là à dire que toutes les implémentations l'utilisent... Il n'y a qu'un pas que je suis prêt à franchir :).
(En C++14 le trait std::is_final devient standard.)
Au final, l'utilisation de std::tuple permet de s'affranchir de ces difficultés tout en optimisant l'espace mémoire1.
template<T, Deleter = std::default_delete<T>>
class unique_ptr {
std::tuple<T*, Deleter> m_t;
//...
};
Il y a toutefois un léger problème avec std::tuple. Si un élément doit être initialisé, alors ils doivent tous l'être. Cela peut causer quelques soucis si un des types n'a ni copy-ctor, ni move-ctor.
1 À condition de mettre les types dans l'ordre croissant d'alignement afin de réduire le padding lorsqu'il y en a plus de 2 (falcon::optimal_tuple).
lundi 9 juin 2014
256 couleurs et plus dans la console
Note: ajout à la fin concernant le TrueColor.
J'ai récemment appris l'existence de 256 couleurs dans la console. À chaque fois que je cherche des infos sur les couleurs je tombe toujours sur les trucs basiques.
Mais récemment j'ai vu un peu trop de couleurs sur un screen. Ce qui m'a dirigé sur un dépôt contenant un $LS_COLORS particulièrement fourni.
En fait, il s'avère qu'en rajoutant extended dans la recherche "color shell" on puisse trouver quelque(s) ressource(s). J'aurais bien voulu y penser la semaine dernière, ça m'aurait évité de comprendre par tâtonnement...
Comme le lien ci-dessus est très bien expliqué je ne fais ici qu'un condensé. Je passe également sous silence les 8 et 16 couleurs.
Utilisation
<Esc>[FormatColorm
Le caractère <Esc> est obtenu avec les séquences suivantes:
- \e
- \033
- \x1B
(Pour bash et echo, utiliser l'option -e pour interpréter les séquences backslashés.)
FormatColor correspond à la couleur de texte, de fond ou l'effet. Plusieurs peuvent être mis en les séparant par des ';'. L'ordre n'a pas d'importance.
| Couleur | Texte | Fond |
|---|---|---|
| Noir | 30 | 40 |
| Rouge | 31 | 41 |
| Vert | 32 | 42 |
| Jaune | 33 | 43 |
| Bleu | 34 | 44 |
| Magenta | 35 | 45 |
| Cyan | 36 | 46 |
| Gris clair | 37 | 47 |
Les couleurs précédentes peuvent être configurées sur certains shells.
| Effet | Code | Code annulation |
|---|---|---|
| normal | 0 | |
| gras | 1 | 21 |
| italique | 3 | 23 |
| souligné | 4 | 24 |
| clignotant | 5 | 25 |
| inversé | 7 | 27 |
| Couleur | Texte | Fond |
|---|---|---|
| Gris foncé | 90 | 40 |
| Rouge clair | 91 | 101 |
| Vert clair | 92 | 102 |
| Jaune clair | 93 | 103 |
| Bleu clair | 94 | 104 |
| Magenta clair | 95 | 105 |
| Cyan clair | 96 | 106 |
| Blanc | 97 | 107 |
Couleur étendue avec X allant de 0 à 255 inclus:
- Texte: 38;5;X
- Fond: 48;5;X
Par contre, outre le fait que les couleurs étendues ne fonctionnent pas partout, elles peuvent allègrement pourrir le rendu. Pour retrouver un rendu normal: `tput reset`. À essayer sur les tty, c'est marrant ^^.
Le bout de code ci-dessous permet de visualiser la palette de couleur.
for m in 38 48 ; do
n=0
for l in {0..31} ; do
for c in {0..7} ; do
echo -ne "\033[$m;5;$n;1m$n\e[0m\t"
((++n))
done
echo
done
done
TrueColor
La console supporte aussi les couleurs hexadécimal.
\e[48;2;$r;$g;${b}mpour le fond\e[38;2;$r;$g;${b}mpour le texte
$r, $g et $b sont des valeurs allants de 0 à 255. Encore une fois, les effets comme gras, italique, etc, peuvent être ajoutés (echo -e '\e[38;2;205;110;0;3mplop' pour un texte couleur cuivre et italique).
samedi 31 mai 2014
Valeur, référence ou pointeur ? (2/2)
Dans le précédent billet, j'opposai les paramètres par références constantes contre ceux par valeurs.
Sans plus attendre entamons la seconde question
Référence ou pointeur ?
Si je dois faire court je dirai: pointeur jamais ; référence quand possible.
Sans autre forme de procès :D
Mais on me dit dans l'oreillette qu'il faut argumenter... Alors c'est parti.
Les références possèdent un contrat beaucoup plus fort que les pointeurs: elles ne peuvent être nulles et référencent toujours la même variable.
À contrario, les pointeurs peuvent changer la variable référencée ou ne pointer sur aucune variable (nullptr).
Une référence est l'équivalent d'un pointeur constant non-nul (avec une syntaxe d'utilisation plus simple: pas besoin de déréférencer).
De leurs restrictions, celles-ci ne peuvent pas toujours correspondre au besoin ; les pointeurs sont alors envisageables.
De plus, les pointeurs sont beaucoup utilisés dans les constructions dynamiques (allocation dynamique) quand les classes sont à sémantique d'entités. Principalement car ces dernières ne sont pas copiables et que l'allocation dynamique permet de s'affranchir de la portée (le scope) en se détachant de la pile.
Le pointeur parle trop
L'usage de pointeur (pointeur nu) est cependant à prendre avec des pincettes, voici 3 questions que soulève l'usage d'un pointeur
- Dois-je contrôler la durée de vie du pointeur (le détruire) ? [oui, non]
- Est-ce un élément ou une séquence d'élément ? [séquence, simple valeur, ça dépend]
- Le pointeur peut-il être nul ? [oui, non]
Après un petit calcul combinatoire (2*3*2), il y a 12 réponses possibles. Le pire est de répondre: “ça dépend”. Si on l'enlève, il reste quand même 8 possibilités.
La sémantique du pointeur est, au final, très faible. Lui en ajouter devient alors capital.
Plus de sémantique pour un pointeur
Hélas, il n'y a pas de réponse universelle, tout dépend des cas d'usages. De plus certaines combinaisons son conceptuellement douteuses.
Pour exemple, un pointeur non-nul mais qu'on détruira. Le non-nul amène aux références mais idéologiquement une référence n'est pas faite pour être détruite.
On peut néanmoins sortir quelques règles:
- Si le pointeur n'est pas nul et que l'appelé ne gère pas la durée de vie: reference_wrapper qui permet de changer la référence utilisée
- Si l'appelé contrôle la durée de vie: pointeurs intelligents (std::unique_ptr en priorité, std::shared_ptr, ...).
- Si le pointeur peut être nul et que l'appelé ne gère pas la durée de vie alors un pointeur est “justifié”. En interne du moins, pour l’extérieur un non_owner_ptr ou un observer_ptr sera plus parlant. Si le pointeur peut être invalidé pendant l’exécution alors std::weak_ptr ou autres est à envisager.
- Tout ce qui est tableau est indiqué dans les signatures des objets wrapper (unique_ptr<T[]>) ou/et grâce à un attribut de taille. De plus, s'il faut soit des tableaux, soit une valeur alors toujours préférer le type commun: tableau (les valeurs deviennent des tableaux de taille 1). Les tableaux dynamiques sont quant à eux plus faciles à utiliser avec std::vector.
Au final, l'usage de pointeur nu est très peu utilisé, voire pas du tout. De plus, leur mauvais usage avec l'allocation dynamique amène des fuites mémoires principalement dues aux libérations manuelles. Dans un langage comme le C++ un code non exception safe va faire des fuites mémoires. De manière générale la libération s'applique sur toute forme de ressource: lock, fichier, etc.
Pour éviter cela, les ressources doivent être attachées à la pile et la puissance du déterminisme de destruction permettra de les libérer convenablement. On parle aussi de RAII. Pour rappel, tout ce qui est sur la pile est détruit à la sortie du scope. Et la sémantique de mouvement permettra de changer de portée.
Le wiki de Guillaume Belz en parle très bien: pourquoi le RAII est fondamental en C++ ?
jeudi 22 mai 2014
Valeur, référence ou pointeur ? (1/2)
Quand utiliser une variable par valeur, référence ou pointeur ? Telle fut la question qui m'a été posée ^^.
Comme je ne suis pas entièrement satisfait de la réponse que j'ai donné, je fais un article. Pour tout dire, la réponse n'est pas aussi triviale que l'on pourrait le croire depuis l'arrivée du C++11 et la sémantique de mouvement.
Tout d'abord, décomposons cette question en 2 parties:
- Valeur ou référence constante ?
- Référence ou pointeur ?
Je réponds ici à la première, la seconde fera l'objet d'un autre article.
Valeur ou référence constante ?
Le choix se justifie en majorité par un besoin d'optimisation. Une prise par valeur induit forcément une copie, cette dernière pouvant être extrêmement coûteuse. Par exemple, la copie d'un std::vector ne se fait pas en un claquement de doigt, il y a tout un attirail derrière: allouer un espace mémoire et copier tous les éléments du vecteur précédent qui peuvent eux-mêmes faire des opérations complexes.
Au contraire de la référence constante qui n'a de prix que la taille d'une référence. Celle-ci étant toujours la même quel que soit le type référencé (sizeof(intptr_t), oui, pareil qu'un pointeur).
J'insiste bien sur référence constante car pour être au plus proche de l'effet d'une copie, l'objet d'origine ne doit pas bouger. De plus, une instance constante ne peut appeler que des méthodes constantes, ce qui assure une invariance (cf: const-correctness).
Donc, référence constante pour les types qui ne sont pas modifiés dans la fonction. Une règle dit: "tout ce qui est plus grand qu'un pointeur pourrait être passé par référence constante". Je préfère dire tous les types en référence constante sauf les types fondamentaux (int, float, etc). Même si mettre une référence constante sur un int n'est pas une erreur, je n'adhère pas vraiment.
Une seule exception cependant, quand le paramètre va de toute façon être copié localement dans la fonction pour être modifié. On pourrait croire que le résultat sera le même, mais c'est être naïf.
class BigInt { ... };
operator+(const BigInt & a, const BigInt & b)
{
BigInt ret(a);
ret += b;
return ret;
}
Supposons que BigInt fasse de l'allocation dynamique pour représenter les nombres. Avec ce code:
BigInt n1(1);
BigInt n2 = BigInt(2) + n1;
Il y a 3 allocations:
- n1
- BigInt(2)
- ret
Alors que cette implémentation de operator+ n'en produit que 2.
operator+(BigInt a, const BigInt & b)
{
a += b;
return a;
}
Car il y a copie élision (c'est le même principe que la RVO mais pour les paramètres). (Voir aussi ici et la réponse de Flob90.)
Mais ça c'était avant...
Maintenant qu'il y a la sémantique de mouvement, les copies sont préférées quand une méthode recevant le paramètre va, quoi qu'il arrive, le copier dans une autre variable (variable membre par exemple). Premièrement parce que l'utilisateur pourra faire un move de sa variable pour s'en "débarrasser" car il n'en a plus besoin. Deuxièmement parce que la fonction a besoin d'une copie et le compilateur le fera pour nous.
En comparaison avec une copie sur le vecteur, le move-constructor ou le move-assignment est extrêmement rapide: 3 affections de pointeur pour chaque vecteur.
Par exemple avec cette base:
#include <utility>
#include <vector>
using vector_int_t = std::vector<int>;
class A {
vector_int_t c;
public:
A(vector_int_t cont)
: c(std::move(cont))
{}
};
Le code suivant fait 2 allocations (comme avec les références constantes)
vector_int_t c{1, 2};
A a(c);
Alors que celui-ci qu'une seule
vector_int_t c{1, 2};
A a(std::move(c));
// c.size() == 0;
Et ce dernier aussi
A(vector_int_t{1, 2});
// ou A({1 ,2});
Mais si le type ne gère pas une ressource, le constructeur de mouvement sera l'équivalent d'une copie (voir std::is_trivially_move_constructible ?) et alors une référence constante est mieux.
Quand la copie se fait sous condition
Cependant, il existe des paramètres pouvant être copiés, mais pas toujours. Dans ce cas, bien que la référence constante reste une bonne solution, une version prenant aussi une temporaire (rvalue ici) est probablement mieux. Mais si les types ne sont pas abstraits (comprendre full template) alors il faudra faire 2 versions: une avec rvalue et une avec constref. Ce qui se traduit, quand le code est un peu long, par l'ajout d'une fonction de prédicat ou une version template privée appelé par les 2 autres.
Les && sur les types full templates ont 2 états possibles: rvalue ou lvalue. (categorie de valeurs).
struct Foo {
void foo(std::string const & s) { privfoo(s); }
void foo(std::string && s) { privfoo(std::move(s)); }
private:
template<class String>
// ici && représente soit rvalue, soit une référence (constante)
void privfoo(String && s) {
// ...
if (xyz) {
// soit un move-assign soit un copy-assign
str = std::forward<String>(s);
}
}
std::string str;
};
Ou
struct Bar {
void bar(std::string const & s) { if (check(s)) str = s; }
void bar(std::string && s) { if (check(s)) str = std::move(s); }
private:
bool check(const std::string & s) {
// ...
return xyz;
}
std::string str;
};
Prise d'objet non-copiable sous condition
Par exemple, donner la propriété d'un unique_ptr à une classe selon certains prérequis décidés par une méthode. Contrainte supplémentaire, l'objet n'est pas copiable.
- Une référence constante n'est pas envisageable, l'objet ne pouvant pas être déplacé car constant.
- Une prise par valeur non plus (grâce à move), car la ressource serait systématiquement transmise à la fonction même si cette dernière ne la garde pas. L'appelant est dans l'incapacité de le savoir et perd la ressource.
- Une référence non-constante est possible, mais il ne sera alors pas possible d'envoyer un temporaire. Il faudra obligatoirement passer par une variable intermédiaire ce qui est désagréable quand on ne va rien en faire.
- Reste la rvalue avec laquelle une temporaire fonctionne, mais il faudra automatiquement faire un move quand la variable est une référence. Cela a l'avantage d'informer l'utilisateur sur l'éventuel déplacement de ressource.
Au final, bien qu'une référence fonctionne, seule une rvalue est pratique à l'usage. Seulement, aucunes de ces méthodes n'indiquent une prise partielle, seule la doc nous le dira. Ceci pourrait par contre être une convention d'écriture: si une ressource non copiable est prise par rvalue alors la fonction est libre de se l'approprier quand certaines conditions internes sont remplies.
Quand les opérations ne sont pas connues
Reste la dernière situation: les templates. Les règles sont les mêmes qu'avant mais si le rôle du paramètre n'est pas défini (comprendre la fonction ne fait rien d'autre qu'envoyer le paramètre à une autre fonction ou que le qualifier importe peu) les paramètres sont à prendre par rvalue. À ce moment, toutes les utilisations de cette variable devraient se faire par l’intermédiaire de std::forward, même lorsque des méthodes de celle-ci sont utilisées (la faute au qualifier de référence sur fonction membre).
Toutefois, attention de ne pas déléguer plusieurs fois la responsabilité et de faire forward (et move) que sur la dernière utilisation de la variable (voir le premier commentaire de Florian Blanchet pour l'exemple).
Cependant, certains objets de par leur concept seront pris par valeur. Je pense aux itérateurs, prédicats et comparateurs. Le premier pour une bonne raison: son état change. Et les 2 autres sont sans état (pas de variables membres) ou ne possèdent que des références ou valeurs constantes et sont généralement construit au moment de l'appel de la fonction. Il faut aussi garder en tête qu'une fonction est libre de faire des copies des foncteurs, c'est pour cette raison que lui donner un état est problématique. Si avoir un état est le comportement voulut, alors il reste toujours std::reference_wrapper, std::ref et std::cref. (Note, l'opérateur de parenthèse doit par conséquent toujours être constant pour les prédicats et comparateurs. Si ce n'est pas vérifié, en prenant l'exemple d'un comparateur + sort, cela pourrait changer l'invariant de comparaison et trier n'importe comment.)
Pas de référence constante pour les observers
Bien que cela sorte du cadre de la question d'origine, il ne faut pas prendre par référence constante une valeur à observer.
J'entends par observer les variables qui sont gardées en lecture dans le but de vérifier leur état à un instant t.
Les constref peuvent être des temporaires à leur construction (Jusque-là c'est défini par la norme: prolongement de la durée de vie d'une temporaire). Le problème vient du déplacement vers un scope parent. La constref temporaire est détruite mais la référence est gardée ; référence sur une valeur qui n'existe plus. Cela débouche sur un comportement indéfini et, dans le meilleurs des cas, un segfault.
#include <iostream>
#include <string>
struct Validate
{
const std::string & s;
void display() const { std::cout << s; }
};
Validate f()
{ return {"plop"}; }
int main()
{
Validate x = f();
x.display();
}
(Moi j'ai un segfault)
Pour limiter ce bug il faut empêcher de prendre les rvalue. Soit en faisant un Validate(std::string&&)=delete + Validate(const std::string & s):s(s){} soit en utilisant std::reference_wrapper (et cref). Ou, peut-être mieux, faire un objet observable tout pareil que reference_wrapper mais avec constructeur explicite. L'intérêt d'utiliser l'un des 2 objets cités et de focaliser l'utilisateur sur l'aspect "j'ai besoin que cette variable vive au moins aussi longtemps que moi".
Résumé
- Valeur pour les types fondamentaux (int, double, etc), ceux modifiés sans que l'utilisateur n'ai besoin de le savoir (ex: itérateurs ou premier paramètre de l'opérateur '+') ou les foncteur sans états (prédicats, comparateurs, ...).
- Valeur + std::move quand l'objet peut être déplacé (possède un move-ctor ou/et move-assign non trivial (ex: std::string, std::vector, ...).
- Valeur pour les ressources non copiables à transférer (std::unique_ptr, ...).
- Référence constante pour les paramètres en lecture seule ou ne disposant pas de move-ctor ou/et move-assign non trivial.
- Référence constante et rvalue quand le paramètre peut être copié et possède un move-ctor/move-assign non trivial.
- Rvalue pour les ressources non copiables avec déplacement conditionnel (std::unique_ptr, ...).
- Référence constante pour les types inconnus (template) qui n'ont pas d’intérêt à être pris par valeur (ou au pire, stratégie variable selon le résultat de std::is_trivially_*/std::is_copy_*/std::is_move_* ; même si cela n'est pas utile).
- Rvalue pour les types inconnus (template) quand le paramètre n'a pas de rôle direct dans la fonction ou que le qualifier n'importe pas (ne pas oublier std::forward pour le transmettre à une autre fonction (seulement s'il n'est plus utilisé ensuite)).