constexpr controllo string-literal: sintassi breve, nessuna possibilità di runtime

Aug 21 2020

EDIT: rinominato, poiché la mia soluzione finale non utilizza un metodo di avvelenamento.

Sto cercando un modo per impedire che un metodo constexpr venga chiamato in fase di esecuzione. Sto scrivendo una funzione che accetta una stringa letterale, quindi non posso semplicemente utilizzare un NTTP come un modo per richiedere un constexprparametro:

template<const char* str>
auto func() {...}

Perché allora anche gli usi legittimi di constexpr diventano ingombranti, richiedono che i valori abbiano un collegamento statico e non è possibile inserire una stringa letterale. Voglio fare:

constexpr auto func(const char* str) {...}

Il motivo è perché controllo la stringa rispetto a un elenco di valori e desidero controllare STATICAMENTE che il parametro sia contenuto nel set consentito. Posso farlo facilmente: semplicemente throw()inserendo una constexprfunzione, puoi causare un errore in fase di compilazione. Ma non voglio la possibilità di generare codice di produzione con qualche ramo che fa uscire il programma in fase di runtime. Ciò causerebbe un grosso problema nel mio campo; questa caratteristica è piacevole da avere in un programma che fa "altre cose importanti" e accadono cose brutte se il programma termina.

Ho letto di un sacco di possibili modi per farlo:

  1. Usa C ++ 20 consteval- non hai C ++ 20
  2. Usa C ++ 20 std::is_constant_evaluated- non hai C ++ 20
  3. Avvelena il metodo in fase di esecuzione restituendo un risultato a un simbolo indefinito (ad esempio, extern int iche non viene mai definito). Il compilatore non crea mai codice che restituisce quel simbolo se il metodo viene chiamato in fase di compilazione, ma lo fa se il metodo viene chiamato in fase di runtime, provocando un errore di collegamento. Funziona, ma brutto errore del linker; non il mio preferito. Una versione di questo è mostrata nel mio post qui: le funzioni Constexpr non vengono chiamate in fase di compilazione se il risultato viene ignorato .
  4. In C ++ 17, noexceptviene aggiunto automaticamente a qualsiasi chiamata a una constexprfunzione che viene effettivamente chiamata in un constexprcontesto. Quindi puoi fare noexcept(foo(1))vs noexcept(foo(i))for constexpr int foo(int i)(non dichiarato esplicitamente noexcept) per rilevare se icausa o meno la chiamata constexpr. Ma non puoi farlo dall'interno di una constexprfunzione che ha accettato un parametro: devi farlo dal sito di chiamata. Quindi: probabilmente richiede una MACRO di supporto (non la mia preferita ma funziona).
  5. Crea un lambda il cui tipo restituito non è valido se alcune variabili al di fuori dell'ambito del lambda non lo sono constexpr. Questo post entra nei dettagli:https://stackoverflow.com/a/40413051

Quindi mi propongo di usare il n. 3 o il n. 4 + una macro, ma *** questo post riguarda il n. 5 ***, o idee totalmente nuove.

Qualcuno può trovare un modo per fare # 5 senza un lambda, per esempio? Dopodiché, voglio vedere se riesco a trovare un modo per usarlo all'interno della constexprfunzione stessa piuttosto che richiederlo dal sito di chiamata. Per ora, prova ad avvelenare una constexprfunzione se viene chiamata in runtime, dimenticati di "rilevare" se la chiamata di funzione è constexpr.

Posso ricreare i risultati di # 5 creando un lambda maincome ha fatto l'autore, ma in realtà non è molto utile e non sono ancora convinto che sia completamente legale. Per cominciare, tutto ciò che può essere fatto con un lambda può essere fatto senza un lambda - giusto ??? Non riesco nemmeno a far funzionare il metodo dell'autore originale senza un lambda. Sembra un primo passo necessario per farlo funzionare al di fuori di main().

Di seguito sono riportate un paio di idee che ho provato a ricreare # 5 senza lambda. Esempio dal vivo con un miliardo di permutazioni in più, nessuna delle quali funziona:https://onlinegdb.com/B1oRjpTGP

// Common
template<int>
using Void = void;

// Common
struct Delayer {
    constexpr auto delayStatic(int input) { return input; }
};

