dimanche 4 juin 2023

How to consistently wrap many functions with diverse names and argument lists with consistent C++ overloads?

I want to create macros which allow me to define wrappers for many functions all at once. For example:

#define WRAPPER2(Type, cxxfunction, ret_type, cfunction, ...) \
    ret_type cxxfunction(__VA_ARGS__) { return cfunction(__VA_ARGS__); }

#define WRAPPER(cxxfunction, ret_type, cfunction, ...) \
    WRAPPER2(uint8_t, cxxfunction, void, cfunction ##_u8, __VA_ARGS__) \
    WRAPPER2(uint16_t, cxxfunction, void, cfunction ##_u16, __VA_ARGS__) \
    WRAPPER2(uint32_t, cxxfunction, void, cfunction ##_u32, __VA_ARGS__)

void c_example_u8(uint8_t*, size_t);
void c_example_u16(uint16_t*, size_t);
void c_example_u32(uint32_t*, size_t);

void c_example2_u8(uint8_t*, uint8_t const*, size_t);
void c_example2_u16(uint16_t*, uint16_t const*, size_t);
void c_example2_u32(uint32_t*, uint32_t const*, size_t);

WRAPPER(example, void, c_example, Type*, size_t);
WRAPPER(example2, void, c_example2, Type*, Type*, size_t);
// Sometimes wrappers will even use `template<Type>` arguments, too!

(as an aside, the trivial cfunction##suffix identifier composition is actually much more complicated)

As written above it does not work for two reasons:

  1. __VA_ARGS__ needs to expand to different things based on where it's used -- once with types and argument names, and once with just the argument names.
  2. Type used within __VA_ARGS__ needs to be replaced with the first parameter of WRAPPER2.

A bit of preprocessor magic can solve problem 1:

#define WRAPPER2(Type, cxxfunction, ret_type, cfunction, ...) \
    ret_type cxxfunction(ARG_PARAMS(__VA_ARGS__)) { return cfunction(ARG_NAMES(__VA_ARGS__)); }

Unfortunately references to Type are not replaced with the actual type of each instance. So to solve problem 2 how about a template?

#define WRAPPER2(Type, cxxfunction, ret_type, cfunction, ...) \
    template <Type> \
    ret_type cxxfunction(ARG_PARAMS(__VA_ARGS__)) { return cfunction(ARG_NAMES(__VA_ARGS__)); }

Nope. This makes all the implementations use the same instance of cfunction, which is supposed to vary by type. So we'll have to specialise:

#define WRAPPER2(type_, cxxfunction, ret_type, cfunction, ...) \
    template <Type> \
    ret_type cxxfunction(ARG_PARAMS(__VA_ARGS__)); \
    template<> ret_type cxxfunction<type_>(ARG_PARAMS(__VA_ARGS__)); { return cfunction(ARG_NAMES(__VA_ARGS__)); }

Well, not that because we still have the word Type used in the argument list of the specialisation, in a context where it no longer refers to a template argument.

How about a different approach?

#define WRAPPER2(type_, cxxfunction, ret_type, cfunction, ...) \
    template <Type> using cfunction##_t = ret_type(__VA_ARGS__); \
    cfunction_t<type_>* const cxxfunction = [](auto... args) { return cfunction(args...); }

Unfortunately this introduces a new problem that I can't create overloads of the same function. Also it needs C++14, not C++11 (curiously, even C++14 blocks use of constexpr in the above form, but it's allowed in C++17).

How about a generic variadic template wrapper? Unfortunately the things I found suffered from type inference troubles when passing, eg., an int for a uint8_t argument. In general, if the template infers arguments from the way it's called, these aren't necessarily aligned with the argument list of the function which needs to be called. And in many approaches, being close enough for a regular function is not close enough when it's several layers of template wrapper.

One case of needing to be precise about the declared function argument list is when a caller tries to pass a class with an implicit type conversion, but the type cannot be deduced from a variadic template argument.

Writing all this out feels like I've almost rubber-ducked my way to an answer by combining a couple of things I haven't tried together yet, but I'm so tired at this stage. I need help.

Aucun commentaire:

Enregistrer un commentaire