Outils pour utilisateurs

Outils du site


lang:cpp:classes

Différences

Ci-dessous, les différences entre deux révisions de la page.

Lien vers cette vue comparative

Les deux révisions précédentesRévision précédente
Prochaine révision
Révision précédente
lang:cpp:classes [2020/04/17 23:35] – Déplacement de rubriques dans la nouvelle catégorie Hack rootlang:cpp:classes [2022/07/04 10:05] (Version actuelle) – [Destructeur] : lock_guard->scoped_lock root
Ligne 1: Ligne 1:
 =====Sans héritage===== =====Sans héritage=====
-====Constructeurs / destructeurs==== + 
-===Version "Rule of Zero"===+====Multiples constructeurs==== 
 +Il est possible d'appeler un constructeur depuis un autre constructeur. C'est pratique si on souhaite avoir deux constructeurs très proche. 
 + 
 +Bien qu'on soit dans le paragraphe sans héritage, j'ai mis un cas avec héritage pour montrer qu'on a pas besoin d'appeler le constructeur du parent si on appelle un autre constructeur. 
 + 
 +<code cpp> 
 +class A{}; 
 +class B : public A 
 +
 + public: 
 +  // Call constructor B(int) from constructor B(). 
 +  // No need (forbidden) to call A. 
 +  B() : B(5){} 
 +  // Avoid default function argument 
 +  B(int a/* = 5*/):A(), a_(a){} 
 + 
 + private: 
 +  int a_; 
 +}; 
 +</code> 
 + 
 +====Version "Rule of Zero"===
 + 
 +Cette règle s'applique lorsque la classe ne gère pas de ressources. 
 + 
 +Il est aussi conseillé de réduire son utilisation aux classes n'étant pas polymorphique pour éviter les problèmes de slicing. 
 <code cpp> <code cpp>
 class Class class Class
Ligne 8: Ligne 34:
   // Constructor.   // Constructor.
   Class() = default;   Class() = default;
 +  // Pas de destructeur. Pas de copy/move contructor/assignment operator
 } }
 </code> </code>
  
 Le post originel (?) sur la règle du zéro : <del>[[http://rmartinho.github.com/cxx11/2012/08/15/rule-of-zero.html|Rule of Zero]]</del> {{ :lang:cpp:class:rule_of_zero_flaming_dangerzone_2020-03-10_11_08_47_am_.html |Archive du 10/03/2020 le 20/09/2012}} Le post originel (?) sur la règle du zéro : <del>[[http://rmartinho.github.com/cxx11/2012/08/15/rule-of-zero.html|Rule of Zero]]</del> {{ :lang:cpp:class:rule_of_zero_flaming_dangerzone_2020-03-10_11_08_47_am_.html |Archive du 10/03/2020 le 20/09/2012}}
- 
-C'est la version idéale et ça devrait être le compartiment par défaut. S'il est nécessaire de mettre des fonctions dans le destructeur, c'est peut-être que certaines données devraient être encapsulées dans une classe spécifique respectant le RAII. 
  
 <blockquote> <blockquote>
Ligne 24: Ligne 49:
 <cite>[[https://stackoverflow.com/questions/40959659/c-rule-of-zero-what-is-user-declared-constructor|C++ Rule of Zero & what is "user-declared" constructor?]] {{ :lang:cpp:class:c_rule_of_zero_what_is_user-declared_constructor_-_stack_overflow_2020-03-09_23_01_28_.html |Archive du 04/12/2016 le 09/03/2020}}</cite></blockquote> <cite>[[https://stackoverflow.com/questions/40959659/c-rule-of-zero-what-is-user-declared-constructor|C++ Rule of Zero & what is "user-declared" constructor?]] {{ :lang:cpp:class:c_rule_of_zero_what_is_user-declared_constructor_-_stack_overflow_2020-03-09_23_01_28_.html |Archive du 04/12/2016 le 09/03/2020}}</cite></blockquote>
  
-Par "does not have a user-declared", cela signifie aussi ne pas utiliser ''= default;''.+Par ''does not have a user-declared'', cela signifie aussi ne pas utiliser ''= default;''. 
 + 
 +Autre explication : [[https://www.fluentcpp.com/2019/04/23/the-rule-of-zero-zero-constructor-zero-calorie/|The Rule of Zero in C++]] {{ :lang:cpp:class:the_rule_of_zero_in_c_-_fluent_c_2019-12-18_23_16_30_.html |Archive du 23/04/2019 le 18/12/2019}} 
 + 
 +====Version "Rule of five"====
  
-===Version "Rule of five"===+Cette version est nécessaire si la classe gère des ressources ou si la classe est polymorphique.
  
 <code cpp> <code cpp>
Ligne 46: Ligne 75:
 } }
 </code> </code>
-L'intérêt de tous les ''delete'' est de maitriser le compilateur afin qu'il ne décide pas d'utiliser l'opérateur copie à la place de l'opérateur move. 
  
-La règle des 5 impose de définir ces 5 opérateurs. Pour des raisons de simplicité lors de la déclaration initiale d'une classe, je les déclare ''delete'' puis je les instancie lorsque nécessaire.+<WRAP center round info 60%> 
 +Cette règle impose de définir ces 5 méthodes. Pour des raisons de simplicité lors de la déclaration initiale d'une classe, je déclare le constructeur ''= default'' et les 4 autres en ''delete'' puis je les implémente lorsque nécessaire.
  
-[[https://www.fluentcpp.com/2019/04/23/the-rule-of-zero-zero-constructor-zero-calorie/|The Rule of Zero in C++]] {{ :lang:cpp:class:the_rule_of_zero_in_c_-_fluent_c_2019-12-18_23_16_30_.html |Archive du 23/04/2019 le 18/12/2019}}+L'intérêt de tous les ''delete'' est de maitriser le compilateur et de n'écrire que le code nécessaire.
  
 +</WRAP>
 +
 +<WRAP center round info 60%>
 Il est préférable d'avoir les opérations ''move'' en ''noexcept''. Sinon, le compilateur peut décider d'appeler l'opérateur copie à la place. Il est préférable d'avoir les opérations ''move'' en ''noexcept''. Sinon, le compilateur peut décider d'appeler l'opérateur copie à la place.
  
 [[http://www.hlsl.co.uk/blog/2017/12/1/c-noexcept-and-move-constructors-effect-on-performance-in-stl-containers| C++ noexcept and move constructors effect on performance in STL Containers]] {{ :lang:cpp:class:c_noexcept_and_move_constructors_effect_on_performance_in_stl_containers_trying_to_find_the_obvious_2019-12-18_23_16_48_.html |Archive du 01/12/2017 le 18/12/2019}} [[http://www.hlsl.co.uk/blog/2017/12/1/c-noexcept-and-move-constructors-effect-on-performance-in-stl-containers| C++ noexcept and move constructors effect on performance in STL Containers]] {{ :lang:cpp:class:c_noexcept_and_move_constructors_effect_on_performance_in_stl_containers_trying_to_find_the_obvious_2019-12-18_23_16_48_.html |Archive du 01/12/2017 le 18/12/2019}}
 +</WRAP>
 +
 +===Destructeur===
 +
 +Une classe doit avoir un destruteur virtuel si l'héritage est utilisée et que l'une des classes (elle, enfant ou parent) gère des ressources.
 +
 +L'attribut ''virtual'' doit être placé dans la classe la plus parent.
 +
 +<code cpp>
 +struct A
 +{
 +  ~A() = default;
 +};
 +
 +struct B : public A
 +{
 +  virtual ~B() {std::cout << "coucou" << std::endl;}
 +};
 +
 +int main( int argc, char** argv )
 +{
 +  A* t = new B();
 +  // On n'appelle pas "coucou" bien que son destructeur soit virtuel
 +  delete t;
 +}
 +</code>
 +
 +Il faut de même utiliser ''virtual'' si l'un des membres est une classe qui gère des ressources.
 +
 +<code cpp>
 +class TestClass
 +{
 + public:
 +  TestClass() {std::cout << "const" << std::endl;}
 +  ~TestClass() {std::cout << "destruct" << std::endl;}
 +};
 +
 +struct A
 +{
 +  ~A() = default;
 +};
 +
 +struct B : public A
 +{
 +  TestClass t;
 +};
 +
 +int main( int argc, char** argv )
 +{
 +  A* a = new B();
 +  // Le destructeur de TestClass de B n'est pas appelé.
 +  delete a;
 +}
 +
 +</code>
 +
 +Sortie sans le destructeur de ''A'' ''virtual'' :
 +
 +<code>
 +const
 +</code>
 +
 +  * Durée de vie d'un objet dans la pile
 +
 +Si c'est un objet explicitement déclaré, le destructeur sera appelé lors du bloc fermant.
 +
 +<code cpp>
 +int main()
 +{
 +  A a;
 +  {
 +    B b;
 +    // On appelle ~B()
 +  }
 +  // On appelle ~A()
 +}
 +</code>
 +
 +La technique de mettre un bloc de crochet pour forcer un appel du destructeur à un moment précis est utilisé fréquemment par les ''std::scoped_lock''.
 +
 +<code cpp>
 +int main()
 +{
 +  std::mutex io_mutex;
 +  {
 +    std::scoped_lock<std::mutex> lk(io_mutex);
 +    std::cout << e1.id << " and " << e2.id << " are waiting for locks" << std::endl;
 +    // le mutex est automatiquement libéré lors de l'appel du destructeur de lk.
 +  }
 +  return 1;
 +}
 +</code>
 +
 +Si l'objet est temporaire, le destructeur sera appelé lorsque la totalité de l'instruction sera finie.
 +
 +<code cpp>
 +#include <iostream>
 +
 +class A
 +{
 +public:
 +  A() noexcept { std::cout << "A\n"; }
 +  ~A() noexcept { std::cout << "~A\n"; }
 +  int foo() { return 1; }
 +};
 +
 +int foo2(int a)
 +{
 +  std::cout << "int\n";
 +  return a;
 +}
 +
 +int main()
 +{
 +  std::cout << "0\n";
 +  foo2(foo2(A().foo()));
 +  // Le destructeur de A est appelé ici.
 +  std::cout << "1\n";
 +}
 +</code>
 +
 +Affichage :
 +
 +<code>
 +0
 +A
 +int
 +int
 +~A
 +1
 +</code>
 +
 +===Les 4 opérateurs copy/move constructor/assignment operator===
 +
 +On part du principe que les 4 opérateurs sont toujours définis explicitement (y compris via ''= default'').
 +
 +  * Par défault, ''copy'' et ''move'' font la même chose
 +
 +Avec l'implémentation ''= default'', la différence entre ''copy'' et ''move'' est le fait qu'avec ''move'', on s'épargne une allocation mémoire.
 +
 +<WRAP center round important 60%>
 +Contrairement aux classes qui définissent les opérateurs ''move'', la version par défaut ''move'' ne modifie pas l'objet initial.
 +</WRAP>
 +
 +Exemple avec ''move = default'' :
 +
 +<code cpp>
 +#include <algorithm>
 +
 +class TestClass
 +{
 + public:
 +  TestClass() {a_ = new int[1];}
 +  TestClass(TestClass const& other) = delete;
 +  TestClass(TestClass && other) noexcept = default;
 +  TestClass& operator=(TestClass const& other) = delete;
 +  TestClass& operator=(TestClass && other) noexcept = default;
 +  ~TestClass() {delete[] a_;}
 + public:
 +  int* a_ = nullptr;
 +};
 +
 +int main( int argc, char** argv )
 +{
 +  TestClass t;
 +  TestClass t2 = std::move(t);
 +  // Après std::move, t.a_ vaut toujours la valeur d'origine car le constructeur copy vaut default.
 +  // Il y a donc un double delete.
 +}
 +</code>
 +
 +Exemple avec ''move'' et ''std::exchange'' :
 +
 +<code cpp>
 +#include <algorithm>
 +#include <utility>
 +
 +class TestClass
 +{
 + public:
 +  TestClass() {a_ = new int[1];}
 +  TestClass(TestClass const& other) = delete;
 +  TestClass(TestClass && other) noexcept
 +  {
 +    this->a_ = std::exchange(other.a_, {});
 +  }
 +  TestClass& operator=(TestClass const& other) = delete;
 +  TestClass& operator=(TestClass && other) noexcept
 +  {
 +    this->a_ = std::exchange(other.a_, {});
 +    return *this;
 +  }
 +  ~TestClass() {delete[] a_;}
 + public:
 +  int* a_ = nullptr;
 +};
 +
 +int main( int argc, char** argv )
 +{
 +  TestClass t;
 +  TestClass t2 = std::move(t);
 +}
 +</code>
 +
 +  * Mettre les 2 ''copy'' en ''= delete''
 +
 +L'opérateur ''copy'' devrait toujours être ''delete'' si la classe est polymorphique pour éviter les problèmes de slicing. [[https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rc-copy-virtual|C.67: A polymorphic class should suppress copying]]
 +
 +Mauvais :
 +
 +<code cpp>
 +class B {
 + public:
 +  virtual char m() { return 'B'; }
 +};
 +
 +class D : public B {
 + public:
 +  char m() override { return 'D'; }
 +};
 +
 +void f(B& b) {
 +  auto b2 = b; // oops, slices the object; b2.m() will return 'B'
 +}
 +
 +D d;
 +f(d);
 +</code>
 +
 +OK :
 +
 +<code cpp>
 +#include <memory>
 +
 +class B {
 + public:
 +  B() = default;
 +  // Si on implémente ce clone, il faut mettre le constructeur par copie en public.
 +  // Il est préférable de mettre ce constructeur en privée pour empêcher
 +  // sa mauvaise utilisation.
 +  virtual std::unique_ptr<B> clone() = 0;
 +  virtual char m() { return 1; }
 +  virtual ~B() = default;
 +
 + protected:
 +  // Peut être protected si clone est pure virtuelle.
 +  B(const B&) = default;
 +  B& operator=(const B&) = delete;
 +};
 +
 +class D : public B {
 + public:
 +  D() = default;
 +  D(const D&) = default;
 +  D& operator=(const D&) = delete;
 +  std::unique_ptr<B> clone() override { return std::make_unique<D>(*this); }
 +  char m() override { return 10; }
 +  virtual ~D() = default;
 +};
 +
 +char f(B& b) {
 +  auto b2 = b.clone();
 +  return b2->m();
 +}
 +
 +int main() {
 +  D d;
 +  return f(d);
 +}
 +</code>
 +
 +  * ''copy'' si ''move'' non défini.
 +
 +Si on implémente l'opérateur ''copy'' et que l'opérateur move n'est pas explicitement ''= delete'', l'opérateur ''move'' va appeler l'opérateur ''copy''. On note qu'on est dans le cas où l'opérateur move ne sera pas automatiquement généré par le compilateur.
 +
 +<code cpp>
 +class TestClass
 +{
 + public:
 +  TestClass() {std::cout << "const" << std::endl;}
 +  TestClass(TestClass const& other) {std::cout << "copy" << std::endl;}
 +  ~TestClass() {std::cout << "destruct" << std::endl;}
 +};
 +
 +int main( int argc, char** argv )
 +{
 +  TestClass t;
 +  TestClass t2 = std::move(t);
 +}
 +</code>
 +
 +Sortie :
 +
 +<code>
 +const
 +copy
 +destruct
 +destruct
 +</code>
  
 ====Contenu==== ====Contenu====
 +
 En plus de ces 6 opérateurs, une classe peut contenir (dans l'ordre de préférence, conformément au [[lang:cpp:codingstyle|coding style de Google]]) : En plus de ces 6 opérateurs, une classe peut contenir (dans l'ordre de préférence, conformément au [[lang:cpp:codingstyle|coding style de Google]]) :
  
Ligne 146: Ligne 478:
 Voir le [[helloworld:design_pattern:pont:cpp|pattern pont]] Voir le [[helloworld:design_pattern:pont:cpp|pattern pont]]
  
-=====Conseils sur l'héritage=====+=====Héritage===== 
 + 
 +====Syntaxe==== 
 + 
 +<code cpp> 
 +#include <iostream> 
 +#include <memory> 
 + 
 +class A 
 +
 + public: 
 +  A() = default; 
 +  /* virtual */ ~A() { std::cout << "A\n";
 +  // On applique la règle des 5 car un destructeur est défini. 
 +  A(A&& other) noexcept = default; 
 +  A(A const& other) = delete; 
 +  A& operator=(A&& other) noexcept = default; 
 +  A& operator=(A const& other) = delete; 
 +}; 
 + 
 +class B : public A 
 +
 + public: 
 +  B() = default; 
 +  ~B() /* override */ { std::cout << "B\n";
 +  // On applique la règle des 5 car un destructeur est défini. 
 +  B(B&& other) noexcept = default; 
 +  B(B const& other) = delete; 
 +  B& operator=(B&& other) noexcept = default; 
 +  B& operator=(B const& other) = delete; 
 + 
 + private: 
 +  A a_; 
 +}; 
 + 
 +int main() 
 +
 +  std::unique_ptr<A> a = std::make_unique<B>(); 
 +  // Comme le destructeur n'est pas virtuel et que a est déclarée en A, 
 +  // le destructeur de B n'est pas appelé ni le destructeur de la variable privée a_. 
 +
 +</code> 
 + 
 +====vtable==== 
 + 
 +Il n'existe pas de norme pour savoir où est l'adresse du pointeur de la table virtuelle. Cependant, en tout logique, c'est la première donnée de la classe. 
 + 
 +<code cpp> 
 +#include <iostream> 
 + 
 +struct S { 
 +  int x; 
 +  virtual void f() {} 
 +}; 
 + 
 +int main() { 
 +  S s; 
 +  s.x = 5; 
 +  std::cout << "size : " << sizeof(S) << "\n"; 
 +  void*** ptr (void***)&s; 
 +  std::cout << "address : " << ptr << "\n"; 
 +  std::cout << "bytes : " << *ptr << " and " << *(ptr + 1) << "\n"; 
 +  std::cout << "address if vftable : " << *ptr << "\naddress of f : " << **ptr 
 +            << "\n"; 
 +  std::cin.get(); 
 +  return 0; 
 +
 +</code> 
 + 
 +[[http://www.cplusplus.com/forum/general/33019/|detemine vtable address]] {{ :lang:cpp:class:detemine_vtable_address_-_c_forum_2021-06-21_00_03_46_.html |Archive du 13/12/2010 le 22/06/2021}}
  
 ====override / redéfinition==== ====override / redéfinition====
Ligne 189: Ligne 590:
  
 Pour le destructeur, toujours s'il est ''virtual'', tous les destructeurs seront appelés, de l'enfant vers le parent. Pour le destructeur, toujours s'il est ''virtual'', tous les destructeurs seront appelés, de l'enfant vers le parent.
 +
 +Par défaut, un destructeur est ''noexcept''. Il est donc interdit d'y générer une exception. Il est possible d'ajouter ''noexcept(false)'' mais c'est une mauvaise pratique de codage.
 ====overload / surcharge==== ====overload / surcharge====
   * Cas général   * Cas général
Ligne 213: Ligne 616:
 int f() { std::cout << "int" << std::endl;} int f() { std::cout << "int" << std::endl;}
 </code> </code>
 +
 +Il est déconseillé de mettre des valeurs par défaut aux arguments. Il est préférable de privilégier une surcharge.
 +
 +<code cpp>
 +void f() { f(5);}
 +void f(int i/* = 5*/) { std::cout << "int" << std::endl;}
 +</code>
 +
 +[[https://quuxplusone.github.io/blog/2020/04/18/default-function-arguments-are-the-devil/|Default function arguments are the devil]] {{ :lang:cpp:class:default_function_arguments_are_the_devil_arthur_o_dwyer_stuff_mostly_about_c_2020-04-19_12_10_21_am_.html |Archive du 18/04/2020 le 20/04/2020}}
  
   * Cas de deux méthodes ayant le même prototype compatible   * Cas de deux méthodes ayant le même prototype compatible
Ligne 263: Ligne 675:
 </code> </code>
  
 +====Interface====
 +
 +Il n'y a pas une unique méthode pour définir une interface.
 +
 +===Classe abstraite===
 +
 +Il faut faire une classe avec tous les prototypes et les déclarer toutes virtuelles pures.
 +
 +''Foo'' et ''Bar'' doivent déclarer au minimum les méthodes de ''BaseClass''.
 +
 +<code cpp>
 +#include <iostream>
 +#include <memory>
 +#include <string>
 +#include <vector>
 +
 +struct BaseClass
 +{
 +  virtual ~BaseClass() = default;
 +  virtual std::string getName() const = 0;
 +};
 +
 +struct Bar : BaseClass
 +{
 +  std::string getName() const override
 +  {
 +    return "Bar";
 +  }
 +};
 +
 +struct Foo : BaseClass
 +{
 +  std::string getName() const override
 +  {
 +    return "Foo";
 +  }
 +};
 +
 +int main()
 +{
 +  std::vector<std::unique_ptr<BaseClass>> vec;
 +  vec.emplace_back(std::make_unique<Foo>());
 +  vec.emplace_back(std::make_unique<Bar>());
 +
 +  for (const auto& v : vec) std::cout << v->getName() << std::endl;
 +
 +  std::cout << std::endl;
 +}
 +</code>
 +
 +===Template===
 +
 +''Foo'' et ''Bar'' n'héritent pas explicitement d'une classe commune. Ce mécanisme est caché dans un objet opaque (type-erasure).
 +
 +<WRAP center round important 60%>
 +Ce système de masquage ne doit être appliqué que si nécessaire.
 +
 +[[https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rt-erasure|Where possible, avoid type-erasure]]
 +</WRAP>
 +
 +
 +Ici, ''Foo'', ''Bar'' et aussi ''Object'' et ''Model'' doivent déclarer au minimum les méthodes de ''BaseClass''.
 +
 +<code cpp>
 +#include <iostream>
 +#include <memory>
 +#include <string>
 +#include <vector>
 +
 +class Object
 +{
 + public:
 +  template <typename T>
 +  Object(std::unique_ptr<T> obj) : object(std::make_unique<Model<T>>(std::move(obj))) {}
 +
 +  std::string getName() const
 +  {
 +    return object->getName();
 +  }
 +
 +  struct Concept
 +  {
 +    virtual ~Concept() {}
 +    virtual std::string getName() const = 0;
 +  };
 +
 +  template< typename T >
 +  struct Model final : Concept
 +  {
 +    Model(std::unique_ptr<T> t) noexcept : object(std::move(t)) {}
 +    std::string getName() const final
 +    {
 +      return object->getName();
 +    }
 +   private:
 +    std::unique_ptr<T> object;
 +  };
 +
 +  std::unique_ptr<const Concept> object;
 +};
 +
 +struct Bar
 +{
 +  std::string getName() const
 +  {
 +    return "Bar";
 +  }
 +};
 +
 +struct Foo
 +{
 +  std::string getName() const
 +  {
 +    return "Foo";
 +  }
 +};
 +
 +int main()
 +{
 +  std::vector<std::unique_ptr<Object>> vec;
 +  vec.emplace_back(std::make_unique<Object>(std::make_unique<Foo>()));
 +  vec.emplace_back(std::make_unique<Object>(std::make_unique<Bar>()));
 +
 +  for (const auto& v : vec)
 +    std::cout << v->getName() << std::endl;
 +}
 +</code>
 +
 +[[https://www.modernescpp.com/index.php/c-core-guidelines-type-erasure-with-templates|C++ Core Guidelines: Type Erasure with Templates]] {{ :lang:cpp:class:c_core_guidelines_type_erasure_with_templates_-_modernescpp.com_2020-05-17_3_50_54_pm_.html |Archive du 17/09/2018 le 17/05/2020}}
 =====Hack===== =====Hack=====
  
lang/cpp/classes.1587159357.txt.gz · Dernière modification : 2020/04/17 23:35 de root