// Attempt 1
template<typename PoisonDelayer>
constexpr auto procurePoison(int i) {
    struct Poison {
        // error: use of parameter from containing function
        // constexpr auto operator()() const -> Void<(PoisonDelayer::delayStatic(i), 0)> {}
    } poison;
    
    return poison;
}

// Attempt 2
struct PoisonInnerTemplate {
    const int& _i;

    // Internal compiler error / use of this in a constexpr
    template<typename PoisonDelayer>
    auto drink() const -> Void<(PoisonDelayer::delayStatic(_i), 0)> {}
};

int main()
{
    auto attempt1 = procurePoison<Delayer>(1);
    
    constexpr int i = 1;
    auto attempt2 = PoisonInnerTemplate{i};
    attempt2.drink<Delayer>();

    return 0;
}

Un'altra cosa: ho giocato con l'idea di creare un elenco predefinito di tag consentiti (avvolgere la stringa in una struttura in modo che possa essere un NTTP), e metterli in una sorta di contenitore, e quindi avere un metodo per recuperarli loro. I problemi sono: (1) la sintassi del sito di chiamata diventa piuttosto prolissa per usarli, (2) sebbene andrebbe bene per il sito di chiamata utilizzare una sintassi del tipo MyTags::TAG_ONE, il mio programma deve essere in grado di conoscere la serie completa di tag, quindi devono essere in un array o in una variabile di modello, (3) l'utilizzo di un array non funziona, perché ottenere un elemento di array produce un elemento rvalue, che non ha collegamento, quindi non può essere alimentato come un NTTP, (4) l'utilizzo di una variabile modello con specializzazione esplicita per definire ogni tag richiede che la variabile modello sia di ambito globale, il che non funziona bene per me ...

Questo è il meglio che posso fare - penso che sia un po 'brutto ...:

struct Tag {
    const char* name;
};

template<auto& tag>
void foo() {}

struct Tags {
    static constexpr Tag invalid = {};
    static constexpr Tag tags[] = {{"abc"}, {"def"}};

    template<size_t N>
    static constexpr Tag tag = tags[N];
    
    template<size_t N = 0>
    static constexpr auto& getTag(const char* name) {
        if constexpr(N<2) {
            if(string_view(name)==tag<N>.name) {
                return tag<N>;
            } else {
                return getTag<N+1>(name);
            }
        } else {
            return invalid;
        }
    }
};

int main()
{
    foo<Tags::getTag("abc")>();
}

Risposte

Sean Aug 24 2020 at 20:32

Ecco la mia risposta, che controlla che una stringa letterale sia all'interno di un set consentito in COMPILE-TIME, quindi esegue un'azione basata sul valore di quella stringa. Non constexprè necessario alcun avvelenamento delle funzioni e non ci sono ancora requisiti complicati per fornire stringhe letterali con collegamento statico.

Il merito va a Jarod42 per l '"opzione abbreviata 2", che utilizza gccun'estensione per i letterali definiti dall'utente del modello di stringa, che fa parte di C ++ 20 ma non di C ++ 17.

Penso di essere abbastanza soddisfatto di una qualsiasi delle tre sintassi "stenografiche" del sito di chiamata. Gradirei comunque qualsiasi alternativa o miglioramento o suggerimento su ciò che ho incasinato. Inoltro perfetto, ecc. È lasciato come esercizio per il lettore ;-)

Dimostrazione dal vivo: https://onlinegdb.com/S1K_7sb7D

// Helper for Shorthand Option 1 (below)
template<typename Singleton>
Singleton* singleton;

// Helper to store string literals at compile-time
template<typename ParentDispatcher>
struct Tag {
    using Parent = ParentDispatcher;
    const char* name;
};

// ---------------------------------
// DISPATCHER:
// ---------------------------------
// Call different functions at compile-time based upon
// a compile-time string literal.
// ---------------------------------

template<auto& nameArray, typename FuncTuple>
struct Dispatcher {
    FuncTuple _funcs;
    
    using DispatcherTag = Tag<Dispatcher>;
    
    template<size_t nameIndex>
    static constexpr DispatcherTag TAG = {nameArray[nameIndex]};
    
    static constexpr DispatcherTag INVALID_TAG = {};

    Dispatcher(const FuncTuple& funcs) : _funcs(funcs) {
        singleton<Dispatcher> = this;
    }

