C++ se court-circuite le constructeur de copie

Posté par  . Édité par Oliver, Davy Defaud, lmg HS, gbdivers, gipoisson, Benoît Sibaud, palm123 et Storm. Modéré par bubar🦥. Licence CC By‑SA.
Étiquettes :
38
11
déc.
2016
C et C++

Le calendrier de l’Avent du C++ continue. Après quelques trous dans le calendrier, aujourd’hui une nouvelle surprise : le court-circuit du constructeur de copie.

Cette fonctionnalité est présente dans le C++ depuis la nuit des temps et pourtant peu connue, alors que ses effets de bords peuvent être redoutables. Cette dépêche très pédagogique explique tous les détails d’une optimisation ultime.

Une nerd s’électrocute en touchant la vieille tour C++ de sa voisine à cause des effets de bords du court-circuit du constructeur de copie (C++98 copy elision)

Sommaire

Terminologie

Cette dépêche contient des mots français peu utilisées au quotidien. Ces mots sont le résultat de la traduction des termes du standard C++. Pour ne pas être trop perdu, un petit tour de la signification spécifique à cette dépêche.

Élision

  • « élision du constructeur de copie » : traduction de l’anglais « copy elision » qui est une optimisation évitant de passer par le constructeur de copie ;
  • « élider » : en anglais « elide », supprimer le constructeur de copie ;
  • « éluder » : éviter avec adresse le constructeur de copie, éclipser, court‐circuiter, outrepasser, contourner.

Cette dépêche n’utilise pas « éluder », mais « élider », car l’orthographe est proche du terme anglais « elide ».

Constructeur de copie

Cette dépêche utilise la traduction « constructeur de copie » pour l’expression anglaise « copy constructor ». D’autres documents en français utilisent des traductions sensiblement différentes :

  • « constructeur par copie » ;
  • « constructeur de recopie » ;
  • « constructeur par recopie ».

Les auteurs de cette dépêche ont opté pour l’expression « constructeur de copie », car sémantiquement le mot « de » identifie, alors que le mot « par » décrit. Et car l’orthographe de « copie » est plus proche de l’anglais « copy » (un peu comme choisir « paquet » au lieu de « paquetage » pour traduire « package » en informatique). Il en est de même pour « constructeur de déplacement ».

Si vous souhaitez apporter vos idées sur la traduction, merci de vos commentaires constructifs (par copie). ;-)

Le constructeur de copie peut être coûteux

Lors des appels et retours de fonctions, il arrive qu’un objet temporaire soit créé, avant d’être copié à son emplacement mémoire définitif. Cette approche consomme inutilement des ressources (mémoire, temps d’exécution).

struct GrosseStructure
{
  // ... des attributs volumineux
  // et des références vers d'autres objets
};

GrosseStructure f()
{
  GrosseStructure immense;
  // ...

  // Copie de l'objet immense
  // par retour de fonction
  return immense;
}

void g (GrosseStructure arg)
{
  // ...
}

int main()
{
  // Copie d'un objet temporaire
  // dans l'appel de fonction g()
  g( f() );
}

Élider le constructeur de copie

Dans certains cas, il est possible d’éviter la copie, en créant directement l’objet à son emplacement de destination final. Le C++98 autorise le compilateur à faire une telle optimisation.

L’exemple suivant construit un objet a par une succession de six constructeurs de copie.
GCC et Clang n’exécutent pas le constructeur de copie. Ces deux compilateurs optimisent le code, même avec l’option -O0 (qui signifie pourtant « pas d’optimisation »). Cette optimisation est quand même désactivée avec -fno-elide-constructors.

#include <iostream>

struct A
{
  int i; // incrémenté à chaque copie
  A()           { i = 0; }
  A(A const& a) { i = a.i + 1; }
};

A f() { return A(A()); }

int main()
{
  A a = A( A( A( f() ) ) );
  std::cout << "a.i = " << a.i << std::endl;
}
Option de compilation GCC-6.2 Clang-3.8
-std=c++98 -O0 a.i = 0 a.i = 0
-fno-elide-constructors a.i = 6 a.i = 6

Explications

Dans l’exemple précédent, la fonction f() crée un objet temporaire A. Le compilateur élide le constructeur de copie et crée cet objet A directement dans la pile d’appel (call stack) de la fonction appelante main().

Le compilateur court‐circuite autant de fois que nécessaire. Cette optimisation s’applique aussi à l’initialisation de copie (copy initialization). Ainsi, l’objet temporaire retourné par la fonction f() est directement construit dans a. Et cela indépendamment de l’extension inline. C’est‐à‐dire que le corps de la fonction f() aurait pu se trouver dans une bibliothèque à part.

Plus généralement, et contrairement au langage C, le standard C++ autorise le compilateur à effectuer toutes les optimisations possibles du moment que le résultat soit conforme à celui attendu par le code source. Et l’élision du constructeur de copie va plus loin, car le standard C++ part du principe que les développeurs codent le constructeur de copie pour faire la même chose que les autres constructeurs, le résultat des constructeurs se valent. Le compilateur ne vérifie pas si les corps des différents constructeurs font la même chose (ce serait trop compliqué).

Avertissement : Le standard C++ privilégie cette optimisation, quitte à ne pas exécuter les instructions du constructeur de copie. Si ton constructeur de copie réalise des opérations particulières parce que tu es certain qu’il sera forcément appelé avec A(A()), alors le compilateur risque de te faire tourner en bourrique !

Et le constructeur de déplacement ?

Le compilateur remplace le constructeur de copie par le constructeur de déplacement quand cela est possible. En revanche, généralement, le compilateur préfère élider le constructeur de copie quand cela est possible, donc ne pas utiliser le constructeur par déplacement.

Nous pouvons nous poser la même question pour les opérateurs d’affectation de copie et de déplacement. Donc, expérimentons un peu plus en généralisant le précédent exemple :

#include <iostream>

int t[6] = { 0, 0, 0, 0, 0, 0 };

struct A
{
  A()                    { ++t[0]; }
  A(A const&)            { ++t[1]; }
  A& operator=(A const&) { ++t[2]; return *this; }
#if __cplusplus > 201100
  A(A &&)                { ++t[3]; }
  A& operator=(A &&)     { ++t[4]; return *this; }
#endif
  ~A()                   { ++t[5]; }
};

A f() { return A( A() ); }

int main()
{
  { A a = A( A( A( f() ) ) ); }

  std::cout << "Dflt = " << t[0] << "\n"
               "Copy = " << t[1] << "\n"
               "CpAs = " << t[2] << "\n"
#if __cplusplus > 201100
               "Move = " << t[3] << "\n"
               "MvAs = " << t[4] << "\n"
#endif
               "Dtor = " << t[5] << '\n';
}

Quatre tests produisent le résultat Élision :

g++ -std=c++98  ; clang++ -std=c++98
g++ -std=c++11  ; clang++ -std=c++11

Deux tests produisent le résultat C++98 :

g++     -std=c++98 -fno-elide-constructors
clang++ -std=c++98 -fno-elide-constructors

Deux tests produisent le résultat C++11 :

g++     -std=c++11 -fno-elide-constructors
clang++ -std=c++11 -fno-elide-constructors
Résultats Élision C++98 C++11
Dflt Constructeur par défaut 1 1 1
Copy Constructeur de copie - 6 -
CpAs Opérateur d’affectation de copie - - -
Move Constructeur de déplacement - - 6
MvAs Opérateur d’affectation de déplacement - - -
Dtor Destructeur 1 7 7

Cas autorisés

Le standard C++11 définit les trois cas suivants pour lesquels le compilateur peut élider le constructeur de copie.

Cas 1 : Un objet temporaire utilisé pour initialiser un autre objet

Ci‐dessous, l’objet temporaire A() peut bénéficier de l’élision. Dans ce cas, le constructeur par défaut A() crée l’argument a directement sur la pile d’appel de la fonction f() :

void f (A a) {}

int main()
{
  f( A() );
} // ^--- objet temporaire

Donc, l’objet temporaire n’est pas copié, mais directement créé à l’emplacement mémoire de destination, comme si le constructeur de copie avait été appelé.

Cas 2  : Retour par valeur pour une variable sur le point de sortir de sa portée

Ce sont les fameux « Return Value Optimization » (RVO) et « Named Return Value Optimization » (NRVO) que nous pourrions traduire par :

  • Optimisation du retour par valeur :

    A rvo()
    {
      return A();
    }
    
    int main()
    {
      A a = rvo();
    }
  • Optimisation du retour par valeur à partir d’une variable nommée :

    A nrvo()
    {
      A a;
      return a;
    }
    
    int main()
    {
      A a = nrvo();
    }

Cas 3 : Levée ou capture d’une exception par valeur

Note : Dans ce cas, GCC ne réalise pas l’élision. Ce n’est pas parce que le standard l’autorise que les compilateurs doivent l’implémenter. Et ce n’est pas parce que le standard C++98 a normalisé cette pratique que les compilateurs le faisaient depuis les années 90. Les compilateurs les plus populaires ont progressivement implémenté ces optimisations au fur des années.

void f()
{
  A a;
  throw a;
}

int main()
{
  try {
    f();
  } catch (A a) {
    //…
  }
}

Problématiques

Ces optimisations laissées à la discrétion des compilateurs posent quelques problèmes ennuyeux. Le TS P0135r0 indique que le fil de discussion sur ISO C++ Future Proposals a produit une longue liste des inconvénients. Ces inconvénients découlent de la formulation actuelle du standard C++ à propos de cette élision. Les améliorations effectuées entre C++98 et C++14 n’ont pas supprimé ces inconvénients. Voici trois inconvénients parmi les plus notables :

Inconvénient 1 : Type indéplaçable

Le code qui exploite l’élision se heurte aux types indéplaçables. Dans l’exemple suivant, la fonction f() retourne un objet par valeur. Le compilateur sait qu’il peut élider le constructeur de copie et que le constructeur de déplacement ne lui est pas utile. Le développeur sait que le compilateur peut le faire. Tous, humains et logiciels sont d’accord pour dire que ce code est possible.
Mais pas le standard !

struct NonDeplacable
{
  NonDeplacable()                     = default;
  NonDeplacable(NonDeplacable const&) = default;
  NonDeplacable(NonDeplacable &&)     = delete;
};  // ^-- pas de constructeur de déplacement

NonDeplacable f()
{
  return NonDeplacable();
  // Clang-3.9 error: call to deleted constructor of 'NonDeplacable'
  // GCC-7     error: use of deleted function 'NonDeplacable::NonDeplacable(NonDeplacable&&)'
}

int main()
{
  NonDeplacable x = f();
}

Pour rappel, cette optimisation est optionnelle. Et donc, le standard C++ en interdisant l’élision dans ce cas, évite du code C++ « valide » qui ne puisse pas être compilé par les compilateurs ne sachant pas élider.

Cette situation rend très difficile (voire impossible) l’implémentation d’une fabrique ou d’un constructeur nommé pour les types non déplaçables.

De plus, le standard ne permet pas non plus l’initialisation par inférence des objets non déplaçables, c’est‐à‐dire avec le mot‐clef auto. Alors que Herb Sutter, animateur en chef du comité de standardisation du C++, préconise de recourir, le plus souvent, au mot‐clef auto.

int main()
{
  auto x = f(); // Erreur
}

Inconvénient 2 : Un code portable est moins performant que l’élision

L’écriture d’un code portable ne peut pas se baser sur une hypothétique optimisation, d’autant plus si la construction de copie présente des effets de bords, chose peu recommandable de fait. Par exemple, un développeur pourrait hésiter entre ces deux approches ci‐dessous.

2.1. Retour par valeur

Si le compilateur n’élide pas, l’appel du constructeur de copie peut devenir pénalisant, donc ce n’est pas portable (pour des questions de performance).

A f1()
{
  A a;
  a.data = 42;
  return a;
}

int main()
{
  A a = f1();
}

Heureusement, le C++11 prend le relai et impose que la valeur retournée soit non pas copiée, mais déplacée. Si l’objet occupe plus de mémoire que ce que son sizeof va retourner (cas des chaînes, des vecteurs, etc.), alors déplacer va faire une différence : on gagne une copie et ce n’est pas négligeable. En revanche, si l’objet est simple et que son sizeof compte vraiment toute l’information qu’il représente (comme un quaternion), déplacer va coûter aussi cher que copier.

2.2. Paramètre de sortie

Le compilateur écrit directement dans la donnée préalablement construite par l’appelant. Cela permet de limiter la casse si le compilateur n’élide pas, mais cela peut être moins performant que l’élision.

void f2 (A& a)
{
  a.data = 42; // on écrit directement dedans

  // ou on déplace par affectation b dans a
  A b;
  a = std::move(b);
}

int main()
{
  A a;
  f2(a);
}

Cette technique présente à la fois des avantages et des inconvénients. Elle est très dépendante du contexte.

D’un côté, le compilateur ne saura plus résoudre correctement les problèmes d’aliasing sur des fonctions non inline.

De l’autre, on évite de construire un A à chaque appel. Si A est un gros objet, un vector<> par exemple, même avec l’élision, à chaque appel on construit et on libère un objet A. L’approche du paramètre sortant f2(A&a) peut à la place redimensionner ce vector<> et le remplir ensuite. Un code appelant f2(A&a) en boucle permet d’économiser des millions d’allocations et de libérations, l’objet A étant alors un cache extérieur. Les gains réalisés ainsi sont intéressants.

Inconvénient 3 : Un code portable est moins naturel

