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.