I'm experimenting with new C++ constructs.
In C++ callbacks and templates , I developed a generic object oriented callback handler using templates.
In modern C++ modules I tried out an example that uses C++ modules.
This post brings those two together. I convert my callback design (a single .h file) into a module (a single .cpp file).
You need a recent compiler, and tell it that you use modern C++ constructs.
In this blog I use GCC14, and give it these parameters to enable modules:
-std=c++26 -fmodules-ts
The traditional header file approach
The original design, using header file looked like this:
#ifndef CALLBACKMANAGER_H_ #define CALLBACKMANAGER_H_ template <typename R, typename... Args> requires std::is_void<R>::value || std::is_arithmetic_v<R> class Callback { public: Callback() : callback_(nullptr){} inline void set(std::function<R(Args... args)> callback) { callback_ = callback; } // ... }; #endif /* CALLBACKMANAGER_H_ */
and was used like this (an example that uses a class method as callback. The design can also call back functions, static methods, lambdas and other mechanisms) :
#include "callbackmanager.h" class MyClass { public: inline int handler(const int& num1, const int& num2) const { return num1 + num2; } }; int main() { int a = 4; int b = 5; MyClass myClass; Callback<int, const int&, const int&> cb; // Use a lambda to capture myClass and call the object method cb.set([&myClass](const int& num1, const int& num2) -> int { return myClass.handler(num1, num2); }); int o = cb.call(a, b); printf("Value: %i\n", o); fflush(stdout); // ...
What you notice here:
- the callback functionality is in a header file
- the header file has the usual include guards at start and end (#ifndef ...)
- the client file includes the header file
The module approach
The callback class now sits in a .cpp source file:
module; export module callbackmanager; export template <typename R, typename... Args> requires std::is_void<R>::value || std::is_arithmetic_v<R> class Callback { public: Callback() : callback_(nullptr){} inline void set(std::function<R(Args... args)> callback) { callback_ = callback; } // ... };
and the file that uses it, imports the module instead of including the header.
import callbackmanager; class MyClass { public: inline int handler(const int& num1, const int& num2) const { return num1 + num2; } }; int main() { int a = 4; int b = 5; MyClass myClass; Callback<int, const int&, const int&> cb; // Use a lambda to capture myClass and call the object method cb.set([&myClass](const int& num1, const int& num2) -> int { return myClass.handler(num1, num2); }); int o = cb.call(a, b); printf("Value: %i\n", o); fflush(stdout); // ...
Differences:
- the callback design sits in a source file. It creates the module, and exports the accessible module functionality.
- no include guards anymore. They are not needed
- the client file imports the module
That's it.
Full source
callbackmanager.cpp
/* * callbackmanager.cpp * * Created on: 16 jun. 2024 * Author: jancu */ module; #include <functional> export module callbackmanager; namespace callbackmanager { export template <typename R, typename... Args> requires std::is_void<R>::value || std::is_arithmetic_v<R> class Callback { using callbackfunction_t = std::function<R(Args...)>; public: Callback() : callback_(nullptr), isSet(false){} inline void set(callbackfunction_t callback) { callback_ = callback; isSet = true; } inline void unset() { callback_ = nullptr; isSet = false; } /* * R can either be an arithmetic type, or void */ inline R call(Args... args) { if constexpr (std::is_void<R>::value) { if (!isSet) { return; } (callback_)(args...); } if constexpr (! std::is_void<R>::value) { if (!isSet) { return 0; // R can only be a arithmetic type. 0 should work as default. } return (callback_)(args...); } } inline bool is_set() { return isSet; } private: callbackfunction_t callback_; bool isSet; }; } // namespace callbackmanager
callback_example.cpp (showing a number of examples that use the callback as OO or functional mechanism)
// https://community.element14.com/products/devtools/software/f/forum/54719/c-callbacks-and-templates #include <cstdio> #include <string> #include <typeinfo> import callbackmanager; class MyClass { public: inline int handler(const int& num1, const int& num2) const { return num1 + num2; } static inline int staticHandler(const int& num1, const int& num2) { return num1 + num2; } }; int functionHandler(const int& num1, const int& num2) { return num1 + num2; } int main() { int a = 4; int b = 5; { // scenario: call object method MyClass myClass; callbackmanager::Callback<int, const int&, const int&> cb; // Use a lambda to capture myClass and call the object method cb.set([&myClass](const int& num1, const int& num2) -> int { return myClass.handler(num1, num2); }); int o = cb.call(a, b); printf("Value: %i\n", o); fflush(stdout); } { // scenario: call static method callbackmanager::Callback<int, const int&, const int&> cb; // Use a lambda to call the static method cb.set([](const int& num1, const int& num2) -> int { return MyClass::staticHandler(num1, num2); }); int o = cb.call(a, b); printf("Value: %i\n", o); fflush(stdout); } { // scenario: call C function callbackmanager::Callback<int, const int&, const int&> cb; // Use a lambda to call the classic C function cb.set([](const int& num1, const int& num2) -> int { return functionHandler(num1, num2); }); int o = cb.call(a, b); printf("Value: %i\n", o); fflush(stdout); } { // scenario: call pure lambda callbackmanager::Callback<int, const int&, const int&> cb; // Use a lambda to execute anonymous C code cb.set([](const int& num1, const int& num2) -> int { return num1 + num2; }); int o = cb.call(a, b); printf("Value: %i\n", o); fflush(stdout); } { // scenario: return a bool callbackmanager::Callback<bool, const int&, const int&> cb; // Use a lambda to execute anonymous C code cb.set([](const int& num1, const int& num2) -> bool { return num1 == num2; }); printf("Value: %s\n", cb.call(a, b) ? "true" : "false"); fflush(stdout); } { // scenario: use void callbackmanager::Callback<void, const int&, const int&> cb; // Use a lambda to execute anonymous C code cb.set([](const int& num1, const int& num2) { printf("void gets num1: %i, num2: %i\n", num1, num2); fflush(stdout); return; }); cb.call(a, b); } { // scenario: use void, and no attributes callbackmanager::Callback<void> cb; // Use a lambda to execute anonymous C code cb.set([]() { printf("void with no parameters\n"); fflush(stdout); return; }); cb.call(); } { // scenario: use void, and a const std::string reference callbackmanager::Callback<void, const std::string&> cb; // Use a lambda to execute anonymous C code cb.set([](const std::string& s) { printf(s.c_str()); fflush(stdout); return; }); cb.call("hello, world!\r\n"); } { // scenario: use void, and a const std::string reference std::string updateme; callbackmanager::Callback<void, std::string&> cb; // Use a lambda to execute anonymous C code cb.set([](std::string& s) { s.assign("hello, world!\r\n"); return; }); cb.call(updateme); printf(updateme.c_str()); fflush(stdout); } /* { // scenario: use an unsupported (non-fundamental) type for return value R // this will generate a compile error callbackmanager::Callback<std::string, const int&, const int&> cb; } */ }
cmake file
cmake_minimum_required(VERSION 3.28) project(callbackmanager C CXX ASM) # GoogleTest requires at least C++14 set(CMAKE_CXX_STANDARD 26) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fmodules-ts -fcommon") # set(CMAKE_EXE_LINKER_FLAGS "-Wl,-allow-multiple-definition") include(FetchContent) FetchContent_Declare( googletest URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip ) # For Windows: Prevent overriding the parent project's compiler/linker settings set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) FetchContent_MakeAvailable(googletest) enable_testing() add_executable(${CMAKE_PROJECT_NAME}_example) target_sources(${CMAKE_PROJECT_NAME}_example PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/example/callback_examples.cpp ) target_sources(${CMAKE_PROJECT_NAME}_example PUBLIC FILE_SET cxx_modules TYPE CXX_MODULES FILES ${CMAKE_CURRENT_SOURCE_DIR}/callbackmanager.cpp ) target_link_libraries( ${CMAKE_PROJECT_NAME}_example ) add_executable(${CMAKE_PROJECT_NAME}_test) target_sources(${CMAKE_PROJECT_NAME}_test PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/test/callbackmanager_test.cpp ) target_sources(${CMAKE_PROJECT_NAME}_test PUBLIC FILE_SET cxx_modules TYPE CXX_MODULES FILES ${CMAKE_CURRENT_SOURCE_DIR}/callbackmanager.cpp ) target_link_libraries( ${CMAKE_PROJECT_NAME}_test GTest::gtest_main GTest::gmock_main ) include(GoogleTest) gtest_discover_tests(${CMAKE_PROJECT_NAME}_test)
Thank you for reading.