web-dev-qa-db-fra.com

Protégez le modèle CRTP du débordement de pile dans les appels "virtuels purs"

Prenons l'exemple de CRTP standard suivant:

#include <iostream>

template<class Derived>
struct Base {
    void f() { static_cast<Derived *>(this)->f(); }
    void g() { static_cast<Derived *>(this)->g(); }
};

struct Foo : public Base<Foo> {
    void f() { std::cout << 42 << std::endl; }
};

int main() {
    Foo foo;
    foo.f(); // just OK
    foo.g(); // this will stack overflow and segfault
}

Si c’était un héritage virtuel normal, j’aurais pu marquer les méthodes virtuelles f et g comme pures, comme

struct Base {
    virtual void f() = 0;
    virtual void g() = 0;
};

et obtenez une erreur de compilation indiquant que Foo est abstrait. Mais le CRTP n'offre pas une telle protection. Puis-je l'implémenter d'une manière ou d'une autre? La vérification à l'exécution est également acceptable. J'ai pensé à comparer le pointeur this->f avec static_cast<Derived *>(this)->f, mais je n'ai pas réussi à le faire fonctionner.

21
uranix

Voici une autre possibilité:

#include <iostream>

template<class Derived>
struct Base {
    auto f() { return static_cast<Derived *>(this)->f(); }
    auto g() { return static_cast<Derived *>(this)->g(); }
};

struct Foo : public Base<Foo> {
    void f() { std::cout << 42 << std::endl; }
};

int main() {
    Foo foo;
    foo.f(); // just OK
    foo.g(); // this will not compile
}

Pour GCC, cela donne un message d'erreur assez clair ( "erreur: utilisation de 'auto Base :: g () [avec Derived = Foo]' avant déduction de 'auto'" ) il donne une instanciation de modèle légèrement moins lisible de Base<Foo>::g, avec g s'instanciant elle-même mais se terminant finalement par une erreur. 

12

Vous pouvez affirmer au moment de la compilation que les deux pointeurs vers les fonctions membres sont différents, par exemple:

template<class Derived>
struct Base {
    void g() {
        static_assert(&Derived::g != &Base<Derived>::g,
                      "Derived classes must implement g()."); 
        static_cast<Derived *>(this)->g(); 
    }
};
24
Holt

Vous pouvez utiliser cette solution, vous pouvez avoir une fonction "abstraite non virtuelle" pure et mapper autant que possible au CRTP cette recommandation de H. Sutter :

template<class Derived>
struct Base
  {
  void f(){static_cast<Derived*>(this)->do_f();}
  void g(){static_cast<Derived*>(this)->do_g();}

  private:
  //Derived must implement do_f
  void do_f()=delete;
  //do_g as a default implementation
  void do_g(){}
  };

struct derived
  :Base<derived>
  {
  friend struct Base<derived>;

  private:
  void do_f(){}
  };
12
Oliv

Vous pourriez envisager de faire quelque chose comme ça à la place. Vous pouvez faire de Derived un membre et le fournir directement en tant que paramètre de modèle chaque fois que vous instanciez une Base ou utilisez un type alias comme je l'ai fait dans cet exemple:

template<class Derived>
struct Base {
    void f() { d.f(); }
    void g() { d.g(); }
private:
    Derived d;
};

struct FooImpl {
    void f() { std::cout << 42 << std::endl; }
};

using Foo = Base<FooImpl>;

int main() {
    Foo foo;
    foo.f(); // OK
    foo.g(); // compile time error
}

Bien entendu, Derived n'est plus dérivé _ donc vous pouvez en choisir un meilleur nom.

0
Galik