Modelli C ++ Turing-completo?


Risposte:


110

Esempio

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

È stato un po 'divertente ma non molto pratico.

Per rispondere alla seconda parte della domanda:
questo fatto è utile nella pratica?

Risposta breve: una specie di.

Risposta lunga: Sì, ma solo se sei un demone modello.

Realizzare una buona programmazione usando la meta-programmazione di modelli che è davvero utile per gli altri (cioè una libreria) è davvero molto difficile (sebbene fattibile). Per aiutare a potenziare ha anche MPL aka (Meta Programming Library). Ma prova a eseguire il debug di un errore del compilatore nel codice del tuo modello e sarai pronto per una lunga e difficile corsa.

Ma un buon esempio pratico di come viene utilizzato per qualcosa di utile:

Scott Meyers ha lavorato con estensioni al linguaggio C ++ (io uso il termine in modo approssimativo) utilizzando le funzionalità di creazione dei modelli. Puoi leggere il suo lavoro qui " Applicazione delle funzionalità del codice "


36
Dang there gone concepts (puof)
Martin York

5
Ho solo un piccolo problema con l'esempio fornito: non sfrutta la (completa) completezza di Turing del sistema di template C ++. Il fattoriale può essere trovato anche usando le funzioni ricorsive primitive, che non sono turing-complete
Dalibor Frivaldsky

4
e ora abbiamo concetti lite
nurettin

1
Nel 2017, stiamo spingendo i concetti ancora più indietro. Ecco la speranza per il 2020.
DeiDei

2
@MarkKegel 12 anni dopo: D
Victor,

181

Ho realizzato una macchina turing in C ++ 11. Le funzionalità aggiunte da C ++ 11 non sono significative per la macchina turing. Fornisce solo elenchi di regole di lunghezza arbitraria utilizzando modelli variadici, invece di utilizzare la metaprogrammazione macro perversa :). I nomi delle condizioni vengono utilizzati per produrre un diagramma su stdout. ho rimosso quel codice per mantenere breve l'esempio.

#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!");
}

131
Hai davvero troppo tempo a disposizione.
Mark Kegel

2
Sembra lisp tranne che con una parola certin che sostituisce tutte quelle parentesi.
Simon Kuang

1
La fonte completa è pubblicamente disponibile da qualche parte, per il lettore curioso? :)
OJFord

1
Solo il tentativo merita molto più credito :-) Questo codice viene compilato (gcc-4.9) ma non fornisce alcun output - un po 'più di informazioni, come un post sul blog, sarebbe fantastico.
Alfred Bratterud

2
@OllieFord Ho trovato una versione di esso su una pagina di pastebin e l'ho riproposta qui: coliru.stacked-crooked.com/a/de06f2f63f905b7e .
Johannes Schaub - litb


13

Il mio C ++ è un po 'arrugginito, quindi potrebbe non essere perfetto, ma è vicino.

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.

Il punto è dimostrare che il compilatore sta valutando completamente la definizione ricorsiva finché non raggiunge una risposta.


1
Umm ... non hai bisogno di avere "template <>" sulla riga prima di struct Factorial <0> per indicare la specializzazione del template?
paxos1977

11

Per dare un esempio non banale: http://gitorious.org/metatrace , un tracciante del tempo di compilazione C ++.

Si noti che C ++ 0x aggiungerà una struttura non template, in fase di compilazione, turing-complete sotto forma di constexpr:

constexpr unsigned int fac (unsigned int u) {
        return (u<=1) ? (1) : (u*fac(u-1));
}

Puoi usare constexpr-expression ovunque tu abbia bisogno di costanti di tempo di compilazione, ma puoi anche chiamare constexpr-functions con parametri non-const.

Una cosa interessante è che questo consentirà finalmente la matematica in virgola mobile del tempo di compilazione, sebbene lo standard dichiari esplicitamente che l'aritmetica in virgola mobile del tempo di compilazione non deve corrispondere all'aritmetica in virgola mobile di runtime:

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

Non è specificato se il valore di f () sarà vero o falso.



8

L'esempio fattoriale in realtà non mostra che i modelli sono completi di Turing, tanto quanto mostra che supportano la ricorsione primitiva. Il modo più semplice per dimostrare che i modelli si stanno completando è tramite la tesi di Church-Turing, ovvero implementando una macchina di Turing (disordinata e un po 'inutile) o le tre regole (app, abs var) del lambda calcolo non tipizzato. Quest'ultimo è molto più semplice e molto più interessante.

Ciò che viene discusso è una caratteristica estremamente utile quando si capisce che i modelli C ++ consentono una programmazione funzionale pura in fase di compilazione, un formalismo che è espressivo, potente ed elegante ma anche molto complicato da scrivere se si ha poca esperienza. Notare anche quante persone trovano che il solo fatto di ottenere codice fortemente basato su modelli può spesso richiedere un grande sforzo: questo è esattamente il caso dei linguaggi funzionali (puri), che rendono la compilazione più difficile ma sorprendentemente producono codice che non richiede il debug.


Ehi, a quali tre regole ti riferisci, mi chiedo, per "app, abs, var"? Presumo che i primi due siano rispettivamente applicazione di funzioni e astrazione (definizione lambda (?)). È così? E qual è il terzo? Qualcosa che ha a che fare con le variabili?
Wizek,

