Search code examples
c++runtimeconstexprcompile-time-type-checkingconstexpr-function

constexpr string-literal checking: short syntax, no runtime possibility


EDIT: Renamed, as my final solution does not use a poisoning method.

I'm looking for a way to prevent a constexpr method from being called at runtime. I'm writing a function that accepts a string literal, so I cannot simply use a NTTP as a way to require a constexpr parameter:

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

Because then even legitimate constexpr uses become cumbersome, requiring values to have static linkage, and you can't feed in a string literal. I want to do:

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

The reason is because I check the string against a list of values, and want to STATICALLY check that the parameter is contained in the allowed set. I can do this easily: by just throw()'ing in a constexpr function, you can cause a compile-time error. But I do not want the possibility of generating production code with some branch that causes the program to exit at runtime. That would cause a major problem in my field; this feature is a nice-to-have in a program that does "other important stuff" and bad things happen if the program terminates.

I read about a whole bunch of possible ways to do this:

  1. Use C++20 consteval - don't have C++20
  2. Use C++20 std::is_constant_evaluated - don't have C++20
  3. Poison the method at runtime by returning a result to an undefined symbol (e.g. extern int i that never gets defined). The compiler never creates code which returns that symbol if the method is called at compile-time, but it does if the method is called at runtime, resulting in a link error. Works, but ugly linker error; not my favorite. A version of that is shown in my post here: Constexpr functions not called at compile-time if result is ignored.
  4. In C++17, noexcept gets automatically added onto any call to a constexpr function that actually gets called in a constexpr context. So you can do noexcept(foo(1)) vs noexcept(foo(i)) for constexpr int foo(int i) (not explicitly declared noexcept) to detect whether i causes the call to be constexpr/not. But you can't do that from within a constexpr function that has accepted some parameter - you need to do it from the call-site. So: probably requires a helper MACRO (not my favorite but it works).
  5. Create a lambda whose return type is invalid if certain variables outside the scope of the lambda are not constexpr. This post goes into detail: https://stackoverflow.com/a/40413051

So I'm leaning towards using #3 or #4 + a macro, but ***this post is about #5 ***, or totally new ideas.

Can anyone come up with a way to do #5 without a lambda, for example? After that, I want to see if I can come up with a way to use it within the constexpr function itself rather than requiring it to be used from the call-site. For now, just try to poison a constexpr function if it is called at runtime, forget about "detecting" whether the function call is constexpr.

I can re-create the results of #5 by creating a lambda in main as the author did, but that's not actually very useful, and I'm still not convinced that it's fully legal. To start with, anything that can be done with a lambda can be done without a lambda -- right??? I can't even get the original author's method to work without a lambda. That seems like a first required step to get it to work outside of main().

Below are a couple ideas I've tried to recreate #5 without a lambda. Live example with a billion more permutations, none of which work: 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;
}

One more thing: I toyed with the idea of making a pre-defined list of allowed tags (wrap the string in a struct so it can be a NTTP), and putting them in some kind of container, and then having a method to retrieve them. The problems are: (1) the call-site syntax gets quite verbose to use them, (2) although it'd be fine for the call-site to use a syntax like MyTags::TAG_ONE, my program needs to be able to know the full set of tags, so they need to be in an array or a template variable, (3) using an array doesn't work, because getting an array element produces an rvalue, which doesn't have linkage, so can't be fed as a NTTP, (4) using a template variable with explicit specialization to define each tag requires the template variable to be global-scope, which doesn't work well for me...

This is about the best I can do - I think it's kind of ugly...:

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

Solution

  • Here's my own answer, which checks that a string literal is within an allowed set at COMPILE-TIME, then performs an action based upon the value of that string. No poisoning of constexpr functions is needed, and there are still no cumbersome requirements to provide string literals with static linkage.

    Credit goes to Jarod42 for "shorthand option 2", which uses a gcc extension for string template user-defined literals, which is part of C++20 but not C++17.

    I think I'm happy enough with any of the three "shorthand" call-site syntaxes. I would still welcome any alternatives or improvements, or pointers on what I messed up. Perfect forwarding, etc. is left as an exercise for the reader ;-)

    Live demo: 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;
    }