Last Update:
Activity Indicators - Example of a Modern C++ Library
Table of Contents
In this blog post, we’ll show you how to write a library that displays a task activity indication for multithreading applications. Have a look at several essential Modern C++ techniques and how to combine them to write good code.
Let’s dive in!
This article is a guest post from Pranav Srinivas Kumar:
Pranav has 5+ years of industry experience in C++, focusing on safety-critical embedded software. He works at Permobil, researching on drive-assist technology for powered wheelchairs. Pranav frequently publishes hobby projects on GitHub.
Introduction
Progress bars and loading spinners are a valuable part of my daily life as a software engineer. Installing libraries with apt or pip? Training a neural network with TensorFlow? Copying files to an external hard drive? They’re everywhere. How long should I wait before I CTRL-C this process? Is it even doing anything?
We need activity indicators. They keep us engaged - we are more likely to finish tasks when there is a clear indication of progress. Here’s a page from Apple’s Human Interface Guidelines wiki regarding progress indicators.
Inspired by tqdm and indicatif, I’ve recently published a library called indicators that provides configurable activity indicators for use in C++ command-line applications. In this post, I’ll present a minimal implementation of two indicators: ProgressBar
and MultiProgress
. ProgressBar
will provide an interface to model and manage a single, thread-safe progress bar. MultiProgress
will provide an interface to manage multiple progress bars simultaneously.
Although the indicators library supports C++11 and higher, we will assume C++17 support for this post.
Designing a Progress bar
Let’s set some expectations. Our progress bar must be:
- Thread-safe - we’re able to update the progress bar state from multiple threads
- Informative - we’re able to provide useful stats, e.g., percentage completed, time elapsed etc.
- Configurable - we’re able to set the bar width, color and style
Let’s assume progress is measured as a float in [0.0f, 100.0f]
with the semantics: 25.0f
implies 25%
completed. We can provide an .set_progress(float)
method that users can use to update the progress bar state.
#include <atomic>
#include <mutex>
#include <iostream>
class ProgressBar {
public:
void set_progress(float value) {
std::unique_lock lock{mutex_}; // CTAD (C++17)
progress_ = value;
}
private:
std::mutex mutex_;
float progress_{0.0f};
};
Layout
Now, let’s focus on the layout. We want a progress bar that (1) spans a specific width, (2) progresses from left to right, (3) shows percentage completion, and (4) maybe shows some status text. Here’s a minimalist design:
[{...completed} {remaining...}] {percentage} {status_text}
◄-------- Bar Width --------►
Example:
[■■■■■■■■■■■■■■■■■■■■■■-------] 71% Extracting Archive
Below are some setters that users can use to configure our progress bar. Note the use of std::unique_lock
- we want to be able to change properties like status_text
from multiple threads based on application state.
public:
// [...]
void set_bar_width(size_t width) {
std::unique_lock lock{mutex_};
bar_width_ = width;
}
void fill_bar_progress_with(const std::string& chars) {
std::unique_lock lock{mutex_};
fill_ = chars;
}
void fill_bar_remainder_with(const std::string& chars) {
std::unique_lock lock{mutex_};
remainder_ = chars;
}
void set_status_text(const std::string& status) {
std::unique_lock lock{mutex_};
status_text_ = status;
}
private:
// [...]
size_t bar_width_{60};
std::string fill_{"#"}, remainder_{" "}, status_text_{""};
If the width of our bar is 60 characters, then the completed portion of our bar should span 60 * progress_ / 100
characters. We can use this logic in a .write_progress()
to write our bar to a stream, e.g., console.
Let’s add an .update
method that sets the progress and immediately prints the bar to the stream.
public:
// [...]
void update(float value, std::ostream &os = std::cout) {
set_progress(value);
write_progress(os);
}
void write_progress(std::ostream &os = std::cout) {
std::unique_lock lock{mutex_};
// No need to write once progress is 100%
if (progress_ > 100.0f) return;
// Move cursor to the first position on the same line and flush
os << "\r" << std::flush;
// Start bar
os << "[";
const auto completed = static_cast<size_t>(progress_ * static_cast<float>(bar_width_) / 100.0);
for (size_t i = 0; i < bar_width_; ++i) {
if (i <= completed)
os << fill_;
else
os << remainder_;
}
// End bar
os << "]";
// Write progress percentage
os << " " << std::min(static_cast<size_t>(progress_), size_t(100)) << "%";
// Write status text
os << " " << status_text_;
}
We’re choosing to use std::ostream
here so we can use this class for unit testing, mocking, and writing to log files.
Note that use of os << "\r" <<
. We don’t want to print our progress bar in a newline after each change; we want to update the progress bar in-place. So, we use the RETURN
character to go back to the first position on the same line.
Example
Time to test this out. Let’s write a simple program that configures a ProgressBar
object and updates its state. For a little extra bling, I’m going to use the excellent termcolor library.
#include "progress_bar.hpp"
#include "termcolor.hpp"
#include <thread>
int main() {
std::cout << termcolor::bold << termcolor::yellow;
ProgressBar bar;
bar.set_bar_width(50);
bar.fill_bar_progress_with("■");
bar.fill_bar_remainder_with(" ");
for (size_t i = 1; i <= 100; ++i) {
bar.update(i);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
std::cout << termcolor::reset;
}
Great. We have a thread-safe progress bar class that is reasonably configurable. How do we handle more than one? As it stands, if we use more than one progress bar, their stdout will overlap.
Managing Multiple Progress bars
We need a management class that can refer to multiple progress bars and prints them nicely - one bar per line to the console. Something like Docker’s parallel download progress bars:
Here are some design considerations:
- What is the ownership model? Does
MultiProgress
own a collection of progress bars or does it simply refer to them? - Can each progress bar be updated independently in a thread-safe manner?
- How dynamic is this multi-progress bar class? Can one dynamically add and remove progress bars as and when progress is completed?
For simplicity, let’s assume that our MultiProgress
class manages a fixed number of progress bars and this number is known at compile-time, e.g., MultiProgress<3> bars;
Constructing MultiProgress
I like the idea of our MultiProgress
class not owning the progress bars but simply referring to them. This way, we can construct progress bars and use them as is or as part of a multi-progress bar indicator (or both).
So how do we achieve this? Based on the above docker example, we know the MultiProgress
class needs to hold a container, e.g., an array of indicators. We don’t want to store raw pointers to progress bars. We also can’t use a vector of references; the component type of containers like vectors needs to be assignable and references are not assignable.
We can use std::reference_wrapper
instead. reference_wrapper<T>
is a CopyConstructible and Assignable wrapper around a reference to an object of type T
. Instances of std::reference_wrapper<T>
are objects (they can be copied or stored in containers) but they are implicitly convertible to T&
, so that they can be used as arguments with the functions that take the underlying type by reference.
Let’s allow the user to specify the number of progress bars to manage and have the user also provide references to each bar in the constructor:
#include <atomic>
#include <mutex>
#include <functional>
#include <array>
#include <iostream>
template <typename Indicator, size_t count>
class MultiProgress {
public:
template <typename... Indicators,
typename = typename std::enable_if_t<(sizeof...(Indicators) == count)>>
explicit MultiProgress(Indicators &... bars) : bars_({bars...}) {}
private:
std::array<std::reference_wrapper<Indicator>, count> bars_;
};
Note that MultiProgress
takes a template Indicator
. This allows for easily extending this class to support other types of progress indicators, e.g., progress spinners, block progress bars, or other specializations.
Also note that our use of std::reference_wrapper
comes with a hidden assumption - the Indicators referred to by a MultiProgress
object need to outlast the MultiProgress
object itself. Else our bars_
array will be referring to objects that are already destroyed.
Constructing MultiProgress
now looks like below. This object is configured for exactly 3 bars - the constructor will accept exactly 3 arguments and the object will hold references to these bars.
MultiProgress<ProgressBar, 3> bars(bar1, bar2, bar3);
Updating Progress
Our .update
method will simply loop over all the bars we’re managing and call each one’s .set_progress
method.
// multi_progress.hpp
// [...]
public:
template <size_t index>
typename std::enable_if_t<(index >= 0 && index < count), void>
update(float value, std::ostream &os = std::cout) {
bars_[index].get().set_progress(value);
}
Okay, now our code can update the progress of each bar. We aren’t printing anything yet, though.
Printing progress
Let’s work on printing all these bars. We need to iterate over each bar and print its progress. When printing repeatedly, we need to move the cursor up some number of lines (once for each bar) before printing the bars. This is to ensure that we’re printing “in place” - to give the effect the we’re updating that bar. Not doing this will cause the .write_progress
to keep printing in new lines.
public:
template <size_t index>
typename std::enable_if<(index >= 0 && index < count), void>::type
update(float value, std::ostream &os = std::cout) {
// [...]
write_progress(os);
}
void write_progress(std::ostream &os = std::cout) {
std::unique_lock lock{mutex_};
// Move cursor up if needed
if (started_)
for (size_t i = 0; i < count; ++i)
os << "\x1b[A";
// Write each bar
for (auto &bar : bars_) {
bar.get().write_progress();
os << "\n";
}
if (!started_)
started_ = true;
}
private:
// [...]
std::mutex mutex_;
std::atomic<bool> started_{false};
Note that we’re simply reusing code written in the ProgressBar
class - set_progress
and write_progress
.
Example
Time to test this out. Let’s create three progress bars: bar1
, bar2
, and bar3
. Create a MultiProgress
object for managing these bars.
We want to update the state of these bars in different threads and at different rates. In the example below, bar1
is updated every 100 ms, bar2
every 200 ms, and bar3
every 60 ms.
#include "progress_bar.hpp"
#include "multi_progress.hpp"
#include "termcolor.hpp"
#include <thread>
int main() {
std::cout << termcolor::bold << termcolor::green << "\n\n" << std::endl;
ProgressBar bar1, bar2, bar3;
MultiProgress<ProgressBar, 3> bars(bar1, bar2, bar3);
// Job for the first bar
auto job1 = [&bars]() {
for (size_t i = 0; i <= 100; ++i) {
bars.update<0>(i);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
};
// Job for the second bar
auto job2 = [&bars]() {
for (size_t i = 0; i <= 100; ++i) {
bars.update<1>(i);
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
};
// Job for the third bar
auto job3 = [&bars]() {
for (size_t i = 0; i <= 100; ++i) {
bars.update<2>(i);
std::this_thread::sleep_for(std::chrono::milliseconds(60));
}
};
std::thread first_job(job1);
std::thread second_job(job2);
std::thread third_job(job3);
first_job.join();
second_job.join();
third_job.join();
std::cout << termcolor::reset << std::endl;
return 0;
}
As you can imagine, it should be easy from here to add additional style-related properties to the ProgressBar
class, e.g., foreground color, background color, etc. There is plenty of room to get creative.
Conclusions
In this post, we have explored some activity indicators with C++17 - a thread-safe progress bar and a multi-progress indicator. The indicators library provides a few additional classes, e.g., block progress bars and progress spinners, along with a slightly richer interface for presenting stats, e.g., estimated time remaining.
Thanks for reading!
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here: