I have a C application which has a large amount of highly fragmented configuration data and behavior that I would like to consolidate, eg:
// In one file
#define VAR_1 1
#define VAR_2 2
#define VAR_3 3
// In another file
const gNames[] = {
[VAR_1] = (const u8[]) "1",
[VAR_2] = (const u8[]) "2",
[VAR_3] = (const u8[]) "3",
};
// In yet another file
switch (var) {
case VAR_1:
// some behavior
break;
case VAR_3:
// some other behavior
break;
// And so on
I’d like to consolidate this data, eg:
const Behavior gBehaviors[] = {
[VAR_1] = {
.name = (const u8[]) "1",
.handleFooEvent = Var1FooHandler,
// ...
},
// ...
However as far as I can tell the inability to declare anonymous methods in C will make this pretty miserable as I wont be able to keep handlers and constants grouped together. I’m not particularly experienced with C++, but as far as I can tell there are a number of ways in which I can cleanly declare the individual behavior classes I need, however as C++ doesn’t seem to have support for designated identifiers and I need to maintain indexing that matches with our existing #define
enum I’m not sure how I can actually create a mapping that allows the C code to go from an enum to the appropriate behavior. Is there a reasonable way that I could make this work, either in C, C++, or even some other language/technology?
6
You can do this in the way you ask. C++ does have anonoymous functions They are called lambdas. If you want to have a set of lambdas that are dispatched to based on an integer key you can do this. Whether you should do this is another matter. Standard C++ classes should probably be your first port of call. However with that warning:
https://godbolt.org/z/fTbsGcE96
#include <iostream>
#include <utility>
#include <type_traits>
template <typename... Ts>
struct Overloaded : Ts... {
using Ts::operator()...;
};
template <int X, typename Callable, typename... Args>
void call(Callable&& callable, Args&&... args) {
std::forward<Callable>(callable)(std::integral_constant<int, X>{}, std::forward<Args>(args)...);
}
#define VAR_1 1
#define VAR_2 5
#define VAR_3 9
int main() {
auto fn = Overloaded{
// Specialization of lambda for each compile-time value
[](std::integral_constant<int, VAR_1>, int a, int b) {
std::cout << "a + b = " << (a + b) << "n";
},
[](std::integral_constant<int, VAR_2>, int a, int b) {
std::cout << "a - b = " << (a - b) << "n";
},
[](std::integral_constant<int, VAR_3>, int a, int b) {
std::cout << "a * b = " << (a * b) << "n";
}
};
call<VAR_1>(fn, 5, 3);
call<VAR_2>(fn, 5, 3);
call<VAR_3>(fn, 5, 3);
}
If we break this down. Overloaded is a pattern for turning an list of anonymous functions (known as lambdas in c++) into a single function like object that when itself called matches the type of it’s argument and dispatches to the matching sub lambda.
You can find more info on how this works here
To allow us to dispatch to different handlers based on integer we can use a generic type called std::integral_constant<type, val> which generates a unique type based on the input value. We simply add a tag parameter as the first parameter to each lambda with the integral constant type we wish to use to use to mark that lambda.
The call function simply wraps this up in a convenient interface.
4
So as pointed out in some comments, C++ 20 does support designated initializers and instead I was running into an issue with the Intellisense on VSCode’s C++ extension not supporting them with default settings on Windows (regardless of how the code actually gets built). So overall that should be sufficient to enforce ordering and allow C interop.
1