Cette évolution est dans la continuité de la libération de begin()
et end()
en C++11. Ces trois nouvelles fonctions, size()
, data()
et empty()
, avaient été oubliées lors des deux dernières évolutions. Le C++17 corrige le tir.
Tout d’abord, nous allons revenir sur les raisons de cette évolution. Puis, nous détaillerons ce qu’apporte chacune de ces trois fonctions. Et, pour conclure, nous verrons comment on va pouvoir les utiliser.
Sommaire
- Fonction membre ou fonction libre,
dog.kick()
oukick(dog)
? - Détaillons ces trois nouvelles fonctions du C++17
- Et pour s’en servir ?
Fonction membre ou fonction libre, dog.kick()
ou kick(dog)
?
Fonction membre | Fonction libre |
---|---|
objet.fonction() |
fonction(objet) |
dog.kick() |
kick(dog) |
jean.nom() |
nom(jean) |
conteneur.size() |
size(conteneur) |
chien.courir() |
courir(chien) |
chat.attrape(souris) |
attrape(chat,souris) |
objet.service() |
service(objet) |
Cette question de notation assez anodine a des répercussions non anodines, elles, sur ce qu’il nous est permis de faire.
D’un côté nous sommes dans la continuité du choix de la notation d’envoi de messages choisie par Smalltalk et, de fait, nous tendons à penser que nous faisons de l’orienté objet. De l’autre nous avons l’impression de faire du C ou du Pascal. Et pourtant, ce n’est guère différent (en termes d’ordre) de la notation d’appel employée en CLOS, dialecte orienté objet du LISP. En effet, dans ce dernier, nous aurions employé (f x y)
.
Dans l’article Comment prendre en charge les multi‐methods en C++, Jean‐Louis Leroy discute justement de la perception de ces choix de notation. Il est difficile de ne pas abonder dans son sens sur deux autres points.
D’abord, prononcé à voix haute « kick dog » passe mieux que « dog kick ». Et ensuite, et surtout, qui est véritablement l’acteur ?
- le chien, qui va alors aboyer et renoncer, parce que c’est dans sa nature ?
- un tiers, qui est l’auteur du coup de pied ?
Mais, mettons de côté ces aspects philosophiques, et recentrons‐nous sur les répercussions plus techniques et objectives. Mettons de côté aussi les aspects de dispatch‐multiple (désolé, la page en français est un peu pauvre sur le sujet) où une symétrie fait que x.f(y)
n’a certainement pas plus de légitimité que y.f(x)
et de fait que f(x, y)
. Ce n’est pas le point de cette évolution du standard.
Alors qu’Alexander Stepanov préparait avec Lee Meng une bibliothèque template générique dès le début des années 90, et qu’ils la peaufinaient pour satisfaire les exigences du comité de standardisation du C++, ils avaient dû faire des concessions pour qu’elle soit validée par ce que nous pourrions qualifier d’intelligentsia OO. Stepanov se défend d’avoir conçu cette bibliothèque en suivant le paradigme objet et évoque dans son Notes on Programming sa concession au sujet de size()
qu’il aurait préférée libre. Ainsi, plutôt que d’employer size(v)
, nous employons depuis v.size()
. Cette bibliothèque, vous en avez probablement entendu parler, c’est la STL, qui fut incorporée dans la bibliothèque standard du C++ en 98.
« While we could make a member function to return length, it is better to make it a global friend function. If we do that, we will be able eventually to define the same function to work on built‐in arrays and achieve greater uniformity of design. I made size into a member function in STL in an attempt to please the standard committee. I knew that begin, end and size should be global functions but was not willing to risk another fight with the committee. In general, there were many compromises of which I am ashamed. It would have been harder to succeed without making them, but I still get a metallic taste in my mouth when I encounter all the things that I did wrong while knowing full how to do them right. Success, after all, is much overrated. I will be pointing to the incorrect designs in STL here and there: some were done because of political considerations, but many were mistakes caused by my inability to discern general principles. »
Traduction :
« S’il est possible de créer une fonction membre pour retourner la taille [de
vector
], il est préférable d’en faire une fonction générale. Ce faisant, nous pourrons par la suite utiliser cette même fonction sur les tableaux de base et obtenir ainsi une conception beaucoup plus uniforme. J’ai fait desize
une fonction membre dans la STL pour plaire au comité de standardisation. Je savais quebegin
,end
etsize
auraient dû être des fonctions génériques, mais je ne souhaitais pas entrer à nouveau en conflit avec le comité. Globalement, je ne suis pas très fier de nombre de compromis. Sans ceux‐ci, il aurait été plus difficile de réussir, mais j’ai toujours un arrière‐goût amer lorsque je tombe sur tous ces trucs que j’ai mal faits alors que je savais parfaitement comment les concevoir correctement. Finalement, ce succès est pas mal surestimé. Je pointerai les conceptions erronées dans la STL ici et là : si certaines sont dues à des considérations politiques, la plupart étaient des erreurs provoquées par mon incapacité à dégager de grands principes. »
Des années plus tard, dans ses GOTW compilés dans Exceptional C++, Herb Sutter nous fait réfléchir à l’interface monolithique de std::string
et à toutes ses fonctions qui auraient pu être libres plutôt que membres dans cette classe standard — std::string
représente des chaînes de caractères. Sa préconisation est d’élargir au maximum l’interface des classes par l’extérieur et de garder membres uniquement les fonctions qui ont besoin d’aller taper dans les membres privés, ou qui peuvent être supplantées par redéfinition (overridding en VO).
De ce côté‐là, std::string
est clairement un contrevenant à la règle. Pire, qui n’a jamais croisé des classes de chaînes définies juste parce qu’il manquait des petites choses à std::string
, comme par exemple : std::string::trim_right()
, std::string::toupper()
, std::string::split()
, etc. ?
Où est le mal à cela ? Si nous définissons My::String::trim_left()
, nous avons ce que nous voulons, non ? Et si nous voulons l’appliquer sur une std::string
ou un char const*
, nous avons juste à construire un objet My::String
à partir de ces derniers.
Quoi ?! Il faut encore gérer un autre type foss::string
? Bah, nous convertissons d’abord vers un char const*
ou vers une std::string
, puis vers une My::String
. Non ?
Il se trouve que, justement, C++17 nous apporte std::string_view
, une classe qui veut les unifier toutes, et qui garantit une efficacité équivalente au couple pointeur + taille quand nous manipulons des tableaux en C—et d’autres classes pourraient suivre dans cette thématique. C’est juste une vue sur une chaîne valide, un machin qui permet de regarder sans toucher, sans posséder et sans allouer ! Malheureusement, vu que split()
aurait été définie membre d’une classe tierce, nous ne pourrions pas l’appliquer directement sur la string_view
. Techniquement, de quoi a‐t‐on besoin pour définir split()
? D’un moyen de tester chaque caractère ou, mieux, d’une fonction (libre de préférence) qui permette de chercher un index dans une chaîne et aussi de pouvoir définir des sous‐chaînes.
Quel rapport avec std::size()
& Cie ? Nous sommes dans la même problématique.
Nous voudrions pouvoir écrire des algorithmes génériques sans avoir besoin de savoir ce que nous manipulons, et sans avoir besoin de payer une conversion chère vers un type bidon. Et si la chose que nous manipulons n’a pas le service, nous le lui rajoutons par l’extérieur.
Étendre par l’extérieur est ce qui nous ouvre le plus de possibilités. De plus, nous restons dans le credo C++ de ne pas avoir à payer pour ce dont on n’a pas besoin.
C’est ce sur ce sujet que Herb Sutter et Bjarne Stroustrup se sont entretenus début 2014 : proposer une unification des appels de fonctions, Unified Call Syntax, entre f(x,y)
et x.f(y)
.
Si, pour maintenir une certaine compatibilité, ils ont convenu que ces deux écritures pouvaient définir des comportements différents, ils se sont entendus pour définir qu’en cas d’échec d’une écriture, le compilateur testerait l’autre.
Il s’en est suivi une levée de boucliers et, finalement, l’idée des UCS a été bottée en touche. Pour l’instant ?
Détaillons ces trois nouvelles fonctions du C++17
std::empty()
C’est la plus simple du lot. Elle indique si son paramètre est vide. Elle s’applique à n’importe quoi qui puisse être vide.
Par défaut, elle sait s’appliquer à des tableaux statiques C (elle renvoie toujours faux dans ce cas), à n’importe quoi qui dispose d’une fonction membre empty()
, ou encore aux std::initializer_list
.
std::size()
Cette fonction est beaucoup plus intéressante.
Savez‐vous comment obtenir le nombre d’éléments dans un tableau statique en C ?
Si vous connaissez la réponse, vous connaissez aussi probablement sa limitation :
#define size(T) (sizeof(T)/sizeof(T[0]))
int const t[] = {1, 2, 3};
void f() {
printf("f: %zu\n", size(t));
}
void g(int const t[3]) {
printf("g: %zu\n", size(t));
}
int main(){
f();
g(t);
}
Une compilation en 64 bits (modèle LP64 ou LLP64) donnera :
f: 3
g: 2
Eh oui, g()
reçoit non pas un tableau, mais un pointeur ! Cela fait toute la différence. Autant dire que cette approche nous trahit facilement.
Fort heureusement, nous sommes en C++, et dans ce dernier on peut forcer g()
à attendre un tableau en recevant une référence sur un tableau :
void g(int (&t)[3]);
...
int t3[] = {1, 2, 3};
int t2[] = {1, 2};
g(t3); // compile correctement
g(t2); // est rejeté par le compilateur
Là, vous me direz, « la taille est écrite en dur dans le type du paramètre formel, ça ne sert à rien ! ». OK, « templatisons » et enchaînons pour obtenir directement la taille d’un tableau.
template <std::size_t N>
std::size_t size(int (&t)[N])
{ return N; }
Et le truc qui serait cool, c’est :
struct nombres {
enum type { un, deux, trois, MAX__ };
char const* chaines[] = { "un", "deux", "trois", "quatre" };
}
static_assert(nombres::MAX__ == size(nombres::chaines));
Sauf que cela ne compilera pas parce que cette fonction size()
n’est pas constexpr
. En C++11, c’est facile à rajouter, et pourtant officiellement, c’est de std::extent<decltype(tab)>::value
, seulement, dont nous disposons. En C++17, ça y est, nous avons std::size()
. C’est explicite, c’est simple à utiliser, et cela fait ce dont nous avions besoin depuis longtemps.
Note : en C++98, nous devions recourir à boost ou à Loki pour avoir un équivalent au static_assert
du C++11, et à d’autres feintes telles que la suivante pour avoir une taille exploitable dans une expression constante :
namespace {
template<typename T, std::size_t N>
char (&array_size_helper(T (&)[N]))[N];
} // namespace
#define array_size(array) (sizeof ::array_size_helper(array))
std::data()
Ici, rien de bien compliqué de prime abord : nous avons une fonction qui nous renvoie l’adresse où est stocké le premier élément d’une séquence d’éléments contigus en mémoire.
De même, cela est défini automatiquement sur tout conteneur qui expose une fonction membre éponyme, sur les tableaux statiques à la C, et sur les std::initializer_list
.
Nous pouvons nous poser la question de l’intérêt, car il suffisait d’écrire :
template <typename Collection>
void dump(Collection const& c)
{
api_c_(&c[0], &[size(c)]);
}
Sauf que… Et si l’opérateur d’accès indexé sur la collection spécifiait dans ses préconditions que l’index devait être strictement inférieur à la taille ?
reference operator[](size_t index)
[[expects: index < size()]] // syntaxe probable -- C++20
{
assert(index < this->size()); // syntaxe 98-17
return quivabien(index);
}
Bon, pour le second paramètre réel passé à api_c
, on peut écrire &c[0]+size(c)
. Seulement, si la collection est vide, nous sommes de nouveaux grillés. Et encore, c’est à supposer que la fonction avec une API C n’exige pas que le premier pointeur pointe bien vers un élément déréférençable, même si le second pointeur est égal au premier — ceci est le cas de beaucoup de fonctions du standard C : un memcpy()
de 0 élément vers ou depuis NULL
est officiellement illégal, et donc un UB.
Bref, vous l’aurez compris, data()
ne s’encombre pas avec ces considérations. Et depuis sa libération elle est donc compatible avec les tableaux statiques.
Bonjour data(c)
. Adieu &c[0]
.
Et pour s’en servir ?
C’est bien beau tout ça, mais nous n’avons pas abordé la façon dont nous pouvons nous en servir.
Vous devez vous dire que nous sommes un peu bêtes. Il suffit d’écrire, pour reprendre le dernier exemple :
template <typename Collection>
void dump(Collection const& c)
{
api_c_(std::data(c), std::data(c)+std::size(c));
// Ou si on suppose que les préconditions de l'API C soient exagérées
if (!std::empty(c))
api_c_(std::data(c), std::data(c)+std::size(c));
}
Et voilà ! Non ?
Pouvons‐nous nous en servir sur un type blas::vector
qui expose data
, empty
et size
en membres ? Oui, c’est prévu pour ! C’est donc parfait.
Mais… Et si ces fonctions n’étaient pas membres mais libres. Ou pire : et si elles avaient une version membre dont le nom commençait par une majuscule ?
Ah. Il n’y aurait qu’à définir une surcharge de std::empty(blas::vector const& v)
qui renvoie v.Empty()
. Le hic est que, bien que la norme nous autorise à procéder ainsi dans ce cas de figure précis (i.e. rajouter une spécialisation pour un type non standard dans l’espace de noms std
), il est fortement encouragé de procéder autrement. On préfère ignorer ainsi les exceptions à l’interdiction de rajouter des définitions dans std::
. Il faut bien reconnaître qu’il est plus simple de retenir une règle générale que d’expliquer les exceptions autorisées.
[C++11: 17.6.4.2.1/1]: The behavior of a C++ program is undefined if it adds declarations or definitions to namespace std or to a namespace within namespace std unless otherwise specified. A program may add a template specialization for any standard library template to namespace std only if the declaration depends on a user‐defined type and the specialization meets the standard library requirements for the original template and is not explicitly prohibited.
Traduction :
Le comportement d’un programme C++ est indéfini s’il ajoute des déclarations ou définitions à l’espace de noms std ou à un espace de noms au sein de std, sauf indication contraire. Un programme pourra ajouter une spécialisation pour tout modèle de l’espace de noms std si et seulement si, cette déclaration concerne un type défini par l’utilisateur, que cette spécialisation convient aux exigences du modèle de base et qu’elle n’est pas explicitement interdite.
Procédons autrement donc. Si seulement nous pouvions nous contenter d’un empty(v)
sans avoir besoin de taper ces fichus noms d’espaces de noms (sic), ça serait parfait. Il se trouve que… justement, si ! Nous pouvons le faire.
Si l’on écrit :
template <typename Collection>
void dump(Collection const& c)
{
if (!empty(c))
api_c_(data(c), data(c)+size(c));
}
Cela va compiler avec n’importe quel type défini par l’utilisateur qui dispose de fonctions empty()
, data()
et size()
qui sont définies dans le même espace de noms que celui où ce type utilisateur est défini.
On appelle ce principe le Koenig Lookup (comme dans Andrew Koenig) ou encore plus généralement aujourd’hui l’Argument‐Dependent (name) Lookup, §3.4.2 de la norme.
Il ne reste que les cas des tableaux statiques à la C. Ce ne sont pas des types définis dans un espace de noms. L’ADL ne peut pas être appliqué. Ainsi, pour eux, on a besoin de rajouter ces trois lignes au début de la fonction :
using std::empty;
using std::data;
using std::size;
Pour blas::vector
, il ne nous reste qu’à définir ces trois fonctions dans l’espace de noms blas
. Si maintenant cette bibliothèque nous interdit aussi de définir quoi que ce soit dans son espace de noms, alors il n’y a plus qu’à ouvrir une fiche d’anomalie sur le système de suivi de bogues en expliquant qu’il serait vachement sympa d’ouvrir la compatibilité avec le C++, avec explications sur l’ADL/KNL à l’appui.
Si maintenant vous deviez écrire une bibliothèque d’une qualité et d’une robustesse à toute épreuve, vous êtes invités à lire ce billet fort instructif d’Eric Niebler, qui est au cœur de la STL v2, au sujet du design de points de customisation.
# oups
Posté par lmg HS (site web personnel) . Évalué à 4.
Désolé, il y a eu un échec, la dépêche n'était pas prête pour publication. S'il y a moyen de la remettre en édition histoire que je revienne sur un état antérieur publiable, je suis preneur. Merci.
[^] # Re: oups
Posté par lmg HS (site web personnel) . Évalué à 3.
Merci :)
# Hum hum…
Posté par barmic . Évalué à 3.
C'est typiquement pas le cas des méthodes
size()
etdata()
(pourempty()
on peut l'implémenter à partir desize()
si la complexité de cette dernière est constante).D'un point de vue de la syntaxe, c'est un peu plus compliqué que cela. Dire que
a.f(b)
,b.f(a)
etf(a, b)
n'est qu'une convention, c'est aller un peu vite… ÉcriremyValue.addTo(myArray)
choque tout le monde quelque soit le langage.D'un point de vu pratique il n'est pas pratique d'avoir de l'autocomplétion sur des méthodes libres.
D'un point de vu lecture, le chainage est plus compliqué de mon point de vu :
vs
Ça peut permettre le multidispatch, mais j'ai rarement eu besoin de ça (mais ça c'est mon expérience), mais surtout ça se comporte comment si j'ai une classe B qui hérite de A une classe D qui hérite de D et 2 méthodes :
et que je tente :
Au final tout ça c'est pour tenter de singer du structural typing ? Autant en faire réellement, non ? (d'autant que c'est ce que permettait les concepts, si je ne m'abuse).
[^] # Re: Hum hum…
Posté par nokomprendo (site web personnel) . Évalué à 1.
A priori, c'est une ambiguïté que le programmeur doit résoudre. Apparemment c'est comme ça en Julia (qui utilise le multiple-dispatch) : https://docs.julialang.org/en/stable/manual/methods/#man-method-design-ambiguities-1. Et ça doit être pour cela que Herb Sutter conseille de conserver les méthodes redéfinies en membres de classe.
[^] # Re: Hum hum…
Posté par barmic . Évalué à 1.
Ce que je comprends de Herb Sutter, c'est qu'il ne veut pas violer l'encapsulation (et il a bien raison !).
[^] # Re: Hum hum…
Posté par lmg HS (site web personnel) . Évalué à 3.
C'est vrai, et c'est là que j'aime bien le commentaire d'Alexander Stepanov qui dit que
size()
aurait dû être une fonction externe et amie.Presque. Je dis que
a.f(b)
,f(a, b)
, ou(f a b)
ce n'est qu'une convention d'écriture. Un sucre syntaxique.Quand je parle de la validité de
a.f(b)
ou deb.f(a)
c'est dans un contexte de dispatch multiple. L'écritureo.f()
est intéressante dans nos langages pour dispatcher l'appel à la bonne fonctionf()
en fonction du type dynamique exact de l'objeto
. Mais quand le dispatch dépend du type dynamique de plusieurs objets, pourquoi en choisir un plutôt qu'un autre? On aura parfois, souvent ?, un verbe qui rend un appel plus clair, mais pas toujours. Un exemple type de dispatch multiple : la gestion de collision entre plusieurs objets de natures différentes (vaisseau, missile, astéroïde…). Dans la collision il n'y a pas un objet qui soit plus acteur qu'un autre.Il est vrai que la complétion sur des fonctions libres peut s'avérer moins pratique car le lexique des fonctions libres et plus conséquent—même en restreignant le périmètre à toutes les fonctions pouvant matcher des variables dans la portée courante. Si mes souvenirs sont bons, ce point était ressorti lors des échanges autour de l'UCS. Mais il n'avait pas été considéré comme
critique.
Le chainage
o.f().g()
respire la vilaine violation de la Loi de Déméter j'ai envie de dire. Quitte à enchainer des transformations de données, je préfère ce qui est fait dans les ranges v3:o | f() | g()
.Je ne suis pas sûr de te suivre concernant ta remarque sur le structural binding. Les concepts ça reste du polymorphisme statique. Dans le cas du dispatch multiple OO, nous sommes sur du polymorphisme dynamique.
[^] # Re: Hum hum…
Posté par barmic . Évalué à 2.
Ça sent surtout les API dites fluent ou simplement les objets o immuables :)
Le besoin d'écrit dans l'article est généralement géré par du duck-typing dans les langages dynamiques ou du structural typing dans les langages statiques. Si tu pouvais exprimer « ma fonction
foo
prend comme paramètre une référence vers un objet sur le quel je peux appeler une méthode qui s'appellesize
qui ne prend pas de paramètre et qui retourne un entier », le besoin décrit (donc hors dispatch multiple) serait tout aussi bien rempli. C'est ce que fais Go par exemple.[^] # Re: Hum hum…
Posté par un_brice (site web personnel) . Évalué à 1.
Certains des besoins oui, mais l'un des avantages décrits pour
std::size()
est de s'utiliser facilement avecstatic_assert
.Au delà du duck typing/structural typing, le C++ essaie de fournir des outils pour vérifier à la compilation certains erreurs lors de l'utilisation de bibliothèques. Ce sont les exemples donnés avec
expects
oustatic_assert
.Pour voir la différence: Google pytype implémente des validations statiques pour Python (potentiellement plus restrictives que le système de types du language). C'est à mon sens un besoin auquel Go réponds moins bien.
# std::extend typo?
Posté par Thomas Douillard . Évalué à 4.
Apparemment, c’est pas std::extend dont il s’agit, mais de std::extent : http://en.cppreference.com/w/cpp/types/extent pour ceux qui comme moi sont un peu perplexe et auraient cherchés à comprendre.
A priori ça colle : sur un tableau N-dimensionnel, std::extent::value donne statiquement le nombre d’éléments de la Xième dimension (0 pour un tableau unidimensionnel)
[^] # Re: std::extend typo?
Posté par lmg HS (site web personnel) . Évalué à 1.
Oui, tout à fait, merci. Je me fais avoir à chaque fois.
Je suis un complet newbs avec l'interface d'édition des articles sur linuxfr et j'avoue ne pas savoir comment procéder pour corriger cela facilement. '
[^] # Re: std::extend typo?
Posté par Benoît Sibaud (site web personnel) . Évalué à 4.
Corrigé, merci.
# Vieux con
Posté par cel . Évalué à -8.
Hello,
Désolé pour ce hors sujet, mais je vais faire mon vieux con : la représentation de string_view par une femme qui porte un string qui dépasse, c'était vraiment nécessaire ? Si au moins ça avait été un homme, ça aurait cassé le cliché, mais là, on est en plein stéréotype.
My 2 cents.
[^] # Re: Vieux con
Posté par xcomcmdr . Évalué à 8.
Pourquoi un homme ?
Ça aurait été tout autant sexiste !
Si on ne tolère pas la vu d'une femme en string, on ne devrait pas plus tolérer celle d'un homme.
Sinon, c'est juste qu'on veut bien se faire voir au lieu d'être féministe, c'est tout.
Un homme et une femme en string, ou pas de personnage du tout, voilà l'égalité.
My 2 cents.
"Quand certains râlent contre systemd, d'autres s'attaquent aux vrais problèmes." (merci Sinma !)
[^] # Re: Vieux con
Posté par cel . Évalué à 0.
« ça aurait cassé le cliché »
Je tolère très bien, merci :) Et sinon, « ça aurait cassé le cliché ».
Bon, je ne voulais pas aller sur ce terrain là, mais vu l'illustration, j'aurais préféré pas d'illustration. Et c'est pas la peine de me sortir le classique "si t'aimes pas tu n'as qu'à le faire", je suis une tanche en illustation.
My 2 cents.
[^] # Re: Vieux con
Posté par Lutin . Évalué à 4.
De quel cliché tu parles ?
[^] # Re: Vieux con
Posté par Dr BG . Évalué à 10.
C'est ton a priori sur les cheveux longs qui te font croire que c'est une femme ?
[^] # Re: Vieux con
Posté par cel . Évalué à -4.
Hello,
Mes cheveux longs se portent bien, ils te remercient :)
Après, vouloir faire passer l'illustration du personnage string_view pour un homme, pourquoi pas !
My 2 cents.
[^] # Re: Vieux con
Posté par groumly . Évalué à 2.
Vu les hanches/bassin, ca va être dur de le faire passer pour un homme.
Linuxfr, le portail francais du logiciel libre et du neo nazisme.
[^] # Re: Vieux con
Posté par Pierre Tramal (site web personnel) . Évalué à 3.
Merci de ne pas propager ces stéréotypes réactionnaires de genre !
[^] # Re: Vieux con
Posté par Lutin . Évalué à 5.
C'est triste ton avis sur les jeux de mots.
[^] # Re: Vieux con
Posté par olibre (site web personnel) . Évalué à 6.
La dépêche a été en cours de rédaction depuis plus d'un an (depuis décembre 2016), et l'image illustrant cette dépêche a été finalisée en janvier 2017 :
https://github.com/cpp-frug/materials/commits/gh-pages/images/fonctions_libres.svg
Pendant la réalisation de cette image, différentes personnes, de sexe différents (peut-être même plus de femmes que d'hommes) ont donné leur avis afin d'améliorer les différents aspects, notamment la forme des personnages. Cette image a été appréciée mais aussi approuvée par l'ensemble des bêta-testeurs.
Merci de donner un coup de main à la rédaction des dépêches, plutôt que de jouer au vieux con dans les commentaires.
Commentaire sous licence Creative Commons Zero CC0 1.0 Universal (Public Domain Dedication)
[^] # Re: Vieux con
Posté par Axioplase ıɥs∀ (site web personnel) . Évalué à 1.
Ben, j'aurais suggéré "autres types d'objets" au pluriel, si j'avais participé aux discussions sur l'image.
[^] # Re: Vieux con
Posté par cel . Évalué à 0.
Hello,
Au temps pour moi, je ne commenterai plus les contenus pour lesquels je ne prends pas part à la rédaction.
My 2 cents.
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.