Search code examples
c++stringruntimeconstexprconstexpr-function

constexpr constructor not called as constexpr for implicit type conversion


I made some code which is capable of dispatching to a function based upon the call-site providing a string associated with a given function (via a tuple of function pointers and a parallel array). Instead of accepting a string directly, the dispatch function accepts a Callable type, where a const char* is convertible to a Callable.

The constructor of Callable is constexpr, and looks up a function from the noted tuple with a basic recursive search. I've verified that the constructor is capable of working correctly and creating a constexpr Callable (example included). Since the dispatch function receives the arguments to pass to the Callable's operator(), I know the expected function signature of the Callable's operator() at the time I create it.

I'm trying to perform two checks at compile-time, when they can be done at compile-time. First, I check that the provided string exists in the pre-defined array of strings, at all. Second, I check that the signature of the function associated with that string matches the expected signature from the tuple of function pointers. I create "friendly" error messages at compile-time by throw()'ing within the constexpr method that looks up the function.

I've verified that by creating a constexpr callable, I get the expected error messages at compile-time. This works. What doesn't work is getting compile-time messages if I use my Dispatcher directly, letting the call-site convert a string into a Callable. I know that when I use runtime parameters, my dispatch function won't be called in a constexpr context - I intentionally didn't make that function constexpr; the point is to call it with runtime values. But I thought that implicit conversions "happen at the call-site", not within the called function.

Therefore, I thought that in a call like dispatcher("one", 1) (which calls the first function with a parameter of 1) would look like: "one" gets converted to a Callable at the call-site, then a call gets made as dispatcher(Callable("one"), 1). That would mean that the constexpr constructor could be used, at least. In my experience, as long as you don't ignore the result of a constexpr call, the call is made as constexpr if it can be, else it is made as runtime. See Constexpr functions not called at compile-time if result is ignored. This isn't happening -- the conversion constructor is being called at runtime when the conversion happens within a call to my dispatch function!

Does anyone know of a way I can change my code to get the conversion constructor to be called at compile-time if it can be??? I found a totally different solution to solve this general class of problem in this post, but frankly I like the syntax of the code below better, if I could get it working.

I'm not going to include the above code in the body of this post, but rather include a more canonical example that demonstrates the behavior and also shows the behavior I saw in the post I referenced above, all-in-one.

Live demo of the below: https://onlinegdb.com/r1s1OE77v

Live demo of my "real" problem, if interested: https://onlinegdb.com/rJCQ2bGXw

First the "test fixtures":

// Modified from https://stackoverflow.com/a/40410624/12854372

// In a constexpr context, ContextIsConstexpr1(size_t) always
// simply sets _s to 1 successfully.

extern bool no_symbol_s_is_zero;

struct ContextIsConstexpr1 {
    size_t _s;

    constexpr ContextIsConstexpr1(size_t s) : _s(s ? 1 : no_symbol_s_is_zero) {}
};

// In a constexpr context, ContextIsConstexpr2(size_t) will cause
// a compile-time error if 0 is passed to the constructor

struct ContextIsConstexpr2 {
    size_t _s;

    constexpr ContextIsConstexpr2(size_t s) : _s(1) {
        if(!s) {
            throw logic_error("s is zero");
        }
    }
};

// Accept one of the above. By using a CONVERSION constructor
// and passing in a size_t parameter, it DOES make a difference.

ContextIsConstexpr1 foo(ContextIsConstexpr1 c) { return c; }
ContextIsConstexpr2 bar(ContextIsConstexpr2 c) { return c; }

Now the test code:

