Outils pour utilisateurs

Outils du site


lang:cpp:classes

Ceci est une ancienne révision du document !


Sans héritage

Constructeurs / destructeurs

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.

class A{};
class B : public A
{
 public:
  // Call constructor B(int) from constructor B().
  // Dont call the parent A() when calling another constructor of the same class.
  B() : B(5){}
  // Avoid default function argument
  B(int a/* = 5*/):A(), a_(a){}
 
 private:
  int a_;
};

Version "Rule of Zero"

class Class
{
 public:
  // Constructor.
  Class() = default;
}

Le post originel (?) sur la règle du zéro : Rule of Zero 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.

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.

C++ Rule of Zero & what is "user-declared" constructor? Archive du 04/12/2016 le 09/03/2020

Par “does not have a user-declared”, cela signifie aussi ne pas utiliser = default;.

Version "Rule of five"

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;
}

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.

The Rule of Zero in C++ Archive du 23/04/2019 le 18/12/2019

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.

C++ noexcept and move constructors effect on performance in STL Containers Archive du 01/12/2017 le 18/12/2019

Contenu

En plus de ces 6 opérateurs, une classe peut contenir (dans l'ordre de préférence, conformément au coding style de Google) :

  • Des déclarations de type (typedef, using, et struct et class imbriquées),
  • Des constantes,
  • Des méthodes de fabrique (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 <iostream>
#include <string>
 
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 pattern pont

Conseils sur l'héritage

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

overload / surcharge

  • Cas général

Deux méthodes ont le même nom mais avec des arguments de type différent.

#include <iostream>
 
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;}

Default function arguments are the devil 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 <iostream>
#include <string>
 
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;
};

Hack

Méthodes statiques dans une interface

Normalement, c'est interdit mais apparemment, c'est possible en faisant du bricolage.

Static Interfaces in C++ 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<DerivedClass>
{
  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 <typename T>
class Mixin : virtual T {
public:
    virtual void test() override { /*... do stuff ... */ }
};
 
class Example : public virtual Base, public virtual Mixin<Base> {
    /* definitions specific to the Example class _not_including_
       a definition of the test() method */
};

How to override a function in another base class? Archive du 22/10/2013 le 19/12/2019

Forcer un template à hériter d'une classe

template <class M>
class InterfaceVisitable {
  static_assert(std::is_base_of<XXXXXXXX, M>::value,
                "M must be a descendant of XXXXXXXX");
};

Caster T<Base>* t = new T<Derive>()

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 <cstddef>
#include <iostream>
 
class A
{
 public:
  int a;
};
 
class B : public A
{
 public:
  int b;
};
 
template<typename T>
class C
{
 public:
  T t[2];
};
 
int main()
{
  C<A> a;
  C<B> 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 <iostream>
 
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..."
}

Profiting from the Folly of Others Archive du 04/2020 le 17/04/2020

TODO

lang/cpp/classes.1587334607.txt.gz · Dernière modification : 2020/04/20 00:16 de root