On me dit que le système de modèles en C++ est Turing-complet au moment de la compilation. Ceci est mentionné dans ce post et aussi sur wikipedia .
Pouvez-vous fournir un exemple non trivial d'un calcul qui exploite cette propriété?
Ce fait est-il utile dans la pratique?
Exemple
#include <iostream>
template <int N> struct Factorial
{
enum { val = Factorial<N-1>::val * N };
};
template<>
struct Factorial<0>
{
enum { val = 1 };
};
int main()
{
// Note this value is generated at compile time.
// Also note that most compilers have a limit on the depth of the recursion available.
std::cout << Factorial<4>::val << "\n";
}
C'était un peu amusant mais pas très pratique.
Pour répondre à la deuxième partie de la question:
Ce fait est-il utile dans la pratique?
Réponse courte: en quelque sorte.
Réponse longue: Oui, mais uniquement si vous êtes un démon de modèle.
Il est vraiment très difficile de réaliser une bonne programmation à l'aide d'une méta-programmation de modèle qui est vraiment utile pour les autres (c'est-à-dire une bibliothèque). Pour aider à booster même a MPL aka (Bibliothèque de programmation Meta). Mais essayez de déboguer une erreur de compilation dans votre code de modèle et vous serez dans une longue course difficile.
Mais un bon exemple pratique de son utilisation pour quelque chose d'utile:
Scott Meyers a travaillé sur des extensions du langage C++ (j'utilise le terme de manière lâche) en utilisant les fonctionnalités de création de modèles. Vous pouvez lire sur son travail ici ' Enforcing Code Features '
J'ai fait une machine de turing en C++ 11. Les fonctionnalités ajoutées par C++ 11 ne sont pas significatives pour la machine de turing. Il prévoit simplement des listes de règles de longueur arbitraire en utilisant des modèles variadiques, au lieu d'utiliser une métaprogrammation macro perverse :). Les noms des conditions sont utilisés pour sortir un diagramme sur stdout. j'ai supprimé ce code pour garder l'échantillon court.
#include <iostream>
template<bool C, typename A, typename B>
struct Conditional {
typedef A type;
};
template<typename A, typename B>
struct Conditional<false, A, B> {
typedef B type;
};
template<typename...>
struct ParameterPack;
template<bool C, typename = void>
struct EnableIf { };
template<typename Type>
struct EnableIf<true, Type> {
typedef Type type;
};
template<typename T>
struct Identity {
typedef T type;
};
// define a type list
template<typename...>
struct TypeList;
template<typename T, typename... TT>
struct TypeList<T, TT...> {
typedef T type;
typedef TypeList<TT...> tail;
};
template<>
struct TypeList<> {
};
template<typename List>
struct GetSize;
template<typename... Items>
struct GetSize<TypeList<Items...>> {
enum { value = sizeof...(Items) };
};
template<typename... T>
struct ConcatList;
template<typename... First, typename... Second, typename... Tail>
struct ConcatList<TypeList<First...>, TypeList<Second...>, Tail...> {
typedef typename ConcatList<TypeList<First..., Second...>,
Tail...>::type type;
};
template<typename T>
struct ConcatList<T> {
typedef T type;
};
template<typename NewItem, typename List>
struct AppendItem;
template<typename NewItem, typename...Items>
struct AppendItem<NewItem, TypeList<Items...>> {
typedef TypeList<Items..., NewItem> type;
};
template<typename NewItem, typename List>
struct PrependItem;
template<typename NewItem, typename...Items>
struct PrependItem<NewItem, TypeList<Items...>> {
typedef TypeList<NewItem, Items...> type;
};
template<typename List, int N, typename = void>
struct GetItem {
static_assert(N > 0, "index cannot be negative");
static_assert(GetSize<List>::value > 0, "index too high");
typedef typename GetItem<typename List::tail, N-1>::type type;
};
template<typename List>
struct GetItem<List, 0> {
static_assert(GetSize<List>::value > 0, "index too high");
typedef typename List::type type;
};
template<typename List, template<typename, typename...> class Matcher, typename... Keys>
struct FindItem {
static_assert(GetSize<List>::value > 0, "Could not match any item.");
typedef typename List::type current_type;
typedef typename Conditional<Matcher<current_type, Keys...>::value,
Identity<current_type>, // found!
FindItem<typename List::tail, Matcher, Keys...>>
::type::type type;
};
template<typename List, int I, typename NewItem>
struct ReplaceItem {
static_assert(I > 0, "index cannot be negative");
static_assert(GetSize<List>::value > 0, "index too high");
typedef typename PrependItem<typename List::type,
typename ReplaceItem<typename List::tail, I-1,
NewItem>::type>
::type type;
};
template<typename NewItem, typename Type, typename... T>
struct ReplaceItem<TypeList<Type, T...>, 0, NewItem> {
typedef TypeList<NewItem, T...> type;
};
enum Direction {
Left = -1,
Right = 1
};
template<typename OldState, typename Input, typename NewState,
typename Output, Direction Move>
struct Rule {
typedef OldState old_state;
typedef Input input;
typedef NewState new_state;
typedef Output output;
static Direction const direction = Move;
};
template<typename A, typename B>
struct IsSame {
enum { value = false };
};
template<typename A>
struct IsSame<A, A> {
enum { value = true };
};
template<typename Input, typename State, int Position>
struct Configuration {
typedef Input input;
typedef State state;
enum { position = Position };
};
template<int A, int B>
struct Max {
enum { value = A > B ? A : B };
};
template<int n>
struct State {
enum { value = n };
static char const * name;
};
template<int n>
char const* State<n>::name = "unnamed";
struct QAccept {
enum { value = -1 };
static char const* name;
};
struct QReject {
enum { value = -2 };
static char const* name;
};
#define DEF_STATE(ID, NAME) \
typedef State<ID> NAME ; \
NAME :: name = #NAME ;
template<int n>
struct Input {
enum { value = n };
static char const * name;
template<int... I>
struct Generate {
typedef TypeList<Input<I>...> type;
};
};
template<int n>
char const* Input<n>::name = "unnamed";
typedef Input<-1> InputBlank;
#define DEF_INPUT(ID, NAME) \
typedef Input<ID> NAME ; \
NAME :: name = #NAME ;
template<typename Config, typename Transitions, typename = void>
struct Controller {
typedef Config config;
enum { position = config::position };
typedef typename Conditional<
static_cast<int>(GetSize<typename config::input>::value)
<= static_cast<int>(position),
AppendItem<InputBlank, typename config::input>,
Identity<typename config::input>>::type::type input;
typedef typename config::state state;
typedef typename GetItem<input, position>::type cell;
template<typename Item, typename State, typename Cell>
struct Matcher {
typedef typename Item::old_state checking_state;
typedef typename Item::input checking_input;
enum { value = IsSame<State, checking_state>::value &&
IsSame<Cell, checking_input>::value
};
};
typedef typename FindItem<Transitions, Matcher, state, cell>::type rule;
typedef typename ReplaceItem<input, position, typename rule::output>::type new_input;
typedef typename rule::new_state new_state;
typedef Configuration<new_input,
new_state,
Max<position + rule::direction, 0>::value> new_config;
typedef Controller<new_config, Transitions> next_step;
typedef typename next_step::end_config end_config;
typedef typename next_step::end_input end_input;
typedef typename next_step::end_state end_state;
enum { end_position = next_step::position };
};
template<typename Input, typename State, int Position, typename Transitions>
struct Controller<Configuration<Input, State, Position>, Transitions,
typename EnableIf<IsSame<State, QAccept>::value ||
IsSame<State, QReject>::value>::type> {
typedef Configuration<Input, State, Position> config;
enum { position = config::position };
typedef typename Conditional<
static_cast<int>(GetSize<typename config::input>::value)
<= static_cast<int>(position),
AppendItem<InputBlank, typename config::input>,
Identity<typename config::input>>::type::type input;
typedef typename config::state state;
typedef config end_config;
typedef input end_input;
typedef state end_state;
enum { end_position = position };
};
template<typename Input, typename Transitions, typename StartState>
struct TuringMachine {
typedef Input input;
typedef Transitions transitions;
typedef StartState start_state;
typedef Controller<Configuration<Input, StartState, 0>, Transitions> controller;
typedef typename controller::end_config end_config;
typedef typename controller::end_input end_input;
typedef typename controller::end_state end_state;
enum { end_position = controller::end_position };
};
#include <ostream>
template<>
char const* Input<-1>::name = "_";
char const* QAccept::name = "qaccept";
char const* QReject::name = "qreject";
int main() {
DEF_INPUT(1, x);
DEF_INPUT(2, x_mark);
DEF_INPUT(3, split);
DEF_STATE(0, start);
DEF_STATE(1, find_blank);
DEF_STATE(2, go_back);
/* syntax: State, Input, NewState, Output, Move */
typedef TypeList<
Rule<start, x, find_blank, x_mark, Right>,
Rule<find_blank, x, find_blank, x, Right>,
Rule<find_blank, split, find_blank, split, Right>,
Rule<find_blank, InputBlank, go_back, x, Left>,
Rule<go_back, x, go_back, x, Left>,
Rule<go_back, split, go_back, split, Left>,
Rule<go_back, x_mark, start, x, Right>,
Rule<start, split, QAccept, split, Left>> rules;
/* syntax: initial input, rules, start state */
typedef TuringMachine<TypeList<x, x, x, x, split>, rules, start> double_it;
static_assert(IsSame<double_it::end_input,
TypeList<x, x, x, x, split, x, x, x, x>>::value,
"Hmm... This is borky!");
}
" Les modèles C++ sont Turing complets " donne une implémentation d'une machine Turing dans les modèles ... ce qui n'est pas trivial et prouve le point de manière très directe. Bien sûr, ce n'est pas non plus très utile!
Mon C++ est un peu rouillé, donc il n'est peut-être pas parfait, mais il est proche.
template <int N> struct Factorial
{
enum { val = Factorial<N-1>::val * N };
};
template <> struct Factorial<0>
{
enum { val = 1 };
}
const int num = Factorial<10>::val; // num set to 10! at compile time.
Le but est de démontrer que le compilateur évalue complètement la définition récursive jusqu'à ce qu'il atteigne une réponse.
Pour donner un exemple non trivial: http://gitorious.org/metatrace , un traceur de rayons temporels de compilation C++.
Notez que C++ 0x ajoutera une fonction non modèle, à la compilation et complète de Turing sous la forme de constexpr
:
constexpr unsigned int fac (unsigned int u) {
return (u<=1) ? (1) : (u*fac(u-1));
}
Vous pouvez utiliser l'expression constexpr
- partout où vous avez besoin de constantes de temps de compilation, mais vous pouvez également appeler des fonctions constexpr
- avec des paramètres non const.
Une chose intéressante est que cela permettra enfin de calculer les calculs en virgule flottante au moment de la compilation, bien que la norme stipule explicitement que les arithmétiques à virgule flottante au moment de la compilation n'ont pas à correspondre aux arithmétiques à virgule flottante au moment de l'exécution:
bool f(){ char array[1+int(1+0.2-0.1-0.1)]; //Must be evaluated during translation int size=1+int(1+0.2-0.1-0.1); //May be evaluated at runtime return sizeof(array)==size; }
Il n'est pas précisé si la valeur de f() sera vraie ou fausse.
L'exemple factoriel ne montre pas en fait que les modèles sont Turing complets, autant qu'il montre qu'ils prennent en charge la récursivité primitive. La façon la plus simple de montrer que les modèles sont complets est la thèse de Church-Turing, c'est-à-dire en implémentant soit une machine de Turing (désordonnée et un peu inutile) soit les trois règles (app, abs var) du calcul lambda non typé. Ce dernier est beaucoup plus simple et beaucoup plus intéressant.
Ce qui est discuté est une fonctionnalité extrêmement utile lorsque vous comprenez que les modèles C++ permettent une programmation fonctionnelle pure au moment de la compilation, un formalisme expressif, puissant et élégant mais aussi très compliqué à écrire si vous avez peu d'expérience. Notez également combien de personnes trouvent que le simple fait d'obtenir du code fortement modélisé peut souvent nécessiter un gros effort: c'est exactement le cas avec les langages fonctionnels (purs), ce qui rend la compilation plus difficile mais étonnamment produit du code qui ne nécessite pas de débogage.
The Book Modern C++ Design - Generic Programming and Design Pattern par Andrei Alexandrescu est le meilleur endroit pour acquérir une expérience pratique avec des modèles de programmation génériques utiles et puissants.
Je pense que ça s'appelle modèle de méta-programmation .
Eh bien, voici une implémentation de Turing Machine au moment de la compilation exécutant un castor occupé à 4 états et 2 symboles
#include <iostream>
#pragma mark - Tape
constexpr int Blank = -1;
template<int... xs>
class Tape {
public:
using type = Tape<xs...>;
constexpr static int length = sizeof...(xs);
};
#pragma mark - Print
template<class T>
void print(T);
template<>
void print(Tape<>) {
std::cout << std::endl;
}
template<int x, int... xs>
void print(Tape<x, xs...>) {
if (x == Blank) {
std::cout << "_ ";
} else {
std::cout << x << " ";
}
print(Tape<xs...>());
}
#pragma mark - Concatenate
template<class, class>
class Concatenate;
template<int... xs, int... ys>
class Concatenate<Tape<xs...>, Tape<ys...>> {
public:
using type = Tape<xs..., ys...>;
};
#pragma mark - Invert
template<class>
class Invert;
template<>
class Invert<Tape<>> {
public:
using type = Tape<>;
};
template<int x, int... xs>
class Invert<Tape<x, xs...>> {
public:
using type = typename Concatenate<
typename Invert<Tape<xs...>>::type,
Tape<x>
>::type;
};
#pragma mark - Read
template<int, class>
class Read;
template<int n, int x, int... xs>
class Read<n, Tape<x, xs...>> {
public:
using type = typename std::conditional<
(n == 0),
std::integral_constant<int, x>,
Read<n - 1, Tape<xs...>>
>::type::type;
};
#pragma mark - N first and N last
template<int, class>
class NLast;
template<int n, int x, int... xs>
class NLast<n, Tape<x, xs...>> {
public:
using type = typename std::conditional<
(n == sizeof...(xs)),
Tape<xs...>,
NLast<n, Tape<xs...>>
>::type::type;
};
template<int, class>
class NFirst;
template<int n, int... xs>
class NFirst<n, Tape<xs...>> {
public:
using type = typename Invert<
typename NLast<
n, typename Invert<Tape<xs...>>::type
>::type
>::type;
};
#pragma mark - Write
template<int, int, class>
class Write;
template<int pos, int x, int... xs>
class Write<pos, x, Tape<xs...>> {
public:
using type = typename Concatenate<
typename Concatenate<
typename NFirst<pos, Tape<xs...>>::type,
Tape<x>
>::type,
typename NLast<(sizeof...(xs) - pos - 1), Tape<xs...>>::type
>::type;
};
#pragma mark - Move
template<int, class>
class Hold;
template<int pos, int... xs>
class Hold<pos, Tape<xs...>> {
public:
constexpr static int position = pos;
using tape = Tape<xs...>;
};
template<int, class>
class Left;
template<int pos, int... xs>
class Left<pos, Tape<xs...>> {
public:
constexpr static int position = typename std::conditional<
(pos > 0),
std::integral_constant<int, pos - 1>,
std::integral_constant<int, 0>
>::type();
using tape = typename std::conditional<
(pos > 0),
Tape<xs...>,
Tape<Blank, xs...>
>::type;
};
template<int, class>
class Right;
template<int pos, int... xs>
class Right<pos, Tape<xs...>> {
public:
constexpr static int position = pos + 1;
using tape = typename std::conditional<
(pos < sizeof...(xs) - 1),
Tape<xs...>,
Tape<xs..., Blank>
>::type;
};
#pragma mark - States
template <int>
class Stop {
public:
constexpr static int write = -1;
template<int pos, class tape> using move = Hold<pos, tape>;
template<int x> using next = Stop<x>;
};
#define ADD_STATE(_state_) \
template<int> \
class _state_ { };
#define ADD_RULE(_state_, _read_, _write_, _move_, _next_) \
template<> \
class _state_<_read_> { \
public: \
constexpr static int write = _write_; \
template<int pos, class tape> using move = _move_<pos, tape>; \
template<int x> using next = _next_<x>; \
};
#pragma mark - Machine
template<template<int> class, int, class>
class Machine;
template<template<int> class State, int pos, int... xs>
class Machine<State, pos, Tape<xs...>> {
constexpr static int symbol = typename Read<pos, Tape<xs...>>::type();
using state = State<symbol>;
template<int x>
using nextState = typename State<symbol>::template next<x>;
using modifiedTape = typename Write<pos, state::write, Tape<xs...>>::type;
using move = typename state::template move<pos, modifiedTape>;
constexpr static int nextPos = move::position;
using nextTape = typename move::tape;
public:
using step = Machine<nextState, nextPos, nextTape>;
};
#pragma mark - Run
template<class>
class Run;
template<template<int> class State, int pos, int... xs>
class Run<Machine<State, pos, Tape<xs...>>> {
using step = typename Machine<State, pos, Tape<xs...>>::step;
public:
using type = typename std::conditional<
std::is_same<State<0>, Stop<0>>::value,
Tape<xs...>,
Run<step>
>::type::type;
};
ADD_STATE(A);
ADD_STATE(B);
ADD_STATE(C);
ADD_STATE(D);
ADD_RULE(A, Blank, 1, Right, B);
ADD_RULE(A, 1, 1, Left, B);
ADD_RULE(B, Blank, 1, Left, A);
ADD_RULE(B, 1, Blank, Left, C);
ADD_RULE(C, Blank, 1, Right, Stop);
ADD_RULE(C, 1, 1, Left, D);
ADD_RULE(D, Blank, 1, Right, D);
ADD_RULE(D, 1, Blank, Right, A);
using tape = Tape<Blank>;
using machine = Machine<A, 0, tape>;
using result = Run<machine>::type;
int main() {
print(result());
return 0;
}
Exécution de preuve Ideone: https://ideone.com/MvBU3Z
Explication: http://victorkomarov.blogspot.ru/2016/03/compile-time-turing-machine.html
Github avec plus d'exemples: https://github.com/fnz/CTTM
Vous pouvez consulter cet article du Dr Dobbs sur une implémentation FFT avec des modèles qui, je pense, ne sont pas si triviaux. Le point principal est de permettre au compilateur d'effectuer une meilleure optimisation que pour les implémentations sans modèle car l'algorithme FFT utilise beaucoup de constantes (tables sin par exemple)
Cela peut être utile si vous souhaitez calculer des constantes au moment de la compilation, du moins en théorie. Découvrez métaprogrammation du modèle .
Il est également amusant de souligner qu'il s'agit d'un langage purement fonctionnel bien que presque impossible à déboguer. Si vous regardez James post, vous verrez ce que je veux dire par fonctionnel. En général, ce n'est pas la fonctionnalité la plus utile de C++. Il n'a pas été conçu pour cela. C'est quelque chose qui a été découvert.
A Turing machine est Turing-complete, mais cela ne signifie pas que vous devriez en utiliser un pour le code de production.
Essayer de faire quelque chose de non trivial avec des modèles est, selon mon expérience, désordonné, laid et inutile. Vous n'avez aucun moyen de "déboguer" votre "code", les messages d'erreur au moment de la compilation seront cryptiques et généralement dans les endroits les plus improbables, et vous pouvez obtenir les mêmes avantages de performances de différentes manières. (Indice: 4! = 24). Pire encore, votre code est incompréhensible pour le programmeur C++ moyen, et sera probablement non portable en raison des niveaux de prise en charge étendus dans les compilateurs actuels.
Les modèles sont parfaits pour la génération de code générique (classes de conteneurs, wrappers de classes, mix-ins), mais non - à mon avis, l'intégralité de Turing des modèles est PAS UTILE en pratique.
Un exemple qui est raisonnablement utile est une classe de rapport. Il y a quelques variantes qui flottent. La capture du cas D == 0 est assez simple avec des surcharges partielles. Le véritable calcul consiste à calculer le GCD de N et D et à compiler le temps. Ceci est essentiel lorsque vous utilisez ces ratios dans les calculs au moment de la compilation.
Exemple: lorsque vous calculez des centimètres (5) * kilomètres (5), au moment de la compilation, vous multipliez le rapport <1,100> et le rapport <1000,1>. Pour éviter un débordement, vous voulez un ratio <10,1> au lieu d'un ratio <1000,100>.
Juste un autre exemple de comment ne pas programmer:
modèle <int Profondeur, int A, nom de type B> struct K17 { statique const int x = K17 <Profondeur + 1, 0, K17 <Profondeur , A, B>> :: x + K17 <Profondeur + 1, 1, K17 <Profondeur, A, B>> :: x + K17 <Profondeur + 1, 2, K17 <Profondeur, A, B>> :: x + K17 <Profondeur + 1, 3, K17 <Profondeur, A, B>> :: x + K17 <Profondeur + 1, 4 , K17 <Profondeur, A, B>> :: x; }; Modèle <int A, nom de type B> Struct K17 <16, A, B> {statique const int x = 1; }; statique const int z = K17 <0,0, int> :: x; void main (void) {}
Poster sur les modèles C++ sont complets