C’est le point le plus important de cette dépêche, l’intérêt d’écrire A f1() plutôt que void f1(A& out). Mais pourquoi donc cette dépêche s’obstine à vouloir écrire A f1() alors que void f1(A& out) fait très bien l’affaire ?

    Car utiliser A f1() est bien plus naturel !

En mathématiques, nous écrivons y = f(x), c’est naturel, notre cerveau est habitué à penser comme cela. Alors pourquoi on devrait écrire f(x_in,y_out) ?

Réponse : Les développeurs sont priés d’écrire f(x_in,y_out) car y = f(x) n’est pas portable (pour les raisons évoquées sur les points précédents).

Ce serait bien de pouvoir coder avec le retour par valeur A f() { return A(); } afin de porter la sémantique d’exécution du logiciel et donc de mieux comprendre et maintenir le code source, non ?

Conclusion

Cette dépêche présente une optimisation intéressante permise par le standard C++, mais qui malheureusement n’est pas vraiment exploitée.

Alors, que faire pour résoudre tous ces inconvénients et démocratiser les possibilités de code offertes par l’élision du constructeur de copie ?

Réponse surprise dans la prochaine dépêche… :-)

Réutilisation

Le texte de cette dépêche est protégé par le droit d’auteur la gauche d’auteur et réutilisable sous licence CC BY-SA 4.0. Les images utilisées sont aussi sous licence libre (cliquer sur l’image pour plus de détails).

Donc, n’hésitez pas à réutiliser ce contenu libre pour créer, par exemple, des supports de formation, des présentations (meetups), des publications sur d’autres blogs, des articles pour des magazines, et aussi un article C++17 sur Wikipédia dès que Wikipédia utilisera la licence CC BY-SA 4.0.   ٩(•‿•)۶

Les auteurs

Par respect de la licence, merci de créditer les nombreux auteurs :

Continuer à améliorer ce document

Malgré tout le soin apporté, il reste certainement des oublis, des ambiguïtés, des fôtes… Bien que cette dépêche restera figée sur le site LinuxFr.org, il est possible de continuer à l’enrichir sur le dépôt Git du Groupe des utilisateurs C++ francophone (C++FRUG). C’est donc sur ce dépôt que se trouve les versions les plus à jour. (ღ˘⌣˘ღ)

Appel à contribution

Nous nous efforçons de vous fournir une dépêche de qualité chaque jour ouvré. Et en plus, avec une illustration sympathique. Mais cela demande beaucoup de travail et tenir les délais n’est pas toujours simple.

Merci de nous donner un coup de main, que ce soit sur la correction orthographique, le dessin, la lecture des spécifications techniques, la rédaction d’une nouvelle dépêche à intégrer au calendrier de l’Avent du C++. Tu peux aussi apporter ta contribution à la dépêche Faut‐il continuer à apprendre le C++ ?.

Rejoins‐nous sur la page du groupe de travail C++ sur LinuxFr.org (un compte est nécessaire pour y accéder).

À suivre…