int main()
{
    constexpr size_t CONST = 1;
    #define TEST_OBVIOUS_ONES false
    
    // ------------------------------------------------------------
    // Test 1: result is compile-time, param is compile-time
    // ------------------------------------------------------------

    #if TEST_OBVIOUS_ONES
    
    // Compile-time link error iif s==0 w/ any optimization (duh)
    constexpr auto test1_1 = ContextIsConstexpr1(CONST);
    cout << test1_1._s << endl;

    // Compile-time throw iif s==0 w/ any optimization (duh)
    constexpr auto test1_2 = ContextIsConstexpr2(CONST);
    cout << test1_2._s << endl;

    #endif

    // ------------------------------------------------------------
    // Test 2: result is runtime, param is compile-time
    // ------------------------------------------------------------

    // Compile-time link error iif s==0 w/ any optimization ***See below***
    auto test2_1 = ContextIsConstexpr1(CONST);
    cout << test2_1._s << endl;

    // Runtime throw iif s==0 w/ any optimization
    // NOTE: Throw behavior is different than extern symbol behavior!!
    auto test2_2 = ContextIsConstexpr2(CONST);
    cout << test2_2._s << endl;

    // ------------------------------------------------------------
    // Test 3: Implicit conversion
    // ------------------------------------------------------------

    // Compile-time link error if (1) s==0 w/ any optimization *OR* (2) s>0 w/ low optimization!!
    // Note: New s>0 error due to implicit conversion ***See above***
    auto test3_1 = foo(CONST);
    cout << test3_1._s << endl;

    // Runtime throw iif s==0 w/ any optimization
    auto test3_2 = bar(CONST);
    cout << test3_2._s << endl;

    // ------------------------------------------------------------
    // Test 4: result is ignored, param is compile-time
    // ------------------------------------------------------------

    // Compile-time link error w/ any 's' iif low optimization
    // Note: no error w/ s==0 with high optimization, new error w/ s>0 by ignoring result ***See above***
    ContextIsConstexpr1{CONST};

    // Runtime throw iif s==0 w/ any optimization
    ContextIsConstexpr2{CONST};

    // ------------------------------------------------------------
    // Get runtime input, can't optimize this for-sure
    // ------------------------------------------------------------

    #if TEST_OBVIOUS_ONES

    size_t runtime;
    cout << "Enter a value: ";
    cin >> runtime;

    // ------------------------------------------------------------
    // Test 5: result is runtime, param is runtime
    // ------------------------------------------------------------

    // Compile-time link error w/ any 's' w/ any optimization (duh)
    auto test5_1 = ContextIsConstexpr1(runtime);
    cout << test5_1._s << endl;

    // Runtime throw iif s==0 w/ any optimization (duh)
    auto test5_2 = ContextIsConstexpr2(runtime);
    cout << test5_2._s << endl;

    // ------------------------------------------------------------
    // Test 6: result is ignored, param is runtime
    // ------------------------------------------------------------

    // Compile-time link error w/ any 's' w/ any optimization (duh)
    ContextIsConstexpr1{runtime};

    // Runtime throw iif s==0 w/ any 's' w/ any optimization (duh)
    ContextIsConstexpr2{runtime};

    #endif
}

Solution

  • Does anyone know of a way I can change my code to get the conversion constructor to be called at compile-time if it can be

    As I said in linked posted, call of constexpr functions at compile time is done only in constant expression.

    Parameters are not constexpr.

    One workaround would be to use MACRO:

    #define APPLY_DISPATCHER(dispatcher, str, ...) \
        do { \
            constexpr callable_type_t<decltype(dispatcher),  decltype(make_tuple(__VA_ARGS__))> callable(str); \
            (dispatcher)(callable, __VA_ARGS__); \
        } while (0)
    

    with

    template <typename Dispatcher, typename Tuple> struct callable_type;
    
    template <typename Dispatcher, typename ... Ts>
    struct callable_type<Dispatcher, std::tuple<Ts...>>
    {
        using type = typename Dispatcher::template Callable<Ts...>;
    };
    
    template <typename Dispatcher, typename Tuple> 
    using callable_type_t = typename callable_type<Dispatcher, Tuple>::type;
    

    With usage:

    APPLY_DISPATCHER(dispatcher, "one", 1);
    APPLY_DISPATCHER(dispatcher, "a", 1); // Fail at compile time as expected
    

    Demo.

    But not really better than proposed dispatcher.dispatch(MAKE_CHAR_SEQ("a"), 1); (or with extension dispatcher.dispatch("a"_cs, 1);) (providing dispatch overload to be able to create constexpr Callable).