    template<size_t nameIndex = 0>
    static constexpr auto& tag(string_view name) {
        if(name == nameArray[nameIndex]) {
            return TAG<nameIndex>;
        } else {
            if constexpr (nameIndex+1 < nameArray.size()) {
                return tag<nameIndex+1>(name);
            } else {
                return INVALID_TAG;
            }
        }
    }

    static constexpr size_t index(string_view name) {
        for(size_t nameIndex = 0; nameIndex < nameArray.size(); ++nameIndex) {
            if(name == nameArray[nameIndex]) {
                return nameIndex;
            }
        }
        return nameArray.size();
    }
    
    constexpr auto& operator()(const char* name) const {
        return tag(name);
    }

    template<auto& tag, typename... Args>
    auto call(Args... args) const {
        static constexpr size_t INDEX = index(tag.name);
        static constexpr bool VALID = INDEX != nameArray.size();
        static_assert(VALID, "Invalid tag.");

        return get<INDEX*VALID>(_funcs)(args...);
    }
};

template<auto& nameArray, typename FuncTuple>
auto makeDispatcher(const FuncTuple& funcs) {
    return Dispatcher<nameArray, FuncTuple>(funcs);
}

// ---------------------------------
// SHORTHAND: OPTION 1
// ---------------------------------
// Use a singleton pattern and a helper to let a tag be associated with a
// specific dispatcher, so that the call-site need not specify dispatcher twice
// ---------------------------------

template<auto& tag, typename... Args>
auto call(Args... args) {
    using Tag = remove_reference_t<decltype(tag)>;
    using ParentDispatcher = typename Tag::Parent;
    static auto dispatcher = singleton<ParentDispatcher>;

    return dispatcher->template call<tag>(args...);
}

// ---------------------------------
// SHORTHAND: OPTION 2
// ---------------------------------
// Use a string template user-defined literal operator to shorten call-site syntax
// gcc supports this as an extension implementing proposal N3599 (standardized in C++20)
// If warnings occur, try pragma GCC diagnostic ignored "-Wgnu-string-literal-operator-template"
// ---------------------------------

// Need characters to be in contiguous memory on the stack (not NTTPs) for TAG_FROM_LITERAL
template<char... name>
constexpr char NAME_FROM_LITERAL[] = {name..., '\0'};

// Don't need to specify Dispatcher with user-defined literal method; will use dispatcher.check<>()
struct TagFromLiteral {};

// Need to have a constexpr variable with linkage to use with dispatcher.check<>()
template<char... name>
constexpr Tag<TagFromLiteral> TAG_FROM_LITERAL = {NAME_FROM_LITERAL<name...>};

// Create a constexpr variable with linkage for use with dispatcher.check<>(), via "MyTag"_TAG
template<typename Char, Char... name>
constexpr auto& operator"" _TAG() {
    return TAG_FROM_LITERAL<name...>;
}

// ---------------------------------
// SHORTHAND: OPTION 3
// ---------------------------------
// Use a macro so the call-site need not specify dispatcher twice
// ---------------------------------

#define DISPATCH(dispatcher, name) dispatcher.call<dispatcher(name)>

// ---------------------------------
// COMMON: TEST FUNCTIONS
// ---------------------------------

bool testFunc1(int) { cout << "testFunc1" << endl; }
bool testFunc2(float) { cout << "testFunc2" << endl; }
bool testFunc3(double) { cout << "testFunc3" << endl; }

static constexpr auto funcs = make_tuple(&testFunc1, &testFunc2, &testFunc3);
static constexpr auto names = array{"one", "two", "three"};

int main()
{
    // Create a test dispatcher
    auto dispatcher = makeDispatcher<names>(funcs);

    // LONG-HAND: call syntax: a bit verbose, but operator() helps
    dispatcher.call<dispatcher("one")>(1);
    
    // SHORTHAND OPTION 1: non-member helper, singleton maps back to dispatcher
    call<dispatcher("one")>(1);

    // SHORTHAND OPTION 2: gcc extension for string UDL templates (C++20 standardizes this)
    dispatcher.call<"one"_TAG>(1);

    // SHORHAND OPTION 3: Macro
    DISPATCH(dispatcher, "one")(1);

    return 0;
}