├── Plusifier.hpp ├── README.MD └── test.cpp /Plusifier.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace plusifier { 8 | template 9 | class FunctionWrapper final { 10 | static_assert(sizeof...(F) != 0, "FunctionWrapper should be not empty"); 11 | std::tuple var; 12 | constexpr static inline std::size_t pack_size = sizeof...(F); 13 | 14 | template 15 | struct ReturnType; 16 | 17 | template 18 | struct ReturnType { 19 | using type = R; 20 | }; 21 | 22 | template 23 | constexpr static auto NoOverloadFound() { 24 | return VerificationResult{ pack_size, false }; 25 | } 26 | 27 | struct VerificationResult { 28 | std::size_t function_number = 0; 29 | bool is_invokable = false; 30 | 31 | constexpr operator std::size_t() const { 32 | return function_number; 33 | } 34 | 35 | constexpr operator bool() const { 36 | return is_invokable; 37 | } 38 | }; 39 | 40 | template 41 | constexpr static auto VerifyOverload() { 42 | using function_pointer_signature = std::remove_reference_t(var))>; 43 | 44 | constexpr bool is_invokable = std::is_invocable_v; 45 | 46 | if constexpr (is_invokable) 47 | return VerificationResult{ function_number, true }; 48 | 49 | if constexpr (function_number + 1 < pack_size) 50 | return VerifyOverload(); 51 | else 52 | return NoOverloadFound(); 53 | } 54 | 55 | template 56 | constexpr static auto VerifyOverloadByReturnType() { 57 | using function_pointer_signature = std::remove_reference_t(var))>; 58 | 59 | constexpr bool is_invokable = std::is_invocable_v; 60 | 61 | if constexpr (is_invokable && std::is_same_v::type>) 62 | return VerificationResult{ function_number, true }; 63 | 64 | if constexpr (function_number + 1 < pack_size) 65 | return VerifyOverloadByReturnType(); 66 | else 67 | return NoOverloadFound(); 68 | } 69 | 70 | public: 71 | constexpr FunctionWrapper(F ... functions) : var(functions...) {} 72 | 73 | template 74 | constexpr auto operator()(Args ... args) const { 75 | constexpr auto verification_result = VerifyOverload<0, Args...>(); 76 | if constexpr (!verification_result) 77 | static_assert(NoOverloadFound(), "No suitable overload is found"); 78 | 79 | return std::get(var)(args...); 80 | } 81 | 82 | template 83 | constexpr auto OverloadByReturnType(Args ... args) const { 84 | constexpr auto verification_result = VerifyOverloadByReturnType<0, T, Args...>(); 85 | if constexpr (!verification_result) 86 | static_assert(NoOverloadFound(), "No suitable overload is found"); 87 | 88 | return std::get(var)(args...); 89 | } 90 | }; 91 | 92 | template 93 | class PointerWrapper { 94 | std::unique_ptr pointer; 95 | public: 96 | PointerWrapper(T* ptr = nullptr) : pointer(ptr, D) {} 97 | 98 | template 99 | PointerWrapper(F alloc_fn, Args ... args) : pointer(alloc_fn(args...), D) {} 100 | 101 | template 102 | PointerWrapper(FunctionWrapper alloc_fn, Args ... args) : pointer(alloc_fn.template OverloadByReturnType(args...), D) {} 103 | 104 | operator T* () { 105 | return pointer.get(); 106 | } 107 | 108 | auto operator->() { 109 | return pointer.operator->(); 110 | } 111 | }; 112 | } 113 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # C library plusifier 2 | 3 | A header-only helper library to pack the multiple type-dependent C-style functions into single overload deduced at compile-time. No external libraries are required. 4 | 5 | - [C library plusifier](#c-library-plusifier) 6 | - [Motivation](#motivation) 7 | - [Usage and examples](#usage-and-examples) 8 | - [Function overloading](#function-overloading) 9 | - [Pointer automation](#pointer-automation) 10 | - [Under the hood](#under-the-hood) 11 | - [Internals of the class](#internals-of-the-class) 12 | - [`operator()`](#operator) 13 | - [Function verification](#function-verification) 14 | 15 | ## Motivation 16 | 17 | Many programming languages have the ability to call libraries with the pure C interface. Libraries themselves may be written in various languages, however, it is a de-facto standard for them to have a C interface. 18 | 19 | Due to the lack of function overloading in pure C, library maintainers are required to explicitly specify all of the available types for the function. For example, I'd like to list one of my favourite libraries out there, the Intel Integrated Performance Primitives, [IPP](https://software.intel.com/content/www/us/en/develop/tools/oneapi/components/ipp.html): 20 | 21 | ```cpp 22 | IppStatus ippsMulC_16s_I(Ipp16s val, Ipp16s* pSrcDst, int len); 23 | IppStatus ippsMulC_32f_I(Ipp32f val, Ipp32f* pSrcDst, int len); 24 | IppStatus ippsMulC_64f_I(Ipp64f val, Ipp64f* pSrcDst, int len); 25 | IppStatus ippsMulC_32fc_I(Ipp32fc val, Ipp32fc* pSrcDst, int len); 26 | IppStatus ippsMulC_64fc_I(Ipp64fc val, Ipp64fc* pSrcDst, int len); 27 | // ... and so on 28 | ``` 29 | 30 | If you're a C++ developer like myself, you may find this mildly irritating to look up and change the function every single time you decide to change the type. And it works poorly with generic (templated) code as well. 31 | 32 | ## Usage and examples 33 | 34 | Wrapper object is created in the constructor and then the correct overload is selected in the `operator()` call: 35 | 36 | ```cpp 37 | auto fn = plusifier::FunctionWrapper(/*function overloads*/); 38 | 39 | auto dst = fn(/* function arguments... */); 40 | ``` 41 | 42 | Pointer wrapper object is used similarally: 43 | 44 | ```cpp 45 | auto ptr = plusifier::PointerWrapper(allocator_function, /* allocator function arguments... */); 46 | ``` 47 | 48 | Where `allocator_function` may be both the callable (function pointer, lambda, `std::function`) as well as the `plusifier::FunctionWrapper`. 49 | 50 | ### Function overloading 51 | 52 | For a more simplified example, suppose we have three functions with a slightly different signature: 53 | 54 | ```cpp 55 | int square_s8(const std::int8_t* val, int sz) { 56 | return 1; 57 | } 58 | int square_s32(const std::int32_t* val, int sz) { 59 | return 4; 60 | } 61 | int square_fp32(const float* val) { 62 | return 8; 63 | } 64 | ``` 65 | 66 | With this library, they may be packed into single object: 67 | 68 | ```cpp 69 | auto square = plusifier::FunctionWrapper(square_s8, square_s32, square_fp32); 70 | 71 | auto dst_ch = square(arr_ch.data(), 0); // <-- calls square_s8 72 | auto dst_int = square(arr_int.data(), 0); // <-- calls square_s32 73 | auto dst_fp32 = square(arr_fp32.data()); // <-- calls square_fp32 74 | ``` 75 | 76 | It will check if the passed arguments are viable to be used as the arguments for the functions at the compile-time and select the most appropriate overload. 77 | 78 | ### Pointer automation 79 | 80 | RAII is the lifesaver in modern C++. However, it's a bit tedious to mix it with the C-style allocations. One of the approaches would be to use the `std::unique_ptr` with a custom deleter, but it's quite excess, so I decided to expand this library a little bit more. 81 | 82 | For example, we might have a specified allocation functions for various types: 83 | 84 | ```cpp 85 | Ipp8u* ippsMalloc_8u(int len); 86 | Ipp16u* ippsMalloc_16u(int len); 87 | Ipp32u* ippsMalloc_32u(int len); 88 | Ipp8s* ippsMalloc_8s(int len); 89 | Ipp16s* ippsMalloc_16s(int len); 90 | Ipp32s* ippsMalloc_32s(int len); 91 | Ipp64s* ippsMalloc_64s(int len); 92 | Ipp32f* ippsMalloc_32f(int len); 93 | Ipp64f* ippsMalloc_64f(int len); 94 | // and so on... 95 | ``` 96 | 97 | We'll wrap all of them into single `FunctionWrapper` and pass it to the `PointerWrapper`: 98 | 99 | ```cpp 100 | auto ippsMalloc = plusifier::FunctionWrapper(ippsMalloc_8u, ippsMalloc_16u, ippsMalloc_32u, /* etc */); 101 | 102 | auto ptr = plusifier::PointerWrapper(ippsMalloc, size); 103 | ``` 104 | 105 | ## Under the hood 106 | ### Internals of the class 107 | 108 | `FunctionWrapper` is a variadic template class with the types being the function pointers: 109 | 110 | ```cpp 111 | template 112 | class FunctionWrapper final { 113 | static_assert(sizeof...(F) != 0, "FunctionWrapper should be not empty"); 114 | std::tuple var; 115 | constexpr static inline std::size_t pack_size = sizeof...(F); 116 | }; 117 | ``` 118 | 119 | First `static_assert` is used to create a legit compile-time error when there are no functions passed. `std::tuple` is a heterogeneous container to store those function pointers, and a `pack_size` is a simple helper constant. 120 | 121 | Due to the fact, that there are no references and move semantics in pure C, I've decided to omit the [perfect forwarding](https://eli.thegreenplace.net/2014/perfect-forwarding-and-universal-references-in-c) and pass the parameter pack in the constructor as-is, so the constructor is extremely trivial: 122 | 123 | ```cpp 124 | FunctionWrapper(F ... functions) : var(functions...) {} 125 | ``` 126 | 127 | Then there is a function call operator (`operator()`), overload search and verification routines and small helper functions and classes. 128 | 129 | ### `operator()` 130 | 131 | Function call operator may be split into two parts: compile-time and run-time. First is used to select the correct overload or to indicate the lack of one, while the runtime calls the selected function. 132 | 133 | ```cpp 134 | template 135 | auto operator()(Args ... args) const { 136 | // compile-time 137 | constexpr auto verification_result = VerifyOverload<0, Args...>(); 138 | if constexpr (!verification_result) 139 | static_assert(NoOverloadFound(), "No suitable overload is found"); 140 | 141 | // run-time 142 | return std::get(var)(args...); 143 | } 144 | ``` 145 | 146 | Here the `verification_result` variable is an object of a simple helper struct with two fields and conversion operators. In the first place, I wanted to use a structured binding, but the compiler told me I'm not supposed to. This struct contains index of the function inside the tuple and the fact that the correct overload has been found. This flag ended up there due to the recursive nature of the used template metaprogramming approach. 147 | 148 | Verification starts at index 0 and iterates up to the end of the tuple. 149 | 150 | ### Function verification 151 | Every iteration, I get the function pointer signature from the tuple, as well as the `std::function` signature to ease the following metaprogramming. Then there's an excellent function `std::is_invocable_v` in the standard library, that allows me to check if the function pointer in the tuple may be called with the type pack passed to the `operator()`. If we're good, we would prematurely quit the function, otherwise we'll continue iterating, until the very end of the tuple. 152 | 153 | If there's no suitable overload, a function with a failing `static_assert` is called for a better error diagnostics. -------------------------------------------------------------------------------- /test.cpp: -------------------------------------------------------------------------------- 1 | #include "Plusifier.hpp" 2 | 3 | #include 4 | #include 5 | 6 | namespace { 7 | constexpr int square_s8(const std::int8_t* val, int sz) { 8 | return 1; 9 | } 10 | constexpr int square_s32(const std::int32_t* val, int sz) { 11 | return 4; 12 | } 13 | constexpr int square_fp32(const float* val) { 14 | return 8; 15 | } 16 | 17 | std::int8_t* malloc_s8(int sz) { 18 | return reinterpret_cast(0); 19 | } 20 | std::int32_t* malloc_s32(int sz) { 21 | return reinterpret_cast(0); 22 | } 23 | 24 | void free_univ(void* val) {} 25 | } 26 | 27 | void test() { 28 | const std::array arr_ch{}; 29 | const std::array arr_int{}; 30 | const std::array arr_fp32{}; 31 | 32 | constexpr auto square = plusifier::FunctionWrapper(square_s8, square_s32, square_fp32); 33 | constexpr auto dst_ch = square(arr_ch.data(), 0); 34 | constexpr auto dst_int = square(arr_int.data(), 0); 35 | constexpr auto dst_fp32 = square(arr_fp32.data()); 36 | 37 | static_assert(dst_ch == square_s8(arr_ch.data(), 1)); 38 | static_assert(dst_int == square_s32(arr_int.data(), 1)); 39 | static_assert(dst_fp32 == square_fp32(arr_fp32.data())); 40 | } 41 | --------------------------------------------------------------------------------