C + + implementation of function delay binding

Posted by leonel.machava on Thu, 16 Apr 2020 13:57:55 +0200

  • This code needs c++17 support (can be modified to be compatible with c++11)

Summary

Sometimes we do different operations on the same data, such as:

int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
int do_sth(int a, int b, const std::string& function_name) {
	if (function_name == "add") return add(a, b);
	if (function_name == "mul") return mul(a, b);
}

This approach is feasible, but when we need to add multiple functions such as sub(a, b), div(a, b), etc., every time we add a function, we need to add an if in do_sth, which is easy to make mistakes and does not conform to the opening and closing principle.

Another implementation is to encapsulate each operation into a single class, which is then created using the factory pattern. This approach is in line with the principle of opening and closing, but it is too cumbersome to add a class for each function.

Ideally, if you have a language that combines C + +, Java, and python, you should add functions like this:

// Function management class FunctionManager
//     @Register(): register the function into this class
//     getFunction(): returns the registered function
class FunctionManager;

@Register("add") // Register the function add to the FunctionManager. You can find this function through the string "add"
int add(int a, int b) { return a + b; }
@Register("mul") // Register the function mul to the FunctionManager, which can be found through the string "mul"
int mul(int a, int b) { return a * b; }
int do_sth(int a, int b, const std::string& function_name) {
	// Function management class returns a std::function according to function_name
	std::function<int(int, int)> Function = FunctionManager.getFunction(function_name);
	return Function(a, b);
}
  • FunctionManager: classes for managing functions
  • @Register("Add"): adds a function pointer and function signature (a string that uniquely identifies the function) to the FunctionManager
  • FunctionManager.getFunction(): return function pointer according to function signature

Obviously, the current c + + does not support @ Register. We use macros to Register. The final effect of this article is (a complete program is provided at the end):

// xxx.h
int add(int a, int b);
// xxx.cpp
// Register function with variable name (ADD), function signature (ADD), function pointer (ADD)
_REGISTER_FUNCTION(ADD, "ADD", add);
int add(int a, int b) { return a + b; }
// main.cpp
int do_sth(int a, int b, const std::string& func_sig) {
	// Function manager is a single example
	auto p_function_manager = FunctionManager<decltype(add)>::getInstance();
	// Registered functions are returned here
	auto func = p_function_manager->getFunction(func_sig);
	return 
}

Realization

We name the class that manages the function FunctionManager. After analyzing our requirements carefully, it is not difficult to find that we actually need to find the registered function object according to the string.

Implementation of search

There is a STD:: Map < STD:: string, Functiontype > container in STL that can meet our needs, so the search function has been implemented. Note:

  • The function type needs to be provided externally, and the return value and parameters of the function will change, which will affect the function type. We can't hardcode the function type into the program, so we need the template.
  • Because the function pointer cannot be the return value of the function, the interface to get the function can only return std::function, so the stored function in the map should also be std::function.
  • Considering that there is only one function manager in the program, it is more convenient to set the function manager as a single example (which can also meet the requirements of registering functions).
template <typename FunctionType>
class FunctionManager {
	// There is no delete pointer. Considering this is a demo, ignore this problem
	inline static FunctionManager<FunctionType>* p_function_manager = nullptr;

	std::map<std::string, FunctionType> m_sig_func_map;

public:
	static FunctionManager<FunctionType>* get_instance() {
		if (!p_function_manager) p_function_manager = new FunctionManager<FunctionType>;
		return p_function_manager;
	}
	
	// In fact, the operator [] cannot be used, because when sig does not exist in the map, an < SIG, empty > object will be created automatically, which is ignored in demo
	FunctionType get_function(const std::string& sig) { return m_sig_func_map[sig]; }
};

Implementation of registration

Registration is actually to add function signature (string) and function (std::function object) to functionmanager:: m ﹣ sig ﹣ function ﹣ map. Only one interface needs to be added:

	// You can also use insert. There is a little difference between them
	void register_function(const std::string& sig, FunctionType function) { m_sig_func_map[sig] = function; }

Try to use

Now we can use FunctionManager:

int add(int a, int b) { return a + b; }
void use() {
	std::function<int(int, int)> a(add);
	FunctionManager<std::function<decltype(add)>>::get_instance()->register_function("add", a);
	auto another_add = FunctionManager<std::function<decltype(add)>>::get_instance()->get_function("add");
	std::cout << another_add(1, 3) << std::endl;
}