Aller plus loin

  • # What you see is not what you get

    Posté par  . Évalué à 10.

    Hello,

    Tout d'abord, merci pour cette série d'articles, très denses ! Ça remet les idées en place !

    Et ça fait peur … Sans prétendre au niveau gourou, je pensais avoir un niveau correct en C++. Ici, je m'aperçois que mes fondations ne sont pas si solides.

    Ce qui me dérange le plus, c'est que le code que je lis ne fasse pas ce qu'il semble vouloir dire.

    Pire, selon le compilateur et ses options, le comportement change.

    Dans un autre registre, je déteste particulièrement les templates. À la moindre connerie, je me retrouve avec des dizaines ou centaines d'erreurs de compilation qui me rappellent le Cobol sur AS400 à l'IUT en 1990 …
    Du coup, j'évite d'en écrire. Et je ne les utilise que quand c'est nécessaire (le plus souvent, celles de Qt).

    Mon taff ne me laisse pas le loisir d'apprendre par cœur la norme C++. Qui parmi vous en a le temps ? Qui peut prétendre être un gourou C++ ? Très peu de personnes je pense.

    Jamais je n'aurai toutes ces subtilités en tête.
    À quoi bon mettre tout ça dans la norme si, au mieux, personne ne les utilises, et, au pire, ça génère des prises de tête ou des bugs ?

    • [^] # Re: What you see is not what you get

      Posté par  (site web personnel) . Évalué à 8.

      Le C++ est clairement un langage que plus on le connait, plus on sait à quel point on ne sait pas grand chose. Mais ce n'est pas très grave. Typiquement, ce genre d'optimisation, ne nous change pas vraiment la vie tant que l'on n'est pas à vouloir optimiser très précisément ce qu'il se passe.

      De plus, et c'est particulièrement vrai depuis le C++11, toutes les choses dans la norme ne sont pas directement pour nous, mais pour des concepteurs de bibliothèques qui pourront nous peaufiner des bibliothèques aux petits oignons qu'il ne nous restera plus qu'à utiliser.

      Tous les utilisateurs de Python n'ont pas forcément conscience des dictionnaires qui sont omniprésents et qui expliquent tout ce qu'il est possible de faire avec le langage, en bien ou en mal. Et pourtant, ils n'ont aucun mal à s'en servir. Ici, ce n'est guère différent.

    • [^] # Re: What you see is not what you get

      Posté par  . Évalué à 3.

      Dans un autre registre, je déteste particulièrement les templates. À la moindre connerie, je me retrouve avec des dizaines ou centaines d'erreurs de compilation qui me rappellent le Cobol sur AS400 à l'IUT en 1990 …

      Il y a un secret (en tout cas avec GCC et Clang) pour rendre les erreurs de templates pas trop insurmontables.
      95% du temps, la ligne d'ou provient l'erreur dans ton code est indiquée par la ligne d'erreur "Required from here" de GCC qui va être au début de l'erreur; si tu utilises QtCreator par exemple tu peux double-cliquer dessus et ça t'y emmène dans le code.

      Qui peut prétendre être un gourou C++ ? Très peu de personnes je pense.

      Bjarne lui même a dit qu'il devait se référer au standard pour être sûr de tout.

      À quoi bon mettre tout ça dans la norme si, au mieux, personne ne les utilises,

      Ce sont surtout les auteurs de bibliothèques qui les utilisent.

      Regarde par exemple le code de Eggs.variant ou Boost.Hana

    • [^] # Re: What you see is not what you get

      Posté par  (site web personnel) . Évalué à 4.

      Bonjour,

      Merci pour tes encouragements.
      Je ne sais pas si cela peut te rassurer, mais, nous non plus, nous ne connaissons pas grand chose à la norme C++.

      Pour l’anecdote, un journaliste avait demandé à Bjarne Stroustrup, le fondateur du C++ et membre assidu du comité de normalisation du C++, quel était son niveau de connaissance de la norme. Il avait alors répondu 6 sur 10. Personnellement, je pense que la plupart des personnes qui se disent expert en C++ doivent avoir un niveau de connaissance inférieur à 20 %.

      Pour te rassurer, sache que les membres du comité de normalisation font très attention à ne pas rajouter de complexité inutile au langage. Si une fonctionnalité peut entraîner de mauvaises pratiques de code, alors elle est retravaillée ou écartée. C’est par exemple, le cas du Variable Length Array présent dans le langage C, mais pas dans le C++. C’est aussi pourquoi certaines fonctionnalités très attendues sont encore et encore repoussées, comme import std.string; qui devrait devenir une alternative à #include <string>. La norme C++ se bonifie avec le temps, plus intuitive, moins de pièges.

      Nous allons continuer nos petites dépêches pédagogiques afin de démystifier les template. Objectif pour le 30 novembre 2017 à minuit : ne plus en avoir peur.

      N’hésite pas à nous donner un coup de main ;-)
      (surtout si tu as envie de dessiner)

      Commentaire sous licence Creative Commons Zero CC0 1.0 Universal (Public Domain Dedication)

      • [^] # Re: What you see is not what you get

        Posté par  (site web personnel) . Évalué à 3.

        Personnellement, je pense que la plupart des personnes qui se disent expert en C++
        doivent avoir un niveau de connaissance inférieur à 20 %.

        On (re)tombera facilement dans les pièges comme celui présenté par cette dépêche où l'ordre d'évaluation d'un stream de la dépêche précédente. Il faut juste savoir s'en souvenir quand le comportement n'est pas celui que l'on attendais. Le pire étant une optimisation qui provoque un comportement inattendu dans un cas inattendu.

        Savoir qu'il y a des pièges, pleins, partout et reconnaître que l'on peut ou que l'on va se prendre les pieds dans le tapis à un moment ou à un autre, c'est peut ça « être expert ».

        cas du Variable Length Array présent dans le langage C

        Un autre piège serait de croire qu'un std::vector fait la même chose; sauf que celui-ci va allouer ses éléments sur le tas, alors que le VLA va les allouer sur la pile.

        L'intégration des VLA n'est-il pas toujours en discussion ?

        --
        Une dépêche pour définir une échelle de compétences en C++ ?

        • [^] # Re: What you see is not what you get

          Posté par  (site web personnel) . Évalué à 2.

          L'intégration des VLA n'est-il pas toujours en discussion ?

          On a failli, et puis finalement non. Je n'ai pas suivi ce sujet -> ne pas me demander pourquoi.

          • [^] # Re: What you see is not what you get

            Posté par  (site web personnel) . Évalué à 2.

            Pour information, VLA est le nom pour le langage C. En C++, nous devrions dire Runtime-Sized Arrays.
            Mais bon, VLA est le nom le plus connu. Même les membres du comité de normalisation du C++ disent VLA.
            Du coup, moi aussi je dis VLA (ne multiplions pas les noms qui désignent un peu près la même chose).

            En 2013, le comité a d'abord adopté les VLA (appelé Runtime-sized arrays with automatic storage duration)
            https://isocpp.org/blog/2013/04/n3639-runtime-sized-arrays-with-automatic-storage-duration

            Et puis finalement, retour en arrière. Il me semble que les VLA n'ont pas été intégrés car le risque est trop grand d'allouer plus d'espace que l'espace libre disponible dans la pile d'appel.

            Par contre, le std::dynarray a bien été adopté pour C++14 et permet aussi d'allouer sur la pile.

            Ce qu'il faut retenir, c'est que le comité de normalisation du C++ a tendance à ne pas intégrer ce qui est bugogène (error prone).

            Commentaire sous licence Creative Commons Zero CC0 1.0 Universal (Public Domain Dedication)

            • [^] # Re: What you see is not what you get

              Posté par  (site web personnel) . Évalué à 1. Dernière modification le 12 décembre 2016 à 19:32.

              Après une petite recherche, j'ai trouvé ce chapitre :
              https://wg21.link/p0096#recs.removed

              3.9 Features published and later removed

              3.9.1 Features removed from C++14

              Features in the first CD of C++14, subsequently removed to a Technical Specification

              TS Title
              N3639 Runtime-sized arrays with automatic storage duration
              N3672 A proposal to add a utility class to represent optional objects
              N3662 C++ Dynamic Arrays

              The intention is that an implementation which provides runtime-sized arrays as specified in the first CD should define __cpp_runtime_arrays as 201304. The expectation is that a later document specifying runtime-sized arrays will specify a different value for __cpp_runtime_arrays.

              Donc ni les VLA, ni les std::dynarray, n'ont été intégrés au final dans C++14 (ni d'ailleurs dans C++17).

              Notons que std::optional a été adopté pour C++14, puis retiré, et en ce moment il est à nouveau adopté pour C++17, malgré de vives polémiques… Une dépêche sur ce sujet est prévue pour l'année prochaine ;-)

              Commentaire sous licence Creative Commons Zero CC0 1.0 Universal (Public Domain Dedication)

            • [^] # Re: What you see is not what you get

              Posté par  (site web personnel) . Évalué à 4.

              Il me semble que les VLA n'ont pas été intégrés car le risque est trop grand d'allouer plus d'espace que l'espace libre disponible dans la pile d'appel.

              La taille de la pile est fixe donc, forcément, on peut débordé. C'est exactement le même problème en C. De mémoire, cette pile fait 8 Mo.

              D'ailleurs, pourquoi ne pas allouer cette mémoire "ailleurs" tout en étant en RAII ? De toute façon mettre des objets de tailles variables dans la pile est une très mauvaise idée. (je me rend compte que cela doit revenir à utiliser un vector<> dans la pile)

              "La première sécurité est la liberté"

              • [^] # Re: What you see is not what you get

                Posté par  . Évalué à 2.

                D'ailleurs, pourquoi ne pas allouer cette mémoire "ailleurs" tout en étant en RAII ? De toute façon mettre des objets de tailles variables dans la pile est une très mauvaise idée. (je me rend compte que cela doit revenir à utiliser un vector<> dans la pile)

                La seule bonne raison de le faire est quand tu :
                - es sûr que l’allocation tiendra dans la pile
                - ne veux pas d’allocation dynamique

                Dans un système critique, par exemple, les allocations dans le tas durant l’exécution peuvent être interdites. Du coup, allouer un tableau tampon temporaire dans la pile, si tu es sûr qu’il tient, a du sens.

                Cela dit, je ne sais plus si les VLAs de C permettent de contrôler la taille du tableau avant l’allocation. Il me semble que non, et que c’est justement pour ça que c’est une mauvaise idée.

                Mes commentaires sont en wtfpl. Une licence sur les commentaires, sérieux ? o_0

                • [^] # Re: What you see is not what you get

                  Posté par  (site web personnel) . Évalué à 5.

                  Dans un système critique, par exemple, les allocations dans le tas durant l’exécution peuvent être interdites. Du coup, allouer un tableau tampon temporaire dans la pile, si tu es sûr qu’il tient, a du sens.

                  C'est bien plus propre d'allouer un tableau statique de la taille maximum, cela évite une mauvaise surprise. Même en cas de mémoire contrainte, il faut bien pouvoir allouer la mémoire maximum, autant le faire au lancement du programme. Si tu utilises ce patterns souvent, je ne vois pas comment tu peux prouver que la stack est assez grande. A mon avis, ce genre d'appel est interdit en MISRA C.

                  "La première sécurité est la liberté"

              • [^] # Re: What you see is not what you get

                Posté par  (site web personnel) . Évalué à 8.

                De mémoire, cette pile fait 8 Mo

                Ce n'est pas spécifié et varie selon l'OS, l'architecture, la chaîne de compilation (setrlimit) etc.

                Donc, ça dépend.
                Alors forcement : « ça dépend », ça dépasse.


                -- promptly disappeared in a puff of smoke

                • [^] # Re: What you see is not what you get

                  Posté par  (site web personnel) . Évalué à 3.

                  De mémoire, tu peux le fixer sous Linux, même cela fait parti du binaire sous Windows, mais ce n'est pas conseillé de la changer (mais je ne sais plus pourquoi).

                  Oui, tu peux la changer, mais en pratique, tu ne le fais pas.

                  Donc, moins il y a des choses dans la pile, mieux tu te portes. D'ailleurs, une grosse pile ailleurs dans le tas, pourrait faire l'affaire. Ocaml utilise une pile de 256Ko comme première zone mémoire. L'allocation est ultra rapide, justement parce que c'est une pile. Le GC bosse ensuite, si la donné sort d'un scoop.

                  On pourrait imaginer une allocation RAII, qui utilise une autre pile selon le même genre de fonctionnement.

                  "La première sécurité est la liberté"

                  • [^] # Re: What you see is not what you get

                    Posté par  (site web personnel) . Évalué à 2. Dernière modification le 13 décembre 2016 à 11:57.

                    L'allocation est ultra rapide, justement parce que c'est une pile.

                    Oui, c'est surement la principale raison de l'existence des VLA.
                    ( et d' alloca)

                    Le GC bosse ensuite

                    Bah, dans le cas de la pile, ça ne doit pas coûter plus cher que la restauration du frame pointer.


                    Gros bide avec ma blagounette.

                    • [^] # Re: What you see is not what you get

                      Posté par  (site web personnel) . Évalué à 3.

                      Bah, dans le cas de la pile, ça ne doit pas coûter plus cher que la restauration du frame pointer.

                      Si, parce que c'est un déplacement, la pile est vidé, il faut donc mettre à jour les pointeurs.

                      "La première sécurité est la liberté"

                  • [^] # Re: What you see is not what you get

                    Posté par  (site web personnel) . Évalué à 3.

                    On pourrait imaginer une allocation RAII, qui utilise une autre pile selon le même genre de fonctionnement.

                    Comme C++ permet assez facilement d'utiliser un allocateur maison, j'ai eu à faire ce genre de chose: pré-allocation de blocs et une gestion de piles sur ceux-ci.

                    Pour un truc plus abouti, les TBB d'Intel sont une implémentation intéressante.

                • [^] # Re: What you see is not what you get

                  Posté par  . Évalué à 4.

                  J'ai peut-être raté quelque chose, mais je ne suis même pas sûr que la spec de C99 (ou C11) parle de pile. Elle parle juste de VLA. La raison étant qu'entre le moment où C a été créé, et où il a été normalisé, certains éditeurs de compilateurs C ont porté le langage sur des machines qui n'avaient pas à proprement parle de pile. Et ces machines étaient utilisées en production. Du coup, lors de sa normalisation, le comité ne pouvait pas assumer l'existence d'une pile ou d'un tas (même si c'est comme ça que 99,9% des implémentations sont/étaient faites).

                  Concernant la taille de la pile, ça dépend de l'archi (genre sur IA-64, je crois que la pile était ridiculement petite, genre 15kio, entre autres parce qu'il y avait 128 registres entiers et 128 registres flottants, et même en utilisant ulimit ou autres commandes, on ne pouvait pas la faire trop grossir).

                  • [^] # Re: What you see is not what you get

                    Posté par  (site web personnel) . Évalué à 2.

                    mais je ne suis même pas sûr que la spec de C99 (ou C11) parle de pile

                    En effet, la norme ne spécifie que sur la portée d'un identifiant (« scope ») et sa durée de vie (« storage duration »).

                    Les VLA sont même un cas particulier de la catégorie « automatic storage duration », ils ne vivent pas au delà du « bloc » de leur déclaration.

                    Si la suite est du détail d'implémentation, le fond de l'affaire reste le même: déclarer un VLA, ce n'est pas «allouer dynamiquement» un tableau de taille variable. La durée de vie n'est pas la même.

                  • [^] # Re: What you see is not what you get

                    Posté par  (site web personnel) . Évalué à 3.

                    certains éditeurs de compilateurs C ont porté le langage sur des machines qui n'avaient pas à proprement parle de pile.

                    Tu parles peut être d'une gestion hardware d'une pile. Sur un processeur RISC, il n'y pas forcément de gestion hardware spécifique, il y a juste une convention d'usage de registre. Mais je ne vois pas comment tu peux fonctionner sans pile du tout. Il faut bien gérer les adresses de retour des fonctions.

                    Concernant la taille de la pile, ça dépend de l'archi (genre sur IA-64, je crois que la pile était ridiculement petite, genre 15kio

                    Cela m'étonnerait beaucoup. ça limiterait beaucoup trop la profondeur d'appel de fonction (appel récursif par exemple) (2Mb ici : https://blogs.msdn.microsoft.com/slavao/2005/03/19/be-aware-ia64-stack-size/ ). Comme rappelé dans le poste du blog, avoir une stack minuscule peut être utile si on utilise plein de threads (4ko par exemple à condition de ne rien allouer dedans).

                    "La première sécurité est la liberté"

              • [^] # Re: What you see is not what you get

                Posté par  (site web personnel) . Évalué à 1.

                D'ailleurs, pourquoi ne pas allouer cette mémoire "ailleurs" tout en étant en RAII ?

                C'est ce que fait std::vector, non ?
                Une entête allouée sur la pile dont les éléments sont alloués sur le tas.

      • [^] # Re: What you see is not what you get

        Posté par  (site web personnel) . Évalué à 8.

        Nous allons continuer nos petites dépêches pédagogiques afin de démystifier les template. Objectif pour le 30 novembre 2017 à minuit : ne plus en avoir peur.

        J'en profite pour dire merci pour ces dépêches que je trouve à la fois intéressantes et bien écrites, bravo aux contributeurs. Il fut une époque où je me considérais pas trop mauvais en c++ mais c'était il y a fort longtemps (bien avant le c++11) et ces articles tombent vraiment à point pour redonner envie de sortir de ma zone de confort c++98 :)

  • # euh ?

    Posté par  (site web personnel) . Évalué à 6.

    Quand j'ai commencé à aimer C++, j'ai eu vent de toutes les subtilités des constructeurs. Et j'en avais conclus que c'était imbitable pour des fonctionnalités aussi basiques. Et je suis retourné au C et Perl.

    Plus généralement, et contrairement au langage C, le standard C++ autorise le compilateur à effectuer toutes les optimisations possibles du moment que le résultat soit conforme à celui attendu par le code source.

    Source ? Vous pensez à quoi ?

    "La première sécurité est la liberté"

    • [^] # Re: euh ?

      Posté par  . Évalué à 1.

      Par exemple le compilateur peut réordonner le flot des instructions un peu comme bon lui semble tant que ça fait le bon résultat à la fin : https://godbolt.org/g/bvePqx

    • [^] # Re: euh ?

      Posté par  (site web personnel) . Évalué à 2.

      Plus généralement, et contrairement au langage C, le standard C++ autorise le compilateur à effectuer
      toutes les optimisations possibles du moment que le résultat soit conforme à celui attendu par le code
      source.

      Source ? Vous pensez à quoi ?

      Je trouve en effet que l'exemple donnée d' élision du constructeur par copie par le compilateur est le contraire de ce qui est attendu par le « code source ». Le code source demande explicitement à la construction par copie de modifier une donnée de l'objet source. Ce que le compilateur va ignorer.

      C'est clairement une chausse-trappe.

      • [^] # Re: euh ?

        Posté par  (site web personnel) . Évalué à 5.

        Comme dit dans la phrase juste après que j'ai copié, le C++ semble définir une sémantique "attendue" pour le constructeur, qui permet des optimisations. Par contre, les compilateur ne contrôle en rien cette attente.

        Pour un designer de langage, c'est la facilité : une faiblesse de génération est compensé par une optimisation qui fait des hypothèses sur le code, mais il est impossible de vérifier que le code respecte effectivement l’hypothèse.

        Il manque définitivement un outil qui vérifie ce genre d'erreur, c'est pas humainement possible de tout se rappeler.

        Le C tient en une centaine de page d'explication. Ocaml a peine plus, si on regarde sa doc en ligne.

        A voir, si le C++ "moderne" peut se réduire à moins de 1000 pages…

        "La première sécurité est la liberté"

        • [^] # Re: euh ?

          Posté par  . Évalué à 5.

          En fait le truc, c’est que dans la vraie vie, tu t’en tapes. C’est à dire que le « problème » n’intervient que si le constructeur (de copie) a des effets de bord. Or c’est une très mauvaise pratique qu’il en ait.

          On pourrait imaginer dans le futur pouvoir « marquer » de tels constructeurs, pour que le compilateur n’applique pas ces optimisations dessus. Toutefois, le fait que ça ne soit pas le cas montre surtout que le besoin est plutôt inexistant.

          Mes commentaires sont en wtfpl. Une licence sur les commentaires, sérieux ? o_0

          • [^] # Re: euh ?

            Posté par  (site web personnel) . Évalué à 3.

            ok. Mes lectures remontent à loin, mais dans mon souvenir, il fallait faire attention à la manière de manipuler les objets pour éviter des recopies inutiles, par exemple. Il fallait ruser pour éviter des appels de constructeur en cascade. Mais c'est vrai que ce n'est pas le sujet de l'article.

            "La première sécurité est la liberté"

          • [^] # Re: euh ? [spoiler]

            Posté par  (site web personnel) . Évalué à 1.

            On pourrait imaginer dans le futur pouvoir « marquer » de tels constructeurs,
            pour que le compilateur n’applique pas ces optimisations dessus

            D'après ce que j'ai compris, c++17 va garantir l'élision par défaut.
            Du coup, pour obtenir la copie d'une source, obtenue en modifiant une ou plusieurs données de la source, il faudra penser à changer de méthode.

            Pour être franc, je n'ai pas de cas comme celui-là en tête.

    • [^] # Re: euh ?

      Posté par  (site web personnel) . Évalué à 6.

      Au plus je lis ces séries d'articles, au plus ça confirme le sentiment que j'avais que C++ n'est pas pour moi. Beaucoup trop verbeux et trop compliqué, et les histoires de constructeur de copie sont très symptomatiques de tous les pièges que réserve ce gros langage.

      • [^] # Re: euh ?

        Posté par  (site web personnel) . Évalué à 2.

        En pratique tu ne tombes pas dans les pièges mentionnés, car tu ne joues pas à ça (un constructeur de copie est utile pour copier, pas pour modifier; quand une commande modifie, on n'a pas pour habitude de l'imbriquer n'importe comment).

        Ici, ce sont des cas d'école, fait pour s'amuser et voir nos limites en connaissance, pas pour l'usage de tous les jours.

        Donc c'est une mauvaise excuse de se baser la dessus pour dire que ce n'est pas fait pour soit.
        (et si tu veux jouer, je pense qu'on pourrait faire les mêmes dépêches sur les "pièges" tarabiscotés avec que n'importe quel langage ayant un peu d'âge, bref un truc ayant vécu et dont ont a une histoire, ce qui arrive à n'importe quel langage utilisé)

        • [^] # Re: euh ?

          Posté par  (site web personnel) . Évalué à 2.

          (et si tu veux jouer, je pense qu'on pourrait faire les mêmes dépêches sur les "pièges" tarabiscotés avec que n'importe quel langage ayant un peu d'âge, bref un truc ayant vécu et dont ont a une histoire, ce qui arrive à n'importe quel langage utilisé)

          Oui, mais quand on risque un crash ou une différence de sémantique par rapport au contenu du code, c'est quand même du lourd (plus que des trucs ésotérique dans les regex perl par exemple).

          "La première sécurité est la liberté"

        • [^] # Re: euh ?

          Posté par  . Évalué à 5.

          En pratique tu ne tombes pas dans les pièges mentionnés, car tu ne joues pas à ça (un constructeur de copie est utile pour copier, pas pour modifier; quand une commande modifie, on n'a pas pour habitude de l'imbriquer n'importe comment).

          Moi en pratique, je n'ai pas encore véritablement compris le constructeur de déplacement donc les cas tordus comme expliqué au dessus, c'est vraiment de la théorie…

          Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

        • [^] # Re: euh ?

          Posté par  (site web personnel) . Évalué à -1.

          Donc c'est une mauvaise excuse de se baser la dessus pour dire que ce n'est pas fait pour soit.

          J'ai bien dis que c'était une confirmation. Chaque article sur le C++ que je lis sur LinuxFR ce mois-ci en est une. Et les constructeurs de copie ont toujours été un problème dans C++, au delà du petit cas pathologique expliqué dans cet article.

          • [^] # Re: euh ?

            Posté par  (site web personnel) . Évalué à 3.

            Quel est le problème avec les constructeurs de copie ? Qu'il faut savoir quand il faut les définir et quand il faut les interdire ? Ce n'est franchement pas très compliqué.

            C'est avant tout un problème de design : savoir distinguer les entités des valeurs, problème qui se pose aussi avec d'autres langages sous d'autres formes: ainsi, cf. les values objects qui ne doivent pas être dérivés en Java (p.ex. un point coloré n'est pas un point, mais il est composé d'un point—cf Effective Java v2).

            Certes en C++, un mauvais design va résulter en un plantage plus vite qu'avec d'autres langages qui vont juste se contenter de nous laisser écrire du code susceptible de produire des résultats aberrants.

  • # Typo

    Posté par  (site web personnel) . Évalué à 3.

    …C++ part du principe que les développeur*S* code*NT* …

    Merci pour les dépêches, c'est toujours intéressant d'apprendre ces détails :)

    PS: je ne suis pas super fan des illustrations de cette série de dépêches…

    • [^] # Re: Typo

      Posté par  . Évalué à 2.

      se serait/ce serait/g

      "Quand certains râlent contre systemd, d'autres s'attaquent aux vrais problèmes." (merci Sinma !)

    • [^] # Re: Typo

      Posté par  (site web personnel) . Évalué à 1.

      Merci pour la coquille ;-)
      As-tu des idées pour les illustrations ?
      On ne sait pas dessiner (on débute) et on aimerait illustrer chaque dépêche avec un petit truc sympa.
      Merci de nous aider à améliorer cet aspect esthétique des dépêches C++.

      Commentaire sous licence Creative Commons Zero CC0 1.0 Universal (Public Domain Dedication)

      • [^] # Re: Typo

        Posté par  (site web personnel) . Évalué à 1.

        Je m'attendais à cette réponse :) Non, je n'ai pas d'idée pour améliorer les illustrations. Je pourrais contribuer, c'est vrai, mais ça ne serait pas mieux. En l'état, je ne suis pas convaincu que ces illustrations apportent au contenu qui se suffit très bien (clair, précis, complet).

        • [^] # Re: Typo

          Posté par  (site web personnel) . Évalué à 3.

          Espérons que d'autres lecteurs apprécient ces dessins car je passe beaucoup de temps à les retoucher sur GIMP pour m'assurer que l'arrière plan est bien blanc, réfléchir aux textes, les retoucher… c'est du temps en moins sur la dépêche.

          Sur la prochaine dépêche, on demandera aux lecteurs de LinuxFr.org de donner leur avis (du moins, ceux qui ont un compte).

          Commentaire sous licence Creative Commons Zero CC0 1.0 Universal (Public Domain Dedication)

          • [^] # Re: Typo

            Posté par  (site web personnel) . Évalué à 1.

            Espérons que d'autres lecteurs apprécient ces dessins

            Au moins une personne apprécie.

            l'arrière plan est bien blanc

            l'arrière plan devrait être transparent afin de s'adapter au fond de la CSS utilisée.

            • [^] # Re: Typo

              Posté par  . Évalué à 5.

              Au moins une personne apprécie.

              Au moins deux.

              l'arrière plan devrait être transparent afin de s'adapter au fond de la CSS utilisée.

              Si ta css met un fond noir, le dessin sera illisible. De manière générale, soit tu fixes l’arrière plan et l’avant plan, soit tu n’en fixes aucun, mais en fixer un seul sur les deux, c’est très pénible (quiconque a déjà utilisé un thème « sombre » pour naviguer maudit les dev webs qui ne prennent pas cette précaution élémentaire).

              Mes commentaires sont en wtfpl. Une licence sur les commentaires, sérieux ? o_0

            • [^] # Re: Typo

              Posté par  (site web personnel) . Évalué à 3.

              Très bonne idée l'arrière plan transparent.

              Pour les fichiers PNG : Je vais laisser une aura blanche autours des traits noirs, et ce blanc va s'estomper (être de plus en plus transparent) au fur et à mesure de l'éloignement du trait noir. J'avais déjà fait cela pour du WebP, mais malheureusement Firefox ne prend pas encore en charge ce format :-(
              https://github.com/cpp-frug/materials/blob/gh-pages/images/README.md#c17-sauve-une-%C3%A9coli%C3%A8re

              Le choix va aussi dépendre de la taille du fichier. Actuellement j'arrive à produire des fichiers de quelques dizaines de Ko. Il ne faudrait pas que cela deviennent des centaines de Ko… J'ai été traumatisé par mon mobile qui essayait désespéramment de télécharger des images dans les transports parisiens !

              Pour les fichiers SVG : Je mets, en arrière plan, un rectangle blanc de la taille de l'image avec opacity=30%. Et quand j'y pense, je rajoute un fin trait blanc autour des caractères…

              Si vous avez des astuces, des techniques, des conseils… pour faire cela, merci de partager ;-)

              Merci pour le retour d'expérience

              Commentaire sous licence Creative Commons Zero CC0 1.0 Universal (Public Domain Dedication)

  • # Initialisation des variables

    Posté par  (site web personnel) . Évalué à 3.

    int t[6] = { 0, 0, 0, 0, 0, 0 };

    Pourquoi pas

    int t[6];

    dans ce cas précis, ou même

    int t[6]={};

    pour un usage plus général, quelque soit la portée de la variable.

    • [^] # Re: Initialisation des variables

      Posté par  (site web personnel) . Évalué à 2.

      Oui, c’est une très bonne idée.
      Il faut absolument que tu nous donnes un coup de main pour nous aider en amont.
      => Lors de la rédaction des dépêches.
      Rendez-vous sur cette page :
      http://linuxfr.org/redaction/news/c-point-de-rassemblement-des-contributeurs-aux-depeches-c

      David Marec a écrit :
      D’après ce que j’ai compris, c++17 va garantir l’élision par défaut.

      Et ne vend pas trop la mèche sur la prochaine dépêche, chut c’est une surprise ;-)

      Commentaire sous licence Creative Commons Zero CC0 1.0 Universal (Public Domain Dedication)

    • [^] # Re: Initialisation des variables

      Posté par  . Évalué à 3.

      Je croyais que c'était pas garantie ce comportement ?

      Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

      • [^] # Re: Initialisation des variables

        Posté par  (site web personnel) . Évalué à 2.

        Lequel ?

        Pour l'initialisation des variables « non locale »:

        [§3.7.2]
        Variables with static storage duration (3.7.1) or thread storage duration (3.7.2) shall be
        zero-initialized (8.5) before any other initialization takes place.

        Pour l'initialisation par une liste, c'est plus touffu:

        [§8.5]
        To zero-initialize an object or reference of type Tmeans:
        —if T is an array type, each element is zero-initialized;
        [§8.5.1 Aggregates]
        [§8.5.4]
        if the initializer list has no elements and T is an aggregate, each of the members of T is initialized
        from an empty initializer list.


        La norme que j'ai lue se trouve sur openstd

  • # questions sur les constructeurs et les vecteurs

    Posté par  (site web personnel) . Évalué à 3.

    Bonjour,

    J’en profite parce que je suis tombé deux trois fois sur des soucis bizarres en ajoutant l’utilisation d’objets et de vecteurs de ceux-ci dans le code de Lugaru (dépêche en cours de rédaction).
    En gros ya des classes d’objet mal foutues et donc pas copiables, genre parce que ça alloue des trucs dans le constructeur et les libère dans le destructeurs, et copie = double destruction.
    En faisant un vecteur de ça et un push_back(Object(args)), j’obtiens un crash car ça crée un objet, le copie puis détruit l’original.
    Je me dis qu’il suffit de remplacer par emplace_back(args), mais non, crash quand même, emplace_back appelle un destructeur je comprends pas pourquoi.
    La solution que j’ai trouvé est de faire un vecteur de unique_ptr à la place.

    En gros mes questions :
    - Comment fonction emplace_back, pourquoi il appelle le destructeur
    - Comment expliciter qu’une classe n’est pas copiable ?
    - Peut-on faire des vecteurs de classe non-copiable ?

    Note : le code en question est en c++11 pour l’instant.

    • [^] # Re: questions sur les constructeurs et les vecteurs

      Posté par  (site web personnel) . Évalué à 1. Dernière modification le 15 décembre 2016 à 11:48.

      Si ta classe possède des ressources, il est nécessaire de prévoir comment tu la dupliques. Si tu autorises alors il faut définir le constructeur de copie et l'opérateur d'affectation par copie (plus éventuellement les équivalents en déplacement si tu veux en profiter), sinon, tu interdis. Comment? Réponse dans la FAQ: http://cpp.developpez.com/faq/cpp/?page=Semantique-de-copie

      Dans tous les cas, il te faut un accès au code de ces classes pour spécifier comment la copie et le déplacement se font (ou pas).

    • [^] # Re: questions sur les constructeurs et les vecteurs

      Posté par  . Évalué à 2.

      emplace_back construit un objet avec les paramètres qu’on lui donne. Mais c’est une construction classique, ce que ça évite, c’est de construire l’objet puis le copier, en deux étapes. Lorsque qu’un vecteur réalloue, il copie puis détruit (ou il déplace).

      Pour marquer qu’un objet n’est pas copiable, il faut « deleter » son constructeur de copie, ainsi que l’opérateur d’affectation :

      class NonCopyable {
          ...
          NonCopyable(NonCopyable const& other)=delete;
          NonCopyable & operator=(NonCopyable const& other)=delete;
          ...
      };

      Pour faire des vecteurs de classe non-copiable, il faut que l’objet soit movable, c’est à dire, qu’il ait un constructeur de mouvement défini. Une autre solution est de passer par un unique_ptr, comme tu l’as fait.

      Mes commentaires sont en wtfpl. Une licence sur les commentaires, sérieux ? o_0

      • [^] # Re: questions sur les constructeurs et les vecteurs

        Posté par  (site web personnel) . Évalué à 2.

        Merci pour les infos (Et à lmg HS au dessus).

        Je pense que je supprimerais les constructeurs par copie et que j’utiliserai unique_ptr alors, c’est plus simple.
        Je sais que les classes devraient être corrigées mais en général c’est des trucs que je compte nettoyer plus tard de toutes façons, je veux juste pouvoir les utiliser dans des vecteurs en attendant.

    • [^] # Re: questions sur les constructeurs et les vecteurs

      Posté par  (site web personnel) . Évalué à 3.

      En gros mes questions :
      - Comment fonction emplace_back,

      emplace ajoute l'élément qu'il va construire à la volée avec les arguments donnés.

      pourquoi il appelle le destructeur

      C'est le vector qui faire des copies lorsqu'il va devoir grossir; donc, tout reconstruire ailleurs.

      ex:

      #include <iostream>
      #include <vector>
      #include <string>
      class test
      {
              public:
                      test(size_t indice):_id(indice) {
                              _nr=new int[5];
                              std::cout << _id << " initialized." << std::endl;
                      }
                      ~test() {
                              delete[] _nr;
                              std::cout << _id << " removed." << std::endl;
                      }
              private:
                      int* _nr;
                      size_t _id;
      };
      
      
      int main() {
              std::vector<test> v;
              v.reserve(10);
              for(size_t i(0);i<10;++i)
              {
                      v.emplace_back (i);
              }
              return 0;
      }

      Diminuer le reserve ( voire l'enlever) fait tout planter.

      • [^] # Re: questions sur les constructeurs et les vecteurs

        Posté par  (site web personnel) . Évalué à 2.

        emplace ajoute l'élément qu'il va construire à la volée avec les arguments donnés.

        Je dirais même: directement dans la destination (déjà allouée). Comme le nom l'indique, cela fait de la construction placée avec le new de placement. Mais bon. C'est du détail arcanique. Il est plus important de comprendre dans un premier temps comment on écrit des classes valeurs et savoir distinguer les entités (celles qui n'ont pas besoin d'/ne doivent pas être copiées)—David illustre un cas où le problème peut se poser, celui que tu observes.

  • # Lien cassé

    Posté par  (site web personnel) . Évalué à 1.

    Le lien « constructeur nommé » est cassé (lien relatif au lieu d'absolu). Le lien correct est :

    http://cpp.developpez.com/faq/cpp/?page=Les-constructeurs#Qu-est-ce-que-l-idiome-du-constructeur-nomme-Named-Constructor

    Si un modérateur passe par là …

  • # Test avec le compilateur CL de Microsoft en toolset vc140

    Posté par  (site web personnel, Mastodon) . Évalué à 3.

    Hello,

    Pour le fun j'ai testé le code suivant avec le compilateur CL de Microsoft en toolset vc140.

    #include <iostream>
    
    int t[6] = { 0, 0, 0, 0, 0, 0 };
    
    struct A
    {
      A () { ++t[0]; }
      A (A const&) { ++t[1]; }
      A& operator=(A const&) { ++t[2]; return *this; }
    //#if __cplusplus > 201100
      A (A &&) { ++t[3]; }
      A& operator=(A &&) { ++t[4]; return *this; }
    //#endif
      ~A () { ++t[5]; }
    };
    
    A f () { return A (A ()); }
    
    int main ()
    {
      { A a = A (A (A (f ()))); }
    
      std::cout << "Dflt = " << t[0] << "\n"
        "Copy = " << t[1] << "\n"
        "CpAs = " << t[2] << "\n"
    //#if __cplusplus > 201100
        "Move = " << t[3] << "\n"
        "MvAs = " << t[4] << "\n"
    //#endif
        "Dtor = " << t[5] << '\n';
    }

    Résultats :

    Dflt = 1
    Copy = 0
    CpAs = 0
    Move = 3
    MvAs = 0
    Dtor = 4
    

    A priori CL ne sait pas faire l'élision aussi efficacement que g++ et clang++ dans ce cas de figure.

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.