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
}
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
).