We see that the use of FunctionManager is actually inconvenient:

  • When registering, we need to provide std::function object of corresponding function. In fact, we only want to provide function pointer when we use it
  • You need to get a single instance when registering and getting functions

These miscellaneous codes can be encapsulated by a separate interface:

template <typename FunctionPtr>
void register_function(const std::string& function_sig, FunctionPtr function_ptr) {
	auto function_obj = static_cast<std::function<FunctionPtr>>(function_ptr);
	auto p_function_manager = FunctionManager<decltype(function_obj)>::get_instance();
	p_function_manager->register_function(function_sig, function_obj);
}

template <typename FunctionType>
FunctionType get_function(const std::string& function_sig) {
	auto p_function_manager = FunctionManager<FunctionType>::get_instance();
	return p_function_manager->get_function(function_sig);
}

int main(int argc, char* argv[]) {
	register_function<decltype(add)>("add", add); // Pointer only
	auto another_add = get_function<std::function<int(int, int)>>("add");
	std::cout << another_add(1, 4) << std::endl;
	return 0;
}

It's much more convenient to use. In fact, the implementation of FunctionManager has been completed here, but it's also necessary to make FunctionManager better used, and make FunctionManager applicable to more types of functions. The following is the focus of this article

register

If the existing function manager is used directly, there will be another problem: it needs to be guaranteed by the user to register the function before get function. This is not a big problem in demo, but in a large project, get function is called multiple times in multiple files, and the user needs to ensure that register function is executed before all get functions, which is too dangerous. The safe way is:

void register_all_function() {
	auto p_function_manager = /* get singleton instance */ ;
	p_function_manager->register_function(sig_1, func_1);
	p_function_manager->register_function(sig_2, func_2);
	// ...
}
int main {
	register_all_function();
}

It is the safest way to register at the beginning of main, but every time a function is added, the user must register in register "all" function, which also violates the opening and closing principle.

So we have a new requirement: register the object before the main function is executed. Yes Two methods It can execute a function before main: static member variable and global variable. It should be noted that both of them need to be defined in cpp file, not in header file. Assigning a value to a global variable through register function can execute register function before main, so we need to add a return value to register function:

template <typename FunctionPtr>
bool register_function(const std::string& function_sig, FunctionPtr function_ptr) {
	// ...
	return true;
}

Register functions with global variables

By defining global variables, you can register the functions you need to function manager before main executes. In order to make it easier to use and code readable, FunctionManager provides a macro "register" function to encapsulate the registration function. The principle is as follows:

#define _REGISTER_FUNCTION(FunctionSig, FunctionPtr) \
	bool b = register_function<decltype(FunctionPtr)>(FunctionSig, FunctionPtr);

This brings new problems: in actual use, there are usually multiple functions of the same type in a. cpp file, and "register" function will be called multiple times:

_REGISTER_FUNCTION("ADD", add);
int add(int a, int b) { return a + b; }
_REGISTER_FUNCTION("MUL", mul); // Compile error, variable b defined repeatedly
int mul(int a, int b) { return a * b; }

Calling register function multiple times causes global variable b to be defined repeatedly. Therefore, users need to provide variable names that are not repeated manually to prevent compilation errors. Finally, the implementation of register function is as follows:

#define _REGISTER_FUNCTION(VariableName, FunctionSig, FunctionPtr) \
	bool Bool##VariableName = register_function<decltype(FunctionPtr)>(FunctionSig, FunctionPtr);

// xxx.h
int add(int, int);
// xxx.cpp
_REGISTER_FUNCTION(ADD, "ADD", add);
int add(int a, int b) { return a + b; }

Functions that function manager adapts to

Ordinary function

  • Ordinary function
float add(float a, float b) { return a + b; }
_REGISTER_FUNCTION(ADD, "ADD", add);

auto new_add = get_function<decltype(add)>("ADD");
  • Functions in namespace
namespace FunctionManagerTest {
    float add(float a, float b) { return a + b; }
}
_REGISTER_FUNCTION(ADD, "ADD", FunctionManagerTest::add);

