Outils pour utilisateurs

Outils du site


lang:cpp:template

Ceci est une ancienne révision du document !


Héritage

CRTP (Curiously recurring template pattern)

Principe

Le pattern CRTP fait de l'héritage inverse. C'est dans le parent qu'on ajoute les fonctionnalités. Cela a comme avantage de s'affranchir des méthode virtuelles et le gain de performance résulte dans le fait que les méthodes sont optimisées par le compilateur via les fonctions inline.

Principe du CRTP : on définit l'appel des fonctions dans la classe de base (qui est template). On accède à la classe enfant via un cast du template. La classe dérivée hérite de la classe de base avec elle même comme template.

template <typename T>
class CRTPBase
{
  private:
    T& impl() { return *static_cast<T*>(this); }
};
 
class CRTPDerived : public CRTPBase<CRTPDerived>
{
};

Benchmark Archive du 26/04/2016 le 10/03/2020

Virtual sans CRTP

class VirtualBase
{
  public :
    virtual int tick(int n) = 0;
};
 
class VirtualDerived : public VirtualBase
{
  public :
    int m_counter;
  public :
    VirtualDerived() : m_counter(0) {}
    int tick(int n) { m_counter += n; return m_counter; }
};
 
int test_virtual_methods (int test_run)
{
  VirtualBase* pObj = static_cast<VirtualBase*>(new VirtualDerived);
  for( int i = 0 ; i < test_run; i++ )
  {
    for( int j = 0 ; j < test_run; j++ )
    {
      pObj->tick(j);
    }
  }
  return static_cast<VirtualDerived*>(pObj)->m_counter;
}
 
int main (int argc, char** argv)
{
  return test_virtual_methods(2000);
}

CRTP sans virtual

template <typename T>
class CRTPBase
{
  public:
    int tick(int n) 
    {
      return impl().tick(n);
    }
  private:
    T& impl() { return *static_cast<T*>(this); }
};
 
class CRTPDerived : public CRTPBase<CRTPDerived> 
{
  public :
    int m_counter;
  public:
    CRTPDerived() : m_counter(0) {}
    int tick(int n) { m_counter += n; return m_counter; }
};
 
int test_crtp (int test_run)
{
  CRTPBase<CRTPDerived>* pObj = new CRTPDerived ;
  for( int i = 0 ; i < test_run; i++ )
  {
    for( int j = 0 ; j < test_run; j++ )
    {
      pObj->tick(j);
    }
  }
  return static_cast<CRTPDerived*>(pObj)->m_counter;
}
 
int main (int argc, char** argv)
{
  return test_crtp(2000);
}

Le problème majeur du CRTP, il n'y a plus de classe parent commune et il est donc impossible de mettre plusieurs implémentations de CRTPBase dans un même type de pointeur (std::vector par exemple).

J'ai essayé de résoudre ce problème en utilisant un std::vector<std::variant<...>> mais son utilisation avec std::visit casse les performances.

Benchmark

Performance -O0 -fno-inline -O2 -fno-inline -O2
Virtuelle 1,695s 0,803s 0,830s
CRTP 3,265s 1,489s 0,130s

Héritage multiple

Si on tente de faire un héritage multiple, on va avoir une classe parent commune.

clang : error: ambiguous cast from base 'crtp<Sensitivity>' to derived 'Sensitivity'

gcc : error: 'crtp<Sensitivity>' is an ambiguous base of 'Sensitivity'

Visual Studio : error C2594: 'static_cast': ambiguous conversions from 'crtp<T>' to 'T &'

Pour avoir un parent différent, l'astuce est de rajouter un template à crtp qui ne sert à rien.

template<typename T, template<typename> typename>
struct crtp
{
  T& underlying()
  {
    return static_cast<T&>(*this);
  }
 
  T const& underlying() const
  {
    return static_cast<T const&>(*this);
  }
};
 
template<typename T>
struct Scale : public crtp<T, Scale>
{
  void scale(double multiplicator)
  {
    this->underlying().setValue(this->underlying().getValue() * multiplicator);
  }
};
 
template<typename T>
struct Square : public crtp<T, Square>
{
  void square()
  {
    this->underlying().setValue(this->underlying().getValue() * this->underlying().getValue());
  }
};
 
 
class Sensitivity : public Scale<Sensitivity>, public Square<Sensitivity>
{
public:
  double getValue() const
  {
    return value_;
  }
  void setValue(double value)
  {
    value_ = value;
  }
private:
  double value_ = 0.;
};
 
int main()
{
  Sensitivity s;
 
  s.setValue(10.);
 
  s.scale(2.);
 
  return 0;
}

The Curiously Recurring Template Pattern (CRTP) Archive du 12/05/2017 le 13/03/2020

What the Curiously Recurring Template Pattern can bring to your code Archive du 16/05/2017 le 13/03/2020

