3
\$\begingroup\$

I tried to make a callback type for embedded, I know std::function and lambda but I know that there is some dynamic memory allocation that I want to avoid, I came to this implementation:

#pragma once
namespace interfaces
{
    template <class returnType, class ... parameterType>
    class ICallable
   {
       public:
       virtual returnType execute(parameterType ... args) = 0;
   };
}

static callback :

#pragma once

#include "ICallable.hpp"

template <class ReturnType, class ... ParameterType>
class StaticCallback : public interfaces::ICallable <ReturnType,ParameterType...>
{
   public:
      typedef ReturnType (*CallbackFunction)(ParameterType ...);

      constexpr StaticCallback(CallbackFunction function) : m_function(function)
      {
      }

   virtual ReturnType execute(ParameterType ... args) override final
   {
       return m_function(args...);
   }


 private:
    CallbackFunction m_function;
};

and callback with an object

#pragma once
#include "volund/interfaces/system/ICallable.hpp"

 template <class objectType,class returnType,class ... parameterType>
 class ObjectCallback : public interfaces::ICallable<returnType, parameterType ...>
 {
    public:
      typedef returnType (objectType::*Callbackfunction)(parameterType ... args);
      constexpr ObjectCallback(objectType& self, Callbackfunction function) : m_object(&self), m_functionToCall(function)
      {
      }
      constexpr ObjectCallback(objectType *self, Callbackfunction function) : m_object(self), m_functionToCall(function)
      {
      }
      virtual returnType execute(parameterType ... params) override final
      {
          return (*m_object.*m_functionToCall)(params...);
      }

    private:
       objectType* m_object;
       Callbackfunction m_functionToCall;
  };

I'm not sure about the reason of dynamic allocation in std::function, so I'm wondering if there is no pitfall I didn't see in my solution. If someone have any tips or advice that would be great =)

EDIT :

This is how i use it, typically from a driver that trigger an interrupt and user code register a callback to define some code to be executed on interrupt :

class driver
{
    public:
      void setCallback(ICallable* callback)
      {
         m_callback = callback;
      }

      driver& get()
      {
         return s_self;
      }

     private:
       static s_self;

       Icallable* m_callback;

       static void interrupt()
       {
          s_self.m_callback->execute();
       }
  }


  class user
  {
      public:
        void task()
        {
            driver::get().setCallback(&m_callFromDriver);
            while(true)
            {
               //do something
            }
        }
       private:
         ObjectCallback m_callFromDriver = ObjectCallback(this, &user::onEvent);


        void onEvent()
        {
            //do something
        }
  }
\$\endgroup\$
1

2 Answers 2

2
\$\begingroup\$

lambda's don't alloc, std::function only allocates memory if the object it is wrapping is 'too large'. The VC++ 2019 std::function impl defines 'too large' as being more than 7 * sizeof(void*) bytes in size. Other impl may use a different limit. This is big enough to handle many (most ?) cases w/o allocating.

You could make your own function class, based on std::function, and put static_assert's in the constructors and assignment op's to ensure none of your code that uses your function would trigger an alloc; or if it would you could increase the 'too large' size of your function class. This would give you something as flexible as std::function, and the control over alloc that you want.

[EDIT]

If you don't want to use the VC++ version, the gcc one is at:

https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/std_function.h

To start you can copy the file and put static_assert's before lines 160 and 237 to catch any cases that would require an alloc (likely also have to check dependencies in other gcc .h files).

... it looks like this version doesn't reserve much space i.e. would alloc in many cases. You'd have to make a couple other tweaks to increase the no_copy size.

\$\endgroup\$
5
  • \$\begingroup\$ He needs an implementation for embedded - so MSVC's version is kinda irrelevant. Who knows what kind of implementation of STL or compiler he has. It might work similarly to what you say but if he wants more control over it then it makes sense to implement it. \$\endgroup\$
    – ALX23z
    Dec 18, 2019 at 21:22
  • \$\begingroup\$ There isn't anything platform specific about the VC++ std::function impl, he can copy it and tweak as I mentioned and it will work with any standards compliant compiler. \$\endgroup\$ Dec 19, 2019 at 0:58
  • \$\begingroup\$ I prefer to avoid the modification of std files. But if i understand what you say std::function use only dynamic allocation on few moments for huge objects, instead of modifying the file maybe i should overrride new function to trigger a stop, so i can know when std::function needs allocation ? \$\endgroup\$ Dec 20, 2019 at 14:33
  • \$\begingroup\$ I wasn't suggesting modifying the std file, I was suggesting copying it to use as a base for a custom function impl that does what is desired. Overriding new is an option, but you will likely catch a lot of other unwanted alloc's ... unless this is the only place that is potentially doing alloc's. \$\endgroup\$ Dec 20, 2019 at 15:54
  • \$\begingroup\$ Good answer! Also relevant: stackoverflow.com/questions/46163607/… \$\endgroup\$
    – Edward
    Dec 21, 2019 at 14:09
1
\$\begingroup\$

std::function requires dynamic allocation due to its type erasure properties - plus how else to make support for std::bind?

1) The main issue with the design is that there is no way to change input types between Callable and the function. Normally, you'd want to pass data in the execute function by reference and then into the function according to its declaration. In your implementation inability to do so results in extra data copying. I don't know how to fix it, or if it is at all possible without making it overly complicated. You can fix it partially by utilizing the std::forward which will result in usage of move operations instead of copy.

//instead of
return m_function(args...);
//write
return m_function(std::forward<parameterType>(args)...);

2) Technical issues:

virtual returnType execute(parameterType ... params) override final;

It is completely meaningless to write virtual ... override final. It is not an error but it is clutter and it is embarrassing - shows that you aren't confident in usage of these keywords. Write either virtual, override, or final. Just one of them, not 2 and definitely not 3. So the execute function should look like:

returnType execute(parameterType ... params) final;

3) template class ICallable<...> must have a virtual destruction just as all interface classes.

template <class returnType, class ... parameterType>
class ICallable
{
   public:
   virtual ~ICallable() = default;
   virtual returnType execute(parameterType ... args) = 0;
};

Otherwise it is almost impossible to deallocate an ICallable object if it was dynamically allocated and only a pointer to ICallable exists. It might not be an issue since you work on embedded and try to avoid dynamic allocations but still you should keep this in mind as it limits amount of usage the class has.

4) Don't use typedef. I had to make extra effort to find definition of Callbackfunction. Instead utilize keyword using as it is much easier to read and comprehend.

// instead of
typedef returnType (*Callbackfunction)(parameterType ... args);
typedef returnType (objectType::*Callbackfunction)(parameterType ... args);
// write
using CallbackFunction = returnType (*)(ParameterType ...);
using CallbackFunction = returnType (objectType::*)(ParameterType ...);

5) Initialize member pointers to nullptr otherwise you can't tell whether they were initialized or not. E.G.,

CallbackFunction m_function = nullptr;

Imagine, you'll need to initiate StaticCallback empty and let somebody else fill it? Also how do you check if it was filled? Add virtual function that checks if it was initiated to the interface and subsequently make implementations in StaticCallback and ObjectCallback.

6) Also, consider making version of ObjectCallback that stores the object inside. Also it is advisable to make an ObjectCallback version that calls only the operator ().

7) Make a template helper function that generates either StaticCallback or ObjectCallback - whatever version needed depending on the parameters you pass. Though, it requires to mess with the SFINAE to make it.

\$\endgroup\$
1
  • \$\begingroup\$ Thanks for the tips ! to answer to number 6, i don't want to use a copy of the object because i use huge objects in tha case (for an embeededsystem) since it store a task and i have a object to notify the task inside so i need the real object not a copy \$\endgroup\$ Dec 20, 2019 at 14:26

Your Answer

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

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