C++17 libère size(), data() et empty()

Posté par (page perso) . Édité par David Marec, Benoît Sibaud, Oliver H, Davy Defaud, palm123, gbdivers, whity, kp, audionuma et Xavier Claude. Modéré par Davy Defaud. Licence CC by-sa
Tags :
36
17
mar.
2018
C et C++

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.

Les fonctions empty(), data() et size() sortent de la prison « class » et rejoignent les fonctions begin() et end() qui les accueillent. Ces deux dernières fonctions accueillent également string_view. Derrières les barreaux de la prison des spécifications techniques, Unified-Call-Syntax va devoir y rester trois ans pour tenter de sortir avec C++20

Sommaire

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 de size une fonction membre dans la STL pour plaire au comité de standardisation. Je savais que begin, end et size 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 (page perso) . É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.

  • # Hum hum…

    Posté par . Évalué à 3.

    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).

    C'est typiquement pas le cas des méthodes size() et data() (pour empty() on peut l'implémenter à partir de size() 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) et f(a, b) n'est qu'une convention, c'est aller un peu vite… Écrire myValue.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 :

    bar(foo(a), b);

    vs

    a.foo().bar(b);

    Ç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 :

    foo(B& b, C& c);
    foo(A& a, D& d);

    et que je tente :

    foo(new B(), new D());

    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 (page perso) . Évalué à 1.

      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

      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 . É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 (page perso) . Évalué à 3.

      La préconisation d'[Herb Sutter] 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).

      C'est typiquement pas le cas des méthodes size() et data() (pour empty() on peut l'implémenter à partir de size() si la complexité de cette dernière est constante).

      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.


      D'un point de vue de la syntaxe, c'est un peu plus compliqué que cela. Dire que a.f(b), b.f(a) et f(a, b) n'est qu'une convention, c'est aller un peu vite… Écrire myValue.addTo(myArray) choque tout le monde quelque soit le langage.

      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 de b.f(a) c'est dans un contexte de dispatch multiple. L'écriture o.f() est intéressante dans nos langages pour dispatcher l'appel à la bonne fonction f() en fonction du type dynamique exact de l'objet o. 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.


      D'un point de vu pratique il n'est pas pratique d'avoir de l'autocomplétion sur des méthodes libres.

      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.


      D'un point de vu lecture, le chainage est plus compliqué de mon point de vu :

      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 . Évalué à 2.

        Le chainage o.f().g() respire la vilaine violation de la Loi de Déméter j'ai envie de dire.

        Ça sent surtout les API dites fluent ou simplement les objets o immuables :)

        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.

        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'appelle size 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 (page perso) . Évalué à 1.

          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 […] C'est ce que fais Go par exemple

          Certains des besoins oui, mais l'un des avantages décrits pour std::size() est de s'utiliser facilement avec static_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 ou static_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 . É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)

  • # Vieux con

    Posté par . É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 . É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 . Évalué à 0.

        Pourquoi un homme ?

        Ça aurait été tout autant sexiste !

        « ça aurait cassé le cliché »

        Si on ne tolère pas la vu d'une femme en string, on ne devrait pas plus tolérer celle d'un homme.

        Je tolère très bien, merci :) Et sinon, « ça aurait cassé le cliché ».

        Un homme et une femme en string, ou pas de personnage du tout, voilà l'égalité.

        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 (page perso) . É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 . É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 . É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 (page perso) . Évalué à 5.

      C'est triste ton avis sur les jeux de mots.

      • [^] # Re: Vieux con

        Posté par . É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.

        • [^] # Re: Vieux con

          Posté par (page perso) . É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 . É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 à ceux qui les ont postés. Nous n'en sommes pas responsables.