In modern software development, efficiently handling I/O operations, compute-intensive tasks, or event-driven logic is crucial. Traditional asynchronous programming models, such as callback functions, often lead to the notorious “Callback Hell,” making the code difficult to read, maintain, and debug. C++11/14 introduced <span>std::future</span> and <span>std::promise</span>, providing basic support for asynchronous operations, but their chained calls are clumsy and prone to blocking.
Continuable is an open-source, header-only C++14 library designed to address these issues, offering an elegant, powerful, and non-intrusive asynchronous programming paradigm. Its design is inspired by JavaScript’s Promise and C#’s <span>async/await</span>, while fully leveraging modern C++ features such as expression templates and automatic type deduction, resulting in exceptional performance and expressiveness.
Core Design Concepts and Features
-
Chained Asynchronous Operations The core of Continuable is the “Continuable” object, which represents an asynchronous computation that has not yet completed and its eventual result (or exception). You can chain multiple asynchronous operations together using the
<span>.then()</span>method, forming a clear task pipeline. -
Encapsulation Based on Callbacks and Promises The library internally uses a callback mechanism but elegantly encapsulates it through the concept of “Promise.” You do not need to deal directly with complex callback registrations; instead, you can append subsequent operations using
<span>.then()</span>, transforming the code structure from “nested” to “flat.” -
Seamless Error Propagation Similar to C++’s exception mechanism, any exception in the Continuable chain will be automatically caught and propagated down the chain until it is caught by a
<span>.fail()</span>handler. This eliminates the hassle of manually checking error states at each step in traditional callbacks. -
Exceptional Performance By utilizing C++14 features such as expression templates and rvalue references, Continuable generates highly optimized code at compile time, avoiding unnecessary copies and dynamic memory allocations, resulting in minimal performance overhead.
-
Rich Combinators It provides combinators like
<span>when_all(...)</span>and<span>when_any(...)</span>, which allow you to easily wait for all asynchronous tasks to complete or any one of them to complete, greatly simplifying concurrent control logic.
Comparison with Traditional <span>std::future</span>
| Feature | <span>std::future</span> |
<span>Continuable</span> |
|---|---|---|
| Chained Calls | Clumsy, requires <span>.get()</span> followed by manual creation of a new future |
Smooth, directly uses <span>.then()</span> for seamless connection |
| Composability | Weak, requires manual management of multiple futures | Strong, built-in <span>when_all</span>, <span>when_any</span>, and other combinators |
| Error Handling | Passed via <span>std::exception_ptr</span>, inconvenient to use |
Concentrated handling via <span>.fail()</span>, similar to exception mechanism |
| Performance | May block threads, copy overhead | No blocking, highly optimized, move semantics |
| C++ Standard | C++11 | C++14 |
Code Example
The following example demonstrates how to use the Continuable library.
First, ensure your project includes the Continuable header file. It is a header-only library that can typically be installed via package managers (like vcpkg, conan) or downloaded directly from the GitHub repository.
#include <cti/continuable.hpp>
#include <iostream>
#include <thread>
#include <chrono>
using namespace cti; // Note: The actual library namespace may be `cti` or `continuable`
using namespace std::chrono_literals;
// Simulate an asynchronous HTTP request that returns a string
continuable<std::string> async_http_request(const std::string& url) {
// Return a Continuable object that will eventually set the value via promise
return make_continuable<std::string>([url](auto promise) {
// Simulate asynchronous work in a new thread
std::thread([promise = std::move(promise), url]() mutable {
std::cout << "Starting request to: " << url << " (on thread: "
<< std::this_thread::get_id() << ")\n";
// Simulate network delay
std::this_thread::sleep_for(2s);
// Simulate successful data return
if (url == "https://api.example.com/data") {
promise.set_value("{ \"key\": \"value\" }");
} else {
// Simulate failure, set exception
try {
throw std::runtime_error("404 Not Found");
} catch (...) {
promise.set_exception(std::current_exception());
}
}
}).detach(); // Detach thread, in real projects use a thread pool
});
}
// Simulate an asynchronous data processing function
continuable<int> async_data_processor(std::string data) {
return make_continuable<int>([data = std::move(data)](auto promise) {
std::thread([promise = std::move(promise), data]() mutable {
std::cout << "Processing data: " << data << " (on thread: "
<< std::this_thread::get_id() << ")\n";
std::this_thread::sleep_for(1s);
// The result of processing is the length of the data (just an example)
promise.set_value(data.size());
}).detach();
});
}
int main() {
std::cout << "Main thread: " << std::this_thread::get_id() << std::endl;
// Start an asynchronous chain
async_http_request("https://api.example.com/data")
// First then: receive the string result of the HTTP request and initiate data processing
.then([](std::string response) {
std::cout << "HTTP request completed. Response: "
<< response << " (on thread: "
<< std::this_thread::get_id() << ")\n";
// Return a new Continuable (async_data_processor)
return async_data_processor(std::move(response));
})
// Second then: receive the integer result after data processing
.then([](int processed_result) {
std::cout << "Data processing completed. Result: "
<< processed_result << " (on thread: "
<< std::this_thread::get_id() << ")\n";
return processed_result * 2; // Can return a normal value, automatically wrapped as a ready Continuable
})
// Error handling: catch any exceptions thrown in the chain
.fail([](std::exception_ptr eptr) {
try {
std::rethrow_exception(eptr);
} catch (const std::exception& e) {
std::cerr << "Async chain failed: " << e.what() << std::endl;
}
// Optionally return a default value or rethrow
return -1;
})
// Final callback: executed regardless of success or failure (optional)
.finally([]() {
std::cout << "Async operation is done (success or failure)." << std::endl;
});
// The main thread can continue doing other work...
// Since asynchronous operations run in the background thread, we need to wait for them to complete.
// This is just for simplicity in the example; real applications may have an event loop.
std::cout << "Main thread is free to do other work...\n";
std::this_thread::sleep_for(5s); // Wait for the async chain to complete
return 0;
}
Key Code Analysis
-
Creating a Continuable:
<span>make_continuable<T></span>is a factory function used to create a Continuable object. It takes a lambda that receives a<span>promise</span>object. After the asynchronous operation is complete, you call<span>promise.set_value()</span>or<span>promise.set_exception()</span>to set the final result or exception. -
Chained Calls:
<span>.then()</span>is the core method. It takes a callback function whose parameter is the result of the previous step. The callback can return:
- A value (like
<span>return 42;</span>), which will be automatically wrapped into an immediately completed Continuable. - Another Continuable object (like
<span>return async_data_processor(...);</span>), thus achieving a true asynchronous chain.
Error Handling: The <span>.fail()</span> method is specifically used to catch exceptions. Any exception thrown anywhere in the chain will skip subsequent <span>.then()</span> calls and be passed to the nearest <span>.fail()</span> handler.
Final Handling: The <span>.finally()</span> method is similar to a destructor, executing regardless of success or failure, suitable for performing some cleanup logic.
Asynchronicity: The example uses <span>std::thread</span> to simulate asynchronous work. In real projects, you might integrate it with networking libraries like asio or existing thread pools. Continuable only cares about the callbacks for the start and end of tasks, not how they are implemented.
Combinator Example: <span>when_all</span>
// Assume there are multiple asynchronous tasks
auto task1 = async_http_request("https://api.example.com/users/1");
auto task2 = async_http_request("https://api.example.com/users/2");
auto task3 = async_http_request("https://api.example.com/users/3");
// Wait for all tasks to complete
when_all(task1, task2, task3)
.then([](std::tuple<std::string, std::string, std::string> results) {
// results is a tuple containing the results of all tasks
auto& [user1, user2, user3] = results;
std::cout << "All requests done!\n";
std::cout << "User1: " << user1 << "\n";
// ... process all results
})
.fail([](std::exception_ptr e) {
// If any task fails, it will enter here
std::cerr << "One of the requests failed!" << std::endl;
});
Conclusion
The Continuable library greatly enhances the experience and efficiency of C++ developers in handling complex asynchronous logic by introducing a modern, smooth asynchronous programming model. It addresses many pain points of <span>std::future</span>, providing clear chained calls, automatic error propagation, and powerful task combinability. Although C++20 introduced <span>coroutines</span> as a more advanced asynchronous programming primitive, Continuable, as a C++14-based library, remains a highly valuable and powerful tool in projects that do not support the new standard or as a transitional solution for understanding asynchronous concepts. For C++ developers seeking high-performance, highly readable asynchronous code, Continuable is undoubtedly a tool worth exploring and utilizing.