1
\$\begingroup\$

I have recently decided to build an event library for the Arduino/ESP microcontrollers, the idea is simple - if an action happens, it invokes a function. In the code it requires holding a vector of function pointers in each Event object. Since the microcontrollers are limited in RAM, especially Arduino, I thought about ways of memory management and one of the ideas that I think maybe useful is to create function groups - objects that will wrap an array of function pointers (also they will be immutable) and will have an execute method that will run over each function and call it with the parameters needed.

My first attempt looked like this:

template<typename... Args>
class StaticFunctionGroup {
private:
    using function = action<Args...>; // action is a pointer for a function that returns 'void'
public:
    template<uint8_t Size>
    constexpr StaticFunctionGroup(function(functions)[Size]) 
        : m_functions(functions), m_size(Size)
    {
    }

    void operator () (Args... args) {
        for (uint8_t i = 0; i < m_size; i++)
            m_functions[i](args...);
    }

private:
    uint8_t m_size;
    function* m_functions;
};

In main() function it could be used as following:

int main()
{ 
    StaticFunctionGroup<> group({
        f1, f2,f3, f4 // f1-f4 are void functions without parameters
    });
    group();
}

The problem is that there is essentially a memory waste as I see it, because first I use a pointer to an array and I am not storing it in the object, and secondly I have to save the size of the array (for simplicity I used one byte sized number). So on the Arduino board this result in 3 bytes wasted for each such group (1 byte - size, 2 bytes the pointer).

Another change that I thought might be helpful was written like this:

template<uint8_t Size, typename... Args>
class StaticFunctionGroup {
private:
    using function = action<Args...>;
public:
    StaticFunctionGroup(function(&& functions)[Size]) 
    {
        for (uint8_t i = 0; i < Size; i++)
            m_functions[i] = functions[i];
    }

    void operator () (Args... args) {
        for (uint8_t i = 0; i < Size; i++)
            m_functions[i](args...);
    }

private:

    function m_functions[Size];
};
template<uint8_t Size>
class StaticFunctionGroup<Size, void>
{
};

This variation eliminates the 3 wasted bytes but the size of the inner array cannot be deduced. so in main() or in any other place I have to declare the size by hand, this is ok from one side, but since I may need to change the functions in many places or add/remove them, this becomes a serious issue.

int main()
{ 
 
    StaticFunctionGroup<4> group({
        f1, f2,f3, f4
    });
}

The Question is what version is considered better? or none of them are good and there is another option for all this?

\$\endgroup\$
1
  • \$\begingroup\$ Is this code currently implemented and working as intended? That is required for question on code review. \$\endgroup\$
    – pacmaninbw
    Commented Jan 13, 2023 at 13:59

1 Answer 1

1
\$\begingroup\$

Type alias for function pointers

In your code you have:

using function = action<Args...>; // action is a pointer for a function that returns 'void'

Instead of having a template of something and a comment to explain it, you can instead just write:

using function = void (*)(Args...);

Although it might be even better to name it function_pointer.

Use perfect forwarding where appropriate

If you have a bunch of parameters and want to pass them to another function, make sure you take the parameters by universal reference, and pass them on using std::forward:

void operator () (Args&&... args) {
    for (uint8_t i = 0; i < m_size; i++)
        m_functions[i](std::forward<Args>(args)...);
}

Sometimes that even makes sense for the constructor.

Alternatives

Ideally you just use a std::array of function pointers. It gets tricky in C++11 to have it deduce the right type from the constructor though; it's much easier in C++17 where you have class template argument deduction:

template<typename... Ts>
class StaticFunctionGroup {
    using Function = std::common_type_t<Ts...>;
    std::array<Function, sizeof...(Ts)> functions;

public:
    constexpr StaticFunctionGroup(Ts&&... ts):
        functions{std::forward<Ts>(ts)...} {}

    template<typename... Args>
    void operator () (Args&&... args) {
        for (auto& function: functions)
            function(std::forward<Args>(args)...);
    }
};

And you can then write:

StaticFunctionGroup group{f1, f2,f3, f4};
group();

With C++20 you could use concepts to restrict Ts such that they are all the same and are function pointers, and that Args are valid for the type of function pointer, as this would give somewhat better error messages.

Much simpler though would be to create a stand-alone call_group() function that takes any container of function pointers as the first parameter:

template<typename T, typename... Args>
void call_group(const T& group, Args&&... args) {
    for (auto& function: group)
        function(std::forward<Args>(args)...);
}

And then write:

void (*group[])() = {f1, f2, f3, f4}; // but a suitable std::array works as well
call_group(group);

This works in C++11. However, although the class above requires C++17, it might still be perfectly usable on Arduino/ESP microcontrollers, assuming your toolchain has new enough compilers.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.