auto new_add = get_function<std::function<decltype(FunctionManagerTest::add)>>("ADD");
  • template function
template<typename T> T addT(T a, T b) { return a + b; }
_REGISTER_FUNCTION(ADD, "ADD", addT<int>);

 auto new_add = get_function<std::function<decltype(addT<int>)>>("ADD");

Class function

  • Static function
class Real {
public:
    static float add(float a, float b) { return a + b; }
};
_REGISTER_FUNCTION(ADD, "ADD", Real::add);

auto new_add = get_function<std::function<decltype(Real::add)>>("ADD");
  • Static function of template class
template<typename T>
class Add {
public:
    static float add(T a, T b) { return a + b; }
};
_REGISTER_FUNCTION(ADD, "ADD", Add<int>::add);

auto new_add = get_function<std::function<decltype(Add<int>::add)>>("ADD");

Adaptive member function

There is a very important class of functions that function manager can support now: member functions. Because a member function will have this pointer as an implicit parameter when it is called, it is obvious that this pointer cannot be obtained directly through & Real:: add. This means that we need to add new interfaces.

template<typename FunctionPtr, typename ObjectPtr>
bool register_member_function(FunctionPtr func_ptr, ObjectPtr obj_ptr) {
	return true;
}

Looking back at the requirements, we hope to be able to call the function directly after it is obtained in FunctionManager; at the same time, what is managed in FunctionManager is only the std::function object that can be called directly, without distinguishing between member function and non member function. Now the problem is simplified to how to provide a default parameter (object pointer) for a function. After that, we can call member functions just like ordinary functions.

We know that std::bind can bind functions to parameters and return a Callable objectThis object can be received with the corresponding STD:: Fun ; in combination with std::placeholder, it can also provide parameters when calling the returned Callable:

class Real { public: int add(int a, int b) { return a + b; } }

Real real;
std::function<int(int,int)> binded_add = std::bind(&Real::add, &real, std::placeholders::_1, std::placeholders::_2);
std::cout << binded_add(2, 1) << std::endl;

Now the problem of how to provide default parameters has been solved, but another problem has arisen again: STD:: function < int (int, int) > in the code is hard coded, and it can't be installed. We need a method that can automatically fill in template parameters in STD:: function < >. After searching the Internet for most of the day, Finally, we have a template based solution:

template <typename Ret, typename Struct, typename ...Args, typename ObjectPtr>
bool register_memeber_function(const std::string& sig, Ret(Struct::* func_ptr)(Args...) const, ObjectPtr obj_ptr) {
	std::function<Ret(Args...)> func = std::bind(func_ptr, obj_ptr);
	auto p_funciton_manager = FunctionManager<decltype(func)>::get_instance();
	p_function_manager->register_function(sig, func);
	return true;
}

class Real { public: int add(int a, int b) { return a + b; } }

Real real;
bool b = register_member_function("ADD", &Real::add, &real);

When & Real:: add is passed in as a parameter, Ret, Struct and variable Args can be automatically derived. Because we will bind object pointers, we only need to return the value Ret, and Args is the template parameter of std::function. In this way, the template parameter problem of std::function is finally solved.

However, the current code cannot be compiled because the correct number of STD:: placeholders has not been added to std::bind. To solve this problem, we need to use a bit of metaprogramming. Someone on StackOverflow Using the method of custom placeholder and STD:: make  integer  sequence to realize std::bind with variable parameters:

// https://stackoverflow.com/questions/26129933/bind-to-function-with-an-unknown-number-of-arguments-in-c
template<int N>
struct my_placeholder { static my_placeholder ph; };

template<int N>
my_placeholder<N> my_placeholder<N>::ph;

namespace std {
    template<int N>
    struct is_placeholder<::my_placeholder<N>> : std::integral_constant<int, N> { };
}

template<class R, class T, class...Types, class U, int... indices>
std::function<R (Types...)> bind_first(std::function<R (T, Types...)> f, U val, std::integer_sequence<int, indices...> /*seq*/) {
    return std::bind(f, val, my_placeholder<indices+1>::ph...);
}
template<class R, class T, class...Types, class U>
std::function<R (Types...)> bind_first(std::function<R (T, Types...)> f, U val) {
    return bind_first(f, val, std::make_integer_sequence<int, sizeof...(Types)>());
}

