=====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;
}
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.
#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 (
#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).
#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
[[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
#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/