=====Sans héritage===== ====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. 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_; }; ====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. class Class { public: // Constructor. Class() = default; // Pas de destructeur. Pas de copy/move contructor/assignment operator } Le post originel (?) sur la règle du zéro : [[http://rmartinho.github.com/cxx11/2012/08/15/rule-of-zero.html|Rule of Zero]] {{ :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}}
If the definition of a class X does not explicitly declare a move constructor, one will be implicitly declared as defaulted if and only if * X does not have a user-declared copy constructor, * X does not have a user-declared copy assignment operator, * X does not have a user-declared move assignment operator, and * X does not have a user-declared destructor, * the move constructor would not be implicitly defined as deleted. [[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}}
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"==== Cette version est nécessaire si la classe gère des ressources ou si la classe est polymorphique. class Class { public: // Constructor. Class() = default; // Destructor. ~Class() = default; // Move constructor. Class(Class && other) noexcept = delete; // Copy constructor. Class(Class const& other) = delete; // Move operator. Class& operator=(Class && other) noexcept = delete; // Copy operator. Class& operator=(Class const& other) = delete; } 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. L'intérêt de tous les ''delete'' est de maitriser le compilateur et de n'écrire que le code nécessaire. 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}} ===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. 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; } Il faut de même utiliser ''virtual'' si l'un des membres est une classe qui gère des ressources. 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; } Sortie sans le destructeur de ''A'' ''virtual'' : const * 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. int main() { A a; { B b; // On appelle ~B() } // On appelle ~A() } 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''. int main() { std::mutex io_mutex; { std::scoped_lock 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; } Si l'objet est temporaire, le destructeur sera appelé lorsque la totalité de l'instruction sera finie. #include 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"; } Affichage : 0 A int int ~A 1 ===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. Contrairement aux classes qui définissent les opérateurs ''move'', la version par défaut ''move'' ne modifie pas l'objet initial. Exemple avec ''move = default'' : #include 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. } Exemple avec ''move'' et ''std::exchange'' : #include #include 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); } * 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 : 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); OK : #include 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 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 clone() override { return std::make_unique(*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); } * ''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. 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); } Sortie : const copy destruct destruct ====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]]) : * Des déclarations de type (''typedef'', ''using'', et ''struct'' et ''class'' imbriquées), * Des constantes, * Des méthodes de fabrique ([[helloworld:design_pattern:fabrique_abstraite|pattern factory]]), * Des constructeurs, * Des opérateurs copy / move, * Un destructeur, * Des méthodes diverses, * Des champs / variables / attributs / membres (c'est la même chose) divers. * Des méthodes. ===Champs / variables / attributs / membres=== Même si un membre n'est pas modifié (sauf dans le constructeur), il est déconseillé de le déclarer ''const''. Cela empêche l'utilisation de l'opérateur ''std::move'' et force l'utilisation de ''std::copy'' dès qu'un membre n'a pas de constructeur trivial. #include #include class A { public: A(int i, const std::string& str) : i_(i), s_(str) {} // Destructor. ~A() = default; // Move constructor. A(A && other) noexcept = default; // Copy constructor. A(A const& other) = delete; // Move operator. A& operator=(A && other) noexcept = delete; // Copy operator. A& operator=(A const& other) = delete; public: int i_; const std::string s_; }; int main() { A a(5, "coucou"); A aa = std::move(a); std::cout << a.i_ << a.s_ << "\n"; std::cout << aa.i_ << aa.s_ << "\n"; } Message du compilateur : error C2280: 'A::A(const A &)': attempting to reference a deleted function note: see declaration of 'A::A' note: 'A::A(const A &)': function was explicitly deleted Comme la classe à un champ ''const'' et non modifiable, il va appeler ''std::copy'' (qui est ''delete'') à la place de ''std::move''. ===Visibilité=== Il existe 3 niveaux de visibilité : * ''public'' : tout le monde peut accéder à l'élément. * ''protected'' : seules la classe et les classes enfant (héritage) peuvent accéder à l'élément. * ''private'' : seule la classe peut accéder à l'élément. Après avoir déclaré un niveau de visibilité, tous les éléments en dessous ont la même visibilité. class Class { public: Class() = default; protected: void Execute(); private: int etat; } En l'absence de visibilité précédant un élément, ils sont ''private'' pour les ''class'' et ''public'' pour les ''struct''. C'est la seule et unique différence entre ''struct'' et ''class''. On classe d'abord en fonction de la visibilité puis en fonction du type de l'élément. ====impl==== Voir le [[helloworld:design_pattern:pont:cpp|pattern pont]] =====Héritage===== ====Syntaxe==== #include #include 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 = std::make_unique(); // 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_. } ====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. #include 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; } [[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==== ===Cas général=== Une classe enfant redéfinit une méthode du parent. Cela peut se faire avec ou sans l'attribut ''virtual''. #include class A { public: /*virtual*/ void f() { std::cout << "A" << std::endl;} }; class B : public A { public: void f() /*override*/ { std::cout << "B" << std::endl;} }; int main() { B b; A *a = &b; // Sans virtual/override, on affiche "A". // Avec virtual/override, on affiche "B". a->f(); b.f(); } ===Constructeur/destructeur=== * Constructeur Lors de l'instantiation d'une classe, un constructeur sera systématiquement appelé à chaque niveau d'héritage, en commençant par celui en bas de la hiérarchie. * Destructeur Dans le cas général d'un appel de fonction ''virtual'', c'est la méthode de la classe la plus enfant qui sera appelée. 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==== * Cas général Deux méthodes ont le même nom mais avec des arguments de type différent. #include void f() { std::cout << "void" << std::endl;} void f(int) { std::cout << "int" << std::endl;} int main() { f(); f(1); } Attention, il n'est pas possible d'avoir deux méthodes avec pour seule différence le type de retour (prototype identique). // Ambigu et interdit void f() { std::cout << "void" << std::endl;} int f() { std::cout << "int" << std::endl;} Il est déconseillé de mettre des valeurs par défaut aux arguments. Il est préférable de privilégier une surcharge. void f() { f(5);} void f(int i/* = 5*/) { std::cout << "int" << std::endl;} [[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 void fn(int i, int j); void fn(int i, int ...); Dans le cas de l'appel à la fonction ''fn(1, 2)'', la fonction ayant le prototype exact sera appelée : ''void fn(int i, int j)'' * Héritage Le compilateur ne sait pas gérer la surcharge à travers l'héritage. #include #include struct BaseInt { void Func(int) { std::cout << "BaseInt...\n"; } void Func(double) { std::cout << "BaseDouble...\n"; } }; struct BaseString { void Func(std::string) { std::cout << "BaseString...\n"; } }; struct Derived : public BaseInt, public BaseString { }; int main() { Derived d; d.Func(10.); } Cela donne le message d'erreur : main.cpp:21:5: error: request for member ‘Func’ is ambiguous Il est nécessaire d'ajouter explicitement les méthodes via ''using''. struct Derived : public BaseInt, public BaseString { using BaseInt::Func; using BaseString::Func; }; ====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''. #include #include #include #include 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> vec; vec.emplace_back(std::make_unique()); vec.emplace_back(std::make_unique()); for (const auto& v : vec) std::cout << v->getName() << std::endl; std::cout << std::endl; } ===Template=== ''Foo'' et ''Bar'' n'héritent pas explicitement d'une classe commune. Ce mécanisme est caché dans un objet opaque (type-erasure). 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]] Ici, ''Foo'', ''Bar'' et aussi ''Object'' et ''Model'' doivent déclarer au minimum les méthodes de ''BaseClass''. #include #include #include #include class Object { public: template Object(std::unique_ptr obj) : object(std::make_unique>(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) noexcept : object(std::move(t)) {} std::string getName() const final { return object->getName(); } private: std::unique_ptr object; }; std::unique_ptr object; }; struct Bar { std::string getName() const { return "Bar"; } }; struct Foo { std::string getName() const { return "Foo"; } }; int main() { std::vector> vec; vec.emplace_back(std::make_unique(std::make_unique())); vec.emplace_back(std::make_unique(std::make_unique())); for (const auto& v : vec) std::cout << v->getName() << std::endl; } [[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===== ====Méthodes statiques dans une interface==== Normalement, c'est interdit mais apparemment, c'est possible en faisant du bricolage. [[http://www.cheshirekow.com/wordpress/?p=55|Static Interfaces in C++]] {{ :lang:cpp:heritage:static_interfaces_in_c_brain_dump_2019-12-19_2_21_38_pm_.html |Archive du 13/08/2019 le 19/12/2019}} L'interface : template < typename T > class StaticInterface { public: StaticInterface() { int(*fooCheck)(int) = T::foo; bool(*barCheck)(bool) = T::bar; } }; Une implémentation de la classe : class DerivedClass : public StaticInterface { public: static int foo(int param){ return 10; } static bool bar(bool param){ return 20; } }; ====Implémentation d'une méthode purement virtuelle d'un parent par un autre parent==== Il est important que ''Base'' ne contienne que les méthodes virtuelles car les deux classes ''Mixin'' et ''Example'' vont en hériter. Si ''Base::test'' n'est pas purement virtuelle, ça sera toujours la méthode de la classe ''Mixin'' qui sera appelée. class Base { public: virtual void test() = 0; }; template class Mixin : virtual T { public: virtual void test() override { /*... do stuff ... */ } }; class Example : public virtual Base, public virtual Mixin { /* definitions specific to the Example class _not_including_ a definition of the test() method */ }; {{ :lang:cpp:heritage:class_example_inherit_graph.svg |}} [[https://stackoverflow.com/questions/19528338/how-to-override-a-function-in-another-base-class|How to override a function in another base class?]] {{ :lang:cpp:heritage:c_-_how_to_override_a_function_in_another_base_class_-_stack_overflow_2019-12-19_2_22_01_pm_.html |Archive du 22/10/2013 le 19/12/2019}} ====Forcer un template à hériter d'une classe==== template class InterfaceVisitable { static_assert(std::is_base_of::value, "M must be a descendant of XXXXXXXX"); }; ====Caster T* t = new T()==== C'est impossible. La notion d'héritage ne marche que pour la classe, pas pour les arguments du template. L'exemple ci-dessous ne marche pas. Caster l'un en l'autre poserait des problèmes pour l'accès à la variable a. #include #include class A { public: int a; }; class B : public A { public: int b; }; template class C { public: T t[2]; }; int main() { C a; C b; std::cout << (((size_t)(&a.t[1])) - ((size_t)(&a.t[0]))) << std::endl; std::cout << (((size_t)(&b.t[1])) - ((size_t)(&b.t[0]))) << std::endl; return 0; } Rendu : 4 8 ====Exécuter une méthode privée==== #include class Widget { private: void forbidden() { std::cout << "Whoops...\n"; } }; namespace { struct TranslationUnitTag {}; } void hijack(Widget& w); template < typename Tag, typename ForbiddenFun, ForbiddenFun forbidden_fun > class HijackImpl { friend void hijack(Widget& w) { (w.*forbidden_fun)(); } }; template class HijackImpl< TranslationUnitTag, decltype(&Widget::forbidden), &Widget::forbidden >; int main() { Widget w; hijack(w); // Prints "Whoops..." } [[https://accu.org/index.php/journals/2776|Profiting from the Folly of Others]] {{ :lang:cpp:class:accu_profiting_from_the_folly_of_others_2020-04-17_11_28_22_pm_.html |Archive du 04/2020 le 17/04/2020}} =====TODO===== Non encore implémenté : https://brevzin.github.io/c++/2019/12/02/named-arguments/