The core idea here is to pass in an integer sequence with the same length as the number of parameters in the template, and add a placeholder for the integer in each sequence. In fact, you don't need to define a placeholder yourself, because the implementation of std::placeholder is similar:

// PLACEHOLDER ARGUMENTS
namespace placeholders {
    _INLINE_VAR constexpr _Ph<1> _1{};
    _INLINE_VAR constexpr _Ph<2> _2{};
} // namespace placeholders

The solution combining the existing implementation and std::placeholder is as follows:

template <typename Ret, typename Struct, typename ...Args, typename ObjectPtr, int... Indices>
std::function<Ret(Args...)> erase_class_info(Ret(Struct::* func_ptr)(Args...), ObjectPtr obj_ptr, std::integer_sequence<int, Indices...>)
{
	std::function<Ret(Args...)> erased_function = std::bind(func_ptr, obj_ptr, std::_Ph<Indices + 1>{}...);
	return erased_function;
}

template <typename Ret, typename Struct, typename ...Args, typename ObjectPtr>
bool register_memeber_function(const std::string& sig, Ret(Struct::* func_ptr)(Args...), ObjectPtr obj_ptr) {
	std::function<Ret(Args...)> erased_func = erase_class_info(func_ptr, obj_ptr, std::make_integer_sequence<int, sizeof...(Args)>());
	auto p_funciton_manager = FunctionManager<decltype(erased_func)>::get_instance();
	p_funciton_manager->register_function(sig, erased_func);
	return true;
}

In addition, the function types of member functions are different after const is added. Simply adding two similar interfaces can solve this problem:

template <typename Ret, typename Struct, typename ...Args, typename ObjectPtr, int... Indices>
std::function<Ret(Args...)> erase_class_info(Ret(Struct::* func_ptr)(Args...) const, ObjectPtr obj_ptr, std::integer_sequence<int, Indices...>)
{
	std::function<Ret(Args...)> erased_function = std::bind(func_ptr, obj_ptr, std::_Ph<Indices + 1>{}...);
	return erased_function;
}

template <typename Ret, typename Struct, typename ...Args, typename ObjectPtr>
bool register_memeber_function(const std::string& sig, Ret(Struct::* func_ptr)(Args...) const, ObjectPtr obj_ptr) {
	std::function<Ret(Args...)> erased_func = erase_class_info(func_ptr, obj_ptr, std::make_integer_sequence<int, sizeof...(Args)>());
	auto p_funciton_manager = FunctionManager<decltype(erased_func)>::get_instance();
	p_funciton_manager->register_function(sig, erased_func);
	return true;
}

Registration of member functions is now supported:

class Real {
public:
	int add(int a, int b) const { return a + b; }
	int sub(int a, int b) { return a - b; }
};

Real real;
bool b1 = register_memeber_function("real", &Real::add, &real);
bool b2 = register_memeber_function("REAL", &Real::sub, &real);
auto f1 = get_function<std::function<int(int, int)>>("real");
auto f2 = get_function<std::function<int(int, int)>>("REAL");
std::cout << f1(2, 1) << std::endl;
std::cout << f2(2, 1) << std::endl;

Now the function manager implementation is complete

supplement

Simpler interface

As mentioned above, static member variables can also be used to register functions, and static member variables can be used to register functions
It can further reduce the code that users need to write, and functional manager does not want users to provide variable names. Refer to the boost macro, boost? Class? Export, which uses the template class and static member variables to register the class, and does not require the user to provide the variable name. Its principle is as follows:

namespace boost::archive::detail::extra_detail {
    template<> struct init_guid<ClassToRegister> {
        static guid_initializer<ClassToRegister> const& g; //Static member g
    }
    static guid_initializer<ClassToRegister> const& g = register_function(); // Define g and register classes at the same time
}

It is still unable to meet the requirements of boost class export, because boost class export is used to register classes. If it is used to register multiple functions of the same type, the static member g will be repeatedly defined, while the function of the same type will often be registered multiple times when the function is actually registered. Such as:

int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
_BOOST_LIKE_REGISTER("ADD", add);
_BOOST_LIKE_REGISTER("MUL", mul); // Static member g is defined repeatedly

Function manager solves the problem of duplicate definition through namespace. The macro "easy register function" is implemented as follows:

#define _EASY_REGISTER_FUNCTION(FunctionSig, FunctionPtr) \
    namespace VicentChenSpace { \
        namespace Dummy { \
            namespace FunctionPtr { \
                struct Dummy { \
                    static bool const& b; \
                }; \
            } \
        } \
    } \
    bool const& VicentChenSpace::Dummy::FunctionPtr::Dummy::b = register_function<decltype(::FunctionPtr)>(FunctionSig, ::FunctionPtr); \

Although "easy" register "function is more convenient to use, there are the following problems:

  • c++17 support required
  • Registering functions with templates is not supported
  • Namespace related issues

Run demo

#include <functional>
#include <map>
#include <iostream>

#define _REGISTER_FUNCTION(VariableName, FunctionSig, FunctionPtr) \
	bool Bool##VariableName = register_function<decltype(FunctionPtr)>(FunctionSig, FunctionPtr);

template <typename FunctionType>
class FunctionManager {
	// There is no delete pointer. Considering this is a demo, ignore this problem
	inline static FunctionManager<FunctionType>* p_function_manager = nullptr;

	std::map<std::string, FunctionType> m_sig_func_map;

public:
	static FunctionManager<FunctionType>* get_instance() {
		if (!p_function_manager) p_function_manager = new FunctionManager<FunctionType>;
		return p_function_manager;
	}

	// You can also use insert. There is a little difference between them
	void register_function(const std::string& sig, FunctionType function) { m_sig_func_map[sig] = function; }
	
	// In fact, the operator [] cannot be used, because when sig does not exist in the map, an < SIG, empty > object will be created automatically, which is ignored in demo
	FunctionType get_function(const std::string& sig) { return m_sig_func_map[sig]; }
};

template <typename FunctionPtr>
bool register_function(const std::string& function_sig, FunctionPtr function_ptr) {
	auto function_obj = static_cast<std::function<FunctionPtr>>(function_ptr);
	auto p_function_manager = FunctionManager<decltype(function_obj)>::get_instance();
	p_function_manager->register_function(function_sig, function_obj);
	return true;
}

template <typename FunctionType>
FunctionType get_function(const std::string& function_sig) {
	auto p_function_manager = FunctionManager<FunctionType>::get_instance();
	return p_function_manager->get_function(function_sig);
}

template <typename Ret, typename Struct, typename ...Args, typename ObjectPtr, int... Indices>
std::function<Ret(Args...)> erase_class_info(Ret(Struct::* func_ptr)(Args...), ObjectPtr obj_ptr, std::integer_sequence<int, Indices...>)
{
	std::function<Ret(Args...)> erased_function = std::bind(func_ptr, obj_ptr, std::_Ph<Indices + 1>{}...);
	return erased_function;
}

template <typename Ret, typename Struct, typename ...Args, typename ObjectPtr>
bool register_memeber_function(const std::string& sig, Ret(Struct::* func_ptr)(Args...), ObjectPtr obj_ptr) {
	std::function<Ret(Args...)> erased_func = erase_class_info(func_ptr, obj_ptr, std::make_integer_sequence<int, sizeof...(Args)>());
	auto p_funciton_manager = FunctionManager<decltype(erased_func)>::get_instance();
	p_funciton_manager->register_function(sig, erased_func);
	return true;
}

template <typename Ret, typename Struct, typename ...Args, typename ObjectPtr, int... Indices>
std::function<Ret(Args...)> erase_class_info(Ret(Struct::* func_ptr)(Args...) const, ObjectPtr obj_ptr, std::integer_sequence<int, Indices...>)
{
	std::function<Ret(Args...)> erased_function = std::bind(func_ptr, obj_ptr, std::_Ph<Indices + 1>{}...);
	return erased_function;
}

template <typename Ret, typename Struct, typename ...Args, typename ObjectPtr>
bool register_memeber_function(const std::string& sig, Ret(Struct::* func_ptr)(Args...) const, ObjectPtr obj_ptr) {
	std::function<Ret(Args...)> erased_func = erase_class_info(func_ptr, obj_ptr, std::make_integer_sequence<int, sizeof...(Args)>());
	auto p_funciton_manager = FunctionManager<decltype(erased_func)>::get_instance();
	p_funciton_manager->register_function(sig, erased_func);
	return true;
}

// -----Normal function registration ------//
// Ordinary function
int add(int a, int b) { return a + b; }
_REGISTER_FUNCTION(ADD, "ADD", add);
int mul(int a, int b) { return a * b; }
_REGISTER_FUNCTION(MUL, "MUL", mul);

// Functions in namespace
namespace VicentSpace { int add(int a, int b) { return a + b; } }
_REGISTER_FUNCTION(NAMESPACE_ADD, "NAMESPACE_ADD", VicentSpace::add);

// template function
template<typename T> T addT(T a, T b) { return a + b; }
_REGISTER_FUNCTION(TEMPLATE_ADD, "TEMPLATE_ADD", addT<int>);

// -----Function registration within class -----//
class Real {
public:
	int add(int a, int b) const { return a + b; }
	int sub(int a, int b) { return a - b; }
	static int mul(int a, int b) { return a * b; }
};
Real real;

template <typename T>
class RealT {
public:
	T addT(T a, T b) const { return a + b; }
	T subT(T a, T b) { return a - b; }
	static T mulT(T a, T b) { return a * b; }
};
RealT<int> real_t;

// Static function
_REGISTER_FUNCTION(STATIC_MUL, "STATIC_MUL", Real::mul);

// Static template function
_REGISTER_FUNCTION(STATIC_TEMPLATE_MUL, "STATIC_TEMPLATE_MUL", RealT<int>::mulT);

// Member function
bool b1 = register_memeber_function("REAL_ADD", &Real::add, &real);
bool b2 = register_memeber_function("REAL_SUB", &Real::sub, &real);

// Template member
bool b3 = register_memeber_function("REALT_ADD", &RealT<int>::addT, &real_t);
bool b4 = register_memeber_function("REALT_SUB", &RealT<int>::subT, &real_t);

int main(int argc, char* argv[]) {

	// Ordinary function
	auto normal_add = get_function<std::function<decltype(add)>>("ADD");
	auto normal_mul = get_function<std::function<decltype(add)>>("MUL");
	std::cout << "Normal Add 1 + 2 = " << normal_add(1, 2) << std::endl;
	std::cout << "Normal Mul 1 * 2 = " << normal_mul(1, 2) << std::endl;

	// Functions in namespace
	auto namespace_add = get_function<std::function<decltype(VicentSpace::add)>>("NAMESPACE_ADD");
	std::cout << "Namespace Add 1 + 2 = " << namespace_add(1, 2) << std::endl;

	// template function
	auto template_add = get_function<std::function<int(int, int)>>("TEMPLATE_ADD");
	std::cout << "Template Add 1 + 2 = " << template_add(1, 2) << std::endl;

	// Static function
	auto static_mul = get_function<std::function<int(int, int)>>("STATIC_MUL");
	std::cout << "Static Mul 1 * 2 = " << static_mul(1, 2) << std::endl;
	
	// Static template function
	auto static_template_mul = get_function<std::function<int(int, int)>>("STATIC_TEMPLATE_MUL");
	std::cout << "Static Template Mul 1 * 2 = " << static_template_mul(1, 2) << std::endl;
	
	// Member function
	auto real_add = get_function<std::function<int(int, int)>>("REAL_ADD");
	auto real_sub = get_function<std::function<int(int, int)>>("REAL_SUB");
	std::cout << "Member Add 2 + 1 = " << real_add(2, 1) << std::endl;
	std::cout << "Const Member Sub 2 - 1 = " << real_sub(2, 1) << std::endl;

	// Template member
	auto real_t_add = get_function<std::function<int(int, int)>>("REALT_ADD");
	auto real_t_sub = get_function<std::function<int(int, int)>>("REAL_SUB");
	std::cout << "Template Member Add 2 + 1 = " << real_t_add(2, 1) << std::endl;
	std::cout << "Template Const Member Sub 2 - 1 = " << real_t_sub(2, 1) << std::endl;
	
	return 0;
}

Reference resources

  1. StackOverflow - Call a function before main
  2. StackOverflow - std::bind to std::function?
  3. StackOverflow - generic member function pointer as a template parameter
  4. StackOverflow - Bind to function with an unknown number of arguments in C++
  5. cppreference - std::bind

Topics: C++ Java Python