Personalmente penso che sarebbe generalmente meglio che un linguaggio supporti la ricorsione primitiva nel compilatore piuttosto che Turing Complete, poiché un compilatore per un linguaggio che supporta la ricorsione primitiva in fase di compilazione potrebbe garantire che qualsiasi compilazione verrà completata o fallita, ma uno il cui processo di costruzione è Turing Complete non può, se non vincolando artificialmente la build in modo che non sia Turing Complete.
supercat

5

Penso che si chiami meta-programmazione dei modelli .


2
Questo è il lato utile. Lo svantaggio è che dubito che la maggior parte delle persone (e certamente non io) capirà mai anche solo una piccola percentuale di quello che sta succedendo nella maggior parte di queste cose. È roba orribilmente illeggibile e irrinunciabile.
Michael Burr,

3
Questo è lo svantaggio dell'intero linguaggio C ++, credo. Sta diventando un mostro ...
Federico A. Ramponi

C ++ 0x promette di renderlo molto più semplice (e nella mia esperienza, il problema più grande sono i compilatori che non lo supportano completamente, cosa che C ++ 0x non aiuterà). I concetti in particolare sembrano chiarire le cose, come sbarazzarsi di molte cose di SFINAE, che è difficile da leggere.
coppro

@MichaelBurr Il comitato C ++ non si preoccupa delle cose illeggibili e non mantenibili; hanno solo l'amore per aggiungere funzionalità.
Sapphire_Brick

4

Bene, ecco un'implementazione di Turing Machine in fase di compilazione che esegue un castoro occupato a 4 stati e 2 simboli

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

Prova Ideone: https://ideone.com/MvBU3Z

Spiegazione: http://victorkomarov.blogspot.ru/2016/03/compile-time-turing-machine.html

Github con altri esempi: https://github.com/fnz/CTTM


3

Puoi controllare questo articolo del Dr. Dobbs su un'implementazione FFT con modelli che non credo siano così banali. Il punto principale è consentire al compilatore di eseguire un'ottimizzazione migliore rispetto alle implementazioni non di modello poiché l'algoritmo FFT utilizza molte costanti (tabelle sin per esempio)

parte I

Seconda parte


2

È anche divertente sottolineare che si tratta di un linguaggio puramente funzionale anche se quasi impossibile da eseguire il debug. Se guardi il post di James , vedrai cosa intendo per essere funzionale. In generale non è la caratteristica più utile di C ++. Non è stato progettato per farlo. È qualcosa che è stato scoperto.



1

Un esempio ragionevolmente utile è una classe di rapporto. Ci sono alcune varianti in giro. Catturare il caso D == 0 è abbastanza semplice con sovraccarichi parziali. Il vero calcolo sta nel calcolare il GCD di N e D e il tempo di compilazione. Ciò è essenziale quando si utilizzano questi rapporti nei calcoli in fase di compilazione.

Esempio: quando calcoli centimetri (5) * chilometri (5), in fase di compilazione moltiplicherai rapporto <1,100> e rapporto <1000,1>. Per evitare l'overflow, si desidera un rapporto <10,1> invece di un rapporto <1000,100>.


0

Una macchina di Turing è completa di Turing, ma ciò non significa che dovresti usarne una per il codice di produzione.

Cercare di fare qualcosa di non banale con i modelli è nella mia esperienza complicato, brutto e inutile. Non hai modo di "eseguire il debug" del tuo "codice", i messaggi di errore in fase di compilazione saranno criptici e di solito nei posti più improbabili, e puoi ottenere gli stessi vantaggi in termini di prestazioni in modi diversi. (Suggerimento: 4! = 24). Peggio ancora, il tuo codice è incomprensibile per il programmatore C ++ medio e probabilmente non sarà portabile a causa degli ampi livelli di supporto all'interno dei compilatori attuali.

I modelli sono ottimi per la generazione di codice generico (classi contenitore, wrapper di classe, mix-in), ma no - a mio parere la completezza di Turing dei modelli NON è UTILE nella pratica.


4! potrebbe essere 24, ma qual è MY_FAVORITE_MACRO_VALUE! ? OK, nemmeno io penso che questa sia una buona idea.
Jeffrey L Whitledge,

0

Solo un altro esempio di come non programmare:

template <int Depth, int A, typename B>
struct K17 {
    const statico int x =
    K17 <Profondità + 1, 0, K17 <Profondità, A, B>> :: x
    + K17 <Profondità + 1, 1, K17 <Profondità, A, B>> :: x
    + K17 <Profondità + 1, 2, K17 <Profondità, A, B>> :: x
    + K17 <Profondità + 1, 3, K17 <Profondità, A, B>> :: x
    + K17 <Profondità + 1, 4, K17 <Profondità, A, B>> :: x;
};
template <int A, typename B>
struct K17 <16, A, B> {static const int x = 1; };
const statico int z = K17 <0,0, int> :: x;
void main (void) {}

I modelli di post in C ++ sono stati completati


per i curiosi, la risposta per x è pow (5,17-profondità);
volato il

Il che è molto più semplice da vedere quando ti rendi conto che gli argomenti del modello A e B non fanno nulla e li cancellano, quindi sostituisci tutte le aggiunte con K17<Depth+1>::x * 5.
David Stone
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.