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
}