C++
Modèle de modèle curieusement récurrent (CRTP)
Recherche…
Introduction
Un modèle dans lequel une classe hérite d'un modèle de classe avec lui-même comme l'un de ses paramètres de modèle. CRTP est généralement utilisé pour fournir un polymorphisme statique en C ++.
Le modèle de modèle curieusement récurrent (CRTP)
CRTP est une alternative puissante et statique aux fonctions virtuelles et à l'héritage traditionnel qui peut être utilisée pour donner des propriétés de type à la compilation. Cela fonctionne en ayant un modèle de classe de base qui prend, comme l'un de ses paramètres de modèle, la classe dérivée. Cela lui permet d'effectuer légalement un static_cast
de this
pointeur vers la classe dérivée.
Bien sûr, cela signifie également qu'une classe CRTP doit toujours être utilisée comme classe de base d'une autre classe. Et la classe dérivée doit se transmettre à la classe de base.
Disons que vous avez un ensemble de conteneurs qui supportent tous les fonctions begin()
et end()
. Les exigences de la bibliothèque standard pour les conteneurs nécessitent davantage de fonctionnalités. Nous pouvons concevoir une classe de base CRTP qui fournit cette fonctionnalité, basée uniquement sur begin()
et end()
:
#include <iterator>
template <typename Sub>
class Container {
private:
// self() yields a reference to the derived type
Sub& self() { return *static_cast<Sub*>(this); }
Sub const& self() const { return *static_cast<Sub const*>(this); }
public:
decltype(auto) front() {
return *self().begin();
}
decltype(auto) back() {
return *std::prev(self().end());
}
decltype(auto) size() const {
return std::distance(self().begin(), self().end());
}
decltype(auto) operator[](std::size_t i) {
return *std::next(self().begin(), i);
}
};
La classe ci-dessus fournit les fonctions front()
, back()
, size()
et operator[]
pour toute sous-classe fournissant begin()
et end()
. Un exemple de sous-classe est un tableau simple alloué dynamiquement:
#include <memory>
// A dynamically allocated array
template <typename T>
class DynArray : public Container<DynArray<T>> {
public:
using Base = Container<DynArray<T>>;
DynArray(std::size_t size)
: size_{size},
data_{std::make_unique<T[]>(size_)}
{ }
T* begin() { return data_.get(); }
const T* begin() const { return data_.get(); }
T* end() { return data_.get() + size_; }
const T* end() const { return data_.get() + size_; }
private:
std::size_t size_;
std::unique_ptr<T[]> data_;
};
Les utilisateurs de la classe DynArray
peuvent utiliser les interfaces fournies par la classe de base CRTP facilement comme suit:
DynArray<int> arr(10);
arr.front() = 2;
arr[2] = 5;
assert(arr.size() == 10);
Utilité: ce modèle évite en particulier les appels de fonction virtuels à l'exécution qui se produisent dans la hiérarchie d'héritage et repose simplement sur des conversions statiques:
DynArray<int> arr(10);
DynArray<int>::Base & base = arr;
base.begin(); // no virtual calls
La seule distribution statique à l'intérieur de la fonction begin()
dans la classe de base Container<DynArray<int>>
permet au compilateur d'optimiser considérablement le code et aucune consultation de table virtuelle ne se produit à l'exécution.
Limitations: Comme la classe de base est basée sur des modèles et différente pour deux DynArray
différents, il est impossible de stocker des pointeurs dans leurs classes de base dans un tableau homogène, comme on peut généralement le faire avec l'héritage normal. type:
class A {};
class B: public A{};
A* a = new B;
CRTP pour éviter la duplication de code
L'exemple dans Visitor Pattern fournit un cas d'utilisation convaincant pour CRTP:
struct IShape
{
virtual ~IShape() = default;
virtual void accept(IShapeVisitor&) const = 0;
};
struct Circle : IShape
{
// ...
// Each shape has to implement this method the same way
void accept(IShapeVisitor& visitor) const override { visitor.visit(*this); }
// ...
};
struct Square : IShape
{
// ...
// Each shape has to implement this method the same way
void accept(IShapeVisitor& visitor) const override { visitor.visit(*this); }
// ...
};
Chaque type d'enfant d' IShape
doit implémenter la même fonction de la même manière. C'est beaucoup de frappe supplémentaire. Au lieu de cela, nous pouvons introduire un nouveau type dans la hiérarchie qui le fait pour nous:
template <class Derived>
struct IShapeAcceptor : IShape {
void accept(IShapeVisitor& visitor) const override {
// visit with our exact type
visitor.visit(*static_cast<Derived const*>(this));
}
};
Et maintenant, chaque forme doit simplement hériter de l'accepteur:
struct Circle : IShapeAcceptor<Circle>
{
Circle(const Point& center, double radius) : center(center), radius(radius) {}
Point center;
double radius;
};
struct Square : IShapeAcceptor<Square>
{
Square(const Point& topLeft, double sideLength) : topLeft(topLeft), sideLength(sideLength) {}
Point topLeft;
double sideLength;
};
Aucun code en double nécessaire.