An Implementation Helper For The Curiously Recurring Template Pattern Archive du 19/07/2017 le 13/03/2020

How to Turn a Hierarchy of Virtual Methods into a CRTP Archive du 22/08/2018 le 13/03/2020

Variadic CRTP: An Opt-in for Class Features, at Compile Time Archive du 22/06/2018 le 13/03/2020

How to Reduce the Code Bloat of a Variadic CRTP Archive du 03/07/2018 le 13/03/2020

Variadic CRTP Packs: From Opt-in Skills to Opt-in Skillsets Archive du 26/06/2018 le 13/03/2020

Removing Duplicates in C++ CRTP Base Classes Archive du 28/08/2018 le 13/03/2020

Mixin classes (similaire à CRTP mais sans inversion de l'héritage)

Ce pattern peut être utilisé par le pattern décorateur.

Le code optimisé généré par le compilateur est parfaitement identique à l'exemple précédent.

class Sensitivity {
 public:
  double getValue() const { return value_; }
  void setValue(double value) { value_ = value; }
 
 private:
  double value_ = 0.;
};
 
template <typename T>
struct Scale : public T {
  void scale(double multiplicator) {
    this->setValue(this->getValue() * multiplicator);
  }
};
 
template <typename T>
struct Square : public T {
  void square() { this->setValue(this->getValue() * this->getValue()); }
};
 
class SensitivityEnhanced : public Scale<Square<Sensitivity>>
{};
 
int main() {
  SensitivityEnhanced s;
 
  s.setValue(10.);
 
  s.scale(2.);
 
  return 0;
}

Mixin Classes: The Yang of the CRTP Archive du 12/12/2017 le 10/03/2020

L'utilisation des Mixin classes est mitigée. Certaines personnes pensent que c'est bien d'ajouter des fonctionnalités par l'héritage. D'autres (dont moi) pensent que si les nouvelles fonctionnalités ne sont pas en rapport avec la classe de base, l'utilisation de la composition est mieux.

Pour Mixin : C++ Mixins - Reuse through inheritance is good... when done the right way Archive du 20/09/2011 le 12/03/2020

Pour Composition : Mixins Are Dead. Long Live Composition Archive du 13/03/2015 le 12/03/2020

Pour mitigé :

On utilisera donc l’héritage quand c’est nécessaire, rappelez vous “un développeur est une personne”. On passera aux mixins quand on est dans la situation “un développeur agit comme un salarié”. On se tournera probablement vers la composition si cette relation s’avère être quelque chose de complexe, un objet nécessitant une classe dédiée.La composition à la rescousse de l'héritage Archive du 18/02/2016 le 12/03/2020

Avantages/inconvénient du code source dans les fichiers entête

  • Avantages
    • Pas besoin d'avoir d'un coté la déclaration des classes (entête) et de l'autre l'implémentation.
    • Pour les templates, le compilateur va générer automatiquement le code pour chaque symbole pour chaque variante d'un template. En mettant le code dans l'entête, on est sûr de ne pas avoir des undefined reference to XXX en oubliant d'instancier un template extern.
    • Pas besoin d'utiliser des extern template.
  • Inconvénient
    • Les fichiers entête sont plus dur à lire pour trouver la liste des méthodes publiques. Sinon, il est possible de déclarer les méthodes dans la classe et de définir les méthodes inline en dessous de la classe.
    • Le compilateur va générer le code des fonctions dans chaque fichier source où le template est utilisé.
    • Le nombre de fichiers inclus dans les entêtes sera (sauf exception) plus important que si on avait laissé uniquement la déclaration de la classe dans le fichier entête. En effet, certains entrées ne sont peut-être nécessaire que pour l'implémentation et par pour le prototype des méthodes.
    • Le lieur va avoir de nombreux fichiers objets ayant les mêmes symboles de type Weak. Il va donc devoir faire beaucoup de nettoyage et le compilateur aura beaucoup travaillé pour rien. Pire, si le code de la méthode template est dépendant d'une macro qui varie entre plusieurs fichiers cpp, seules une des implémentations sera retenue puis généralisée.

Séparer le code source des fonctions et leur définition dans une classe template

C'est possible de ne pas être obligé de rendre inline toutes les méthodes d'une classe template. Mais il faut alors explicitement indiquer quelles combinaisons class / template seront utilisées par un template class XX<YY>;

Splitting templated C++ classes into .hpp/.cpp files--is it possible? Archive du 27/08/2019

template.h
template<class X>
class T
{
public:
   void fff();
};
template_impl.cc
#include "template.h"
 
template<class X>
void T<X>::fff() {}
 
template class T<int>;

Si on décide de mettre le template class dans le même fichier que le .cc, il est indispensable de le mettre tout en bas du fichier, après la définition de toutes les fonctions sinon les fonctions qui peuvent être inline ne seront pas conservées en symbole.

main.cc
#include "template.h"
 
extern template class T<int>();
 
int main()
{
  T<int> tint;
  return tint.retval();
}

Afficher en string le type template

Décorateur

Decorator Pattern Explained with C++ sample Archive du 31/10/2016 le 05/02/2020

  • implicit return type
#include <iostream>
#include <memory>
 
struct Action1 { };
struct Action2 { };
 
class Classe
{
public:
    // Default template.
    template<class U, class ...Args>
    auto go(Args ...args)
    {
        //static_assert(false);
    }
};
 
// Action 1.
template<>
auto Classe::go<Action1>()
{
    std::cout << "coucouGo1" << std::endl;
}
 
// Action 2.
template<>
auto Classe::go<Action2>(int i)
{
    std::cout << "coucouGo2 : " << static_cast<unsigned char>(i) << std::endl;
    return static_cast<unsigned char>(i);
}
 
template <class T>
class Decorator
{
public:
    Decorator(std::unique_ptr<T> t):classe(std::move(t)) {}
 
    template<class U, class... Args>
    auto go(Args ...args)
    {
        std::cout << "coucouDecorator" << std::endl;
        return classe->template go<U>(args...);
    }
    private:
        std::unique_ptr<T> classe;
};
  • explicit return type
#include <iostream>
#include <memory>
 
struct Action1 { using Type = void; };
struct Action2 { using Type = char; };
 
class Classe
{
public:
    // Default template.
    template<class U, class T, class ...Args>
    T go(Args ...args)
    {
        //static_assert(false);
    }
};
 
// Action 1.
template<>
Action1::Type Classe::go<Action1>()
{
    std::cout << "coucouGo1" << std::endl;
}
 
// Action 2.
template<>
Action2::Type Classe::go<Action2>(int i)
{
    std::cout << "coucouGo2 : " << i << std::endl;
    return i;
}
 
template <class T>
class Decorator
{
public:
    Decorator(std::unique_ptr<T> t):classe(std::move(t)) {}
 
    template<class U, class... Args>
    auto go(Args ...args)
    {
        std::cout << "coucouDecorator" << std::endl;
        return classe->template go<U, typename U::Type>(args...);
    }
    private:
        std::unique_ptr<T> classe;
};
  • main.cc
int main()
{
    Decorator<Classe> c(std::make_unique<Classe>());
    c.go<Action1>();
    c.go<Action2>(2);
}

metaprogrammation vs constexpr

Exemple simple

#include <cstdio>
 
template <typename T>
constexpr T square(T x) {
  return x*x;
}
 
int main() {
  printf("%lf %d\n", square(3.4), square<int>(3.4));
  // 11.560000 9
}

Fibonacci

  • Template
fibonacci.cc
template <long N> struct fibonacci
{
  static const long value = fibonacci<N - 1>::value + fibonacci<N - 2>::value;
};
 
template <> struct fibonacci<1>
{
  static const long value = 1;
};
 
template <> struct fibonacci<2>
{
  static const long value = 1;
};
 
int main()
{
  long i = fibonacci<70>::value;
  return 0;
}

gcc et clang génère 190392490709135 avec succès.

  • constexpr
fibonacci2.cc
constexpr long fib(long n)
{
  if (n <= 2) {
    return 1;
  }
  return fib(n - 1) + fib(n - 2);
}
 
int main()
{
  constexpr long i = fib(70);
  return 0;
}

gcc arrive à calculer jusqu'à la valeur 35 et clang jusqu'à 26. Au delà, il faut utiliser -fconstexpr-ops-limit= (2^25 par défaut) pour gcc et -fconstexpr-backtrace-limit=80 -fconstexpr-steps=2147483647 pour clang.

Le code est beaucoup moins optimisé dans cette version avec constexpr. Il aurait fallu d'abord remplir un tableau de 70 valeurs (ce que fait le compilateur dans la première version en calculant les static value) ce qui aurait permis d'éviter d'avoir une récursion très longue de 70 étapes.

Nombres premiers

  • Template

CppCon 2016: Peter Gottschling “How bad is Meta-Programming still today?"

C++11 Template Metaprogramming — Compile Time Computations Archive du 21/12/2014 le 17/01/2020

prime.cc
#include <type_traits>
 
template <int N>
struct Sqrt {
    template <int lo, int hi>
    struct Helper {
        static const int mid = (lo + hi + 1) / 2;
        static const int value = std::conditional < (N / mid < mid), Helper < lo, mid - 1 >, Helper <mid, hi> >::type::value;
    };
    template <int n>
    struct Helper <n, n> {
        static const int value = n;
    };
    static const int value = Helper <0, N>::value;
};
 
template <long x, long max_odd> struct is_prime_to_max_odd {
  static bool const value = x % max_odd != 0 &&
      is_prime_to_max_odd<x, max_odd-2>::value;
};
 
template <long x> struct is_prime_to_max_odd<x, 1> : std::true_type {};
 
template <long x> struct max_prime_compare {
  static long const tmp = Sqrt<x>::value, value = tmp % 2 == 0 ? tmp + 1 : tmp + 2;
};
 
template <long x, bool disable> struct check_odd {
  static bool const value = is_prime_to_max_odd<x, max_prime_compare<x>::value>::value;
};
 
template <long x> struct check_odd <x, true> {
  static bool const value = false;
};
 
template <long x> struct is_prime {
  static bool const value = check_odd<x, x%2==0>::value;
};
 
template <> struct is_prime <1> : std::false_type {};
template <> struct is_prime <2> : std::true_type {};
 
int main() {
  bool b49991 = is_prime<49991>::value;
  bool b49992 = is_prime<49992>::value;
  bool b49993 = is_prime<49993>::value;
  return 0;
}

gcc et clang arrive à calculer à la compilation.

Appeler la méthode généralisée depuis la méthode spécialisée

C'est impossible. Il faut que la méthode de base appelle une méthode spécifique.

template<typename T>
void baseF(T t) { ... }
 
template<typename T>
void F(T t) { baseF<T>(t); }
 
template<>
void F<int>(int t) { baseF<int>(t); }

How to call generic template function in a specialization version Archive du 13/07/2011 le 06/02/2020

class ou typename

Comme on veut.

Personnellement, j'utilise toujours typename.

  • Exemples possibles :
template<class T> class A{};
template<typename T> class B{};
 
template<template<typename> class T> class D{};
template<template<class> class T> class F{};
// Ci-dessous seulement à partir de C++17.
template<template<typename> typename T> class C{};
template<template<class> typename T> class E{};

Difference of keywords 'typename' and 'class' in templates? Archive du 07/01/2010 le 21/02/2020

Boucles sur des types

Dans tous les exemples ci-dessous, il faut précéder par le code :

#include <iostream>
#include <tuple>
 
class A {
 public:
  static void f() { std::cout << "A\n"; };
};
 
class B {
 public:
  static void f() { std::cout << "B\n"; };
};

Fonction template récursive avec les types en dur dans le template

Les types à parcourir sont mis dans le template.

template <typename Arg, typename... Args>
void ff() {
  Arg::f();
  if constexpr (sizeof...(Args) != 0) ff<Args...>();
}
 
int main() {
  ff<A, B>();
}

Fonction template récursive avec les types dans l'argument via un using = std::tuple

Le using est un std::tuple avec les types à l'intérieur.

Ici, le std::tuple est passé comme argument.

template <typename T, typename... Ts>
void f(std::tuple<T, Ts...> y)
{
  T::f();
  if constexpr (sizeof...(Ts) != 0) f(std::tuple<Ts...>{});
}
 
int main() {
  using Types = std::tuple<A, B>;
 
  f(Types{});
}

Fonction template récursive avec les types dans le template (spécialisation) via un using = std::tuple

template <typename T, typename... Ts>
struct AA {};
 
template <typename T, typename... Ts>
struct AA<std::tuple<T, Ts...>> {
  static void ff() {
    T::f();
    if constexpr (sizeof...(Ts) != 0) AA<std::tuple<Ts...>>::ff();
  }
};
 
int main() {
  using Types = std::tuple<A, B>;
 
  AA<Types>::ff();
}

Il est possible de s'affranchir de constexpr en ajoutant une nouvelle spécialisation de fin de récursion.

template <typename T, typename... Ts>
struct AA {};
 
template <typename T, typename... Ts>
struct AA<std::tuple<T, Ts...>> {
  static void ff() {
    T::f();
    AA<std::tuple<Ts...>>::ff();
  }
};
 
template <>
struct AA<std::tuple<>> {
  static void ff() {
  }
};

Je suis obligé de passer par une classe. Je n'arrive pas à le faire avec des fonctions :

template <typename T, typename... Ts>
void ff() {};
 
// gcc : error: non-class, non-variable partial specialization 'ff<std::tuple<_El0, _El ...> >' is not allowed
// clang : error: function template partial specialization is not allowed
// msvc : error C2995: 'void ff(void)': function template has already been defined
 
template <typename T, typename... Ts>
void ff<std::tuple<T, Ts...>>(){}

Fonction template non récursive avec les types dans le template (spécialisation) via un using = std::tuple

template <typename... Ts>
struct AA2 {};
 
template <typename... Ts>
struct AA2<std::tuple<Ts...>> {
  static void ff() {
    (Ts::f(), ...);
  }
};
 
int main() {
  using Types = std::tuple<A, B>;
 
  AA2<Types>::ff();
}
lang/cpp/template.1587934809.txt.gz · Dernière modification : 2020/04/26 23:00 de root