A modern, header-only C++17 library for running registered tasks in separate threads, with grouping, cooperative cancellation, stop timeouts, and a small callback/event API.
- Header-only core: copy
core.hor link the exported CMake interface target. - No required Qt dependency in core: public API uses standard C++ types.
- Type-safe registration: register free functions, lambdas, functors, non-const member functions, and const member functions.
- Task grouping: only one task per group runs at a time, while tasks from different groups can run concurrently.
- Cooperative cancellation: tasks can check
stopTaskFlag()and exit gracefully. - Event callbacks: observe started, finished, cancelled-before-start, stop-requested, and stop-timeout events.
- Configurable logging: redirect core debug and warning messages with a standard C++ callback.
- Optional Qt adapter: build a separate QObject/signal bridge without adding Qt to
core.h. - C++17 payloads: task arguments and results are exposed as
std::vector<std::any>andstd::any.
CoreTemplateStd requires a C++17 compiler. No Qt package is required to build or consume the core target.
Copy core.h into your project, or use CMake from this repository:
add_subdirectory(CoreTemplateStd)
target_link_libraries(your_target PRIVATE CoreTemplateStd::CoreTemplateStd)Use directly from GitHub with FetchContent:
include(FetchContent)
FetchContent_Declare(
CoreTemplateStd
GIT_REPOSITORY https://github.com/valeksan/CoreTemplateStd.git
GIT_TAG v0.4.0
)
FetchContent_MakeAvailable(CoreTemplateStd)
target_link_libraries(your_target PRIVATE CoreTemplateStd::CoreTemplateStd)Install and consume via find_package:
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/your/prefix
cmake --build build
cmake --install buildfind_package(CoreTemplateStd REQUIRED)
target_link_libraries(your_target PRIVATE CoreTemplateStd::CoreTemplateStd)Installed package consumers can include the header explicitly:
#include <CoreTemplateStd/core.h>The old CMake package name remains available as a compatibility layer:
find_package(CoreTemplate REQUIRED)
target_link_libraries(your_target PRIVATE CoreTemplate::CoreTemplate)The core target stays Qt-free. If a Qt application wants signal/slot integration, enable the separate adapter target from the source tree:
set(CORETEMPLATE_BUILD_QT_ADAPTER ON)
add_subdirectory(CoreTemplateStd)
target_link_libraries(your_qt_target PRIVATE
CoreTemplateStd::QtAdapter
)Installed packages expose the adapter as an optional component:
find_package(CoreTemplateStd REQUIRED COMPONENTS QtAdapter)
target_link_libraries(your_qt_target PRIVATE CoreTemplateStd::QtAdapter)CoreQtAdapter owns a std-only Core, exposes it through core(), and re-emits core callbacks as Qt signals with QVariantList/QVariant payloads. The adapter supports common scalar and string payload conversions; unsupported std::any payloads become invalid QVariant values. It uses Core::setWakeCallback with queued Qt delivery and one-shot timers, so the GUI example does not need a polling timer.
The Qt Widgets GUI example is also optional:
cmake -S . -B build/qt_gui \
-DCORETEMPLATE_BUILD_EXAMPLE=OFF \
-DCORETEMPLATE_BUILD_QT_GUI_EXAMPLE=ON
cmake --build build/qt_gui --target ExampleQtGuiAppCoreTemplateStd is a breaking std-only branch of the original Qt-oriented API:
- Qt signals such as
finishedTaskare replaced by callback setters such asonFinished. QVariantandQVariantListpayloads are replaced bystd::anyandstd::vector<std::any>.- Qt event-loop delivery is replaced by explicit
processEvents()calls from the managing thread. QObjectownership is removed; constructCoredirectly without a parent object.- Force termination no longer kills worker threads in the
std::threadbackend; non-cooperative tasks receive stop requests and timeout events. - The old CMake names remain as compatibility aliases, but new code should prefer
CoreTemplateStdandCoreTemplateStd::CoreTemplateStd.
#include <CoreTemplateStd/core.h>
#include <any>
#include <chrono>
#include <iostream>
#include <thread>
int main()
{
Core core;
bool finished = false;
core.registerTask(1, [](int a, int b) -> int {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
return a + b;
});
core.onFinished([&](const FinishedEvent& event) {
std::cout << "Result: " << std::any_cast<int>(event.result) << '\n';
finished = true;
});
core.addTask(1, 10, 20);
while (!finished) {
core.processEvents();
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}See example/console_main.cpp for a minimal runnable example.
The complete API is defined in core.h. The primary methods are:
registerTask: registers a function, lambda, functor, or member function by task type.registerTaskWithContext: registers a task that receives an explicitTaskContextas its first argument.addTask: queues a registered task with arguments.unregisterTask: removes a registered task type.onStarted,onFinished,onCancelledBeforeStart,onStopRequested,onStopTimedOut: set one callback per event kind.setLogHandler,clearLogHandler: configure or reset the global std-only core log sink.setWakeCallback,clearWakeCallback: configure or reset an owner-loop wake hook for adapters and integrations.processEvents: delivers queued owner-thread events and should be called by the managing thread.cancelTaskById,cancelTaskByType,cancelTaskByGroup,cancelTasks,cancelAllTasks,cancelTasksByGroup: request cooperative cancellation.stopTaskById,stopTaskByType,stopTaskByGroup,stopTasks,stopAllTasks,stopTasksByGroup: compatibility names for the cancellation API.isTaskRegistered,groupByTask,isIdle,isTaskAddedByType,isTaskAddedByGroup: query task state.stopTaskFlag: returns the thread-local stop flag for the currently running task.requestStopAll: requests cooperative cancellation for active work and removes queued work.waitForIdle(timeout): processes owner events while waiting for active workers; it does not cancel them.shutdown(timeout): callsrequestStopAll, waits up totimeout, and returns the queued-cancellation count and IDs of workers still running.
Core is designed for one managing thread.
- Create and use a
Coreinstance from one thread. - Call public methods such as
registerTask,addTask, cancellation, and query methods from that same managing thread. - Registered task functions run in worker threads managed by the library.
- Worker completion is queued back to the managing side; call
processEvents()regularly to deliver callbacks and start queued follow-up tasks. - Code running inside a task should not directly call public
Coremethods. Use your application-level message passing to communicate back to the managing thread.
Cancellation is cooperative. A long-running task should periodically check:
if (auto* stop = core.stopTaskFlag(); stop != nullptr && stop->load()) {
return;
}The portable std::thread backend has no safe standard way to kill a running thread. A
non-cooperative task receives a stop request and then a stop-timeout event, but keeps running
until its callable returns. A timeout is a diagnostic, not successful termination.
Task implementations are responsible for checking the stop flag, putting time limits on
blocking operations, and returning before Core is destroyed. The destructor joins active
workers to protect Core state, so violating this contract with an infinite non-cooperative
task can block shutdown. Code that must be forcibly stopped should run in a separate process.
For new code, prefer an explicit context rather than capturing Core solely to read its
thread-local flag:
core.registerTaskWithContext(1, [](TaskContext context, int value) -> int {
while (value-- > 0) {
if (context.stopRequested()) {
return -1;
}
// Process one short unit of work.
}
return 0;
});TaskContext only reports a request; it does not preempt the callable. Before destroying a
Core, call shutdown(timeout) and keep objects captured by running tasks alive until its
result is idle (or until a later waitForIdle succeeds).
#include <CoreTemplateStd/core.h>
#include <any>
#include <chrono>
#include <iostream>
#include <thread>
int main()
{
Core core;
int finishedCount = 0;
auto work = [](int value) -> int {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
return value * 10;
};
core.registerTask(1, work, 1);
core.registerTask(2, work, 1);
core.registerTask(3, work, 2);
core.onFinished([&](const FinishedEvent& event) {
std::cout << "Task " << event.type
<< " result " << std::any_cast<int>(event.result) << '\n';
++finishedCount;
});
core.addTask(1, 10);
core.addTask(2, 20);
core.addTask(3, 30);
while (finishedCount < 3) {
core.processEvents();
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}Tasks 1 and 2 share group 1, so they run sequentially. Task 3 belongs to group 2, so it can run concurrently with group 1.
cmake -S . -B build/std_only_check -DCORETEMPLATE_BUILD_TESTS=ON -DCORETEMPLATE_BUILD_EXAMPLE=ON
cmake --build build/std_only_check
ctest --test-dir build/std_only_check --output-on-failure
./build/std_only_check/example/ExampleConsoleApp- The core is header-only and implemented in
core.h. - Core debug and warning messages go to
std::clogandstd::cerrby default, or to the handler configured withCore::setLogHandler. TaskArgsisstd::vector<std::any>.TaskResultisstd::any; avoidtask produces an emptystd::any.std::any_cast<T>is the caller's responsibility when reading callback payloads.- The current backend uses
std::thread; non-cooperative tasks cannot be forcibly killed by standard C++.
If you find this library helpful and wish to support its development, feel free to use the Sponsor button. Any support is voluntary and entirely optional. The library remains free and open-source.