I wrote a simple main()
function to test this:
int main()
{
show_progress_bar(100, "progress", '#');
}
I immediately noticed that I only saw a couple of updates. This is because the output is not flushed after each update. Whether you see the same effect depends greatly on your output device (I was using Emacs compilation-mode, which might have more buffering than traditional terminals or terminal emulators). This is easily fixed for all users, by using the std::flush
stream manipulator:
std::cout << "\r [" << std::ceil(percentage) << '%' << "] "
<< progress_bar << std::flush;
I fixed a small typo there - you had ceil
instead of std::ceil
. Adrian McCarthy advises that rounding up progress is annoying to users, so it may be better to round down or simply cast to an integer:
std::cout << "\r [" << static_cast<int>(percentage) << '%' << "] "
<< progress_bar << std::flush;
It's probably a good idea to use a manipulator to set a field width for percentage
, so that the ]
doesn't jump position when we reach 10%.
Also the message
is overwritten immediately, and I don't get to see it - perhaps emit a newline after it?
It's not obvious why progress_level
has the value it does - it looks like it's an attempt to fit a particular line length. I would prefer to see this calculated:
static const auto line_length = 70;
static const auto progress_level = 100.0 / line_length;
A really sophisticated version would attempt to find the available width (probably using termcap or curses libraries).
Appending progress characters to the string is usually more efficient than prepending:
progress_bar += symbol;
It's probably better to create a full-length string and just print a substring of it each time - you'll want to use the write()
function prior to C++17 (which introduces string_view
to give substrings without copying).
I'd prefer to be able to pass the output stream as a parameter; most of the time, the standard log stream is more appropriate than the standard output stream.
Improved code
I've made changes to put the message on the same line if it fits (I pass by value, and re-use as the progress bar; this may sometimes save a copy).
#include <chrono>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <string>
#include <thread>
void show_progress_bar(std::ostream& os, int time,
std::string message, char symbol = '*')
{
static const auto bar_length = 70;
// not including the percentage figure and spaces
if (message.length() >= bar_length) {
os << message << '\n';
message.clear();
} else {
message += " ";
}
const auto progress_level = 100.0 / (bar_length - message.length());
std::cout << message;
for (double percentage = 0; percentage <= 100; percentage += progress_level) {
message += symbol;
os << "\r [" << std::setw(3) << static_cast<int>(percentage) << "%] "
<< message << std::flush;
std::this_thread::sleep_for(std::chrono::milliseconds(time));
}
os << "\n\n";
}
int main()
{
show_progress_bar(std::clog, 100, "progress", '#');
}
An alternative approach
Usually, when you need a progress bar, the work you are doing isn't as convenient as a regular sleep. For these tasks, it's convenient to have an object you can either pass (by reference) to the task, or just update between sub-tasks. I knocked one up to show what that looks like:
#include <cmath>
#include <iomanip>
#include <ostream>
#include <string>
class progress_bar
{
static const auto overhead = sizeof " [100%]";
std::ostream& os;
const std::size_t bar_width;
std::string message;
const std::string full_bar;
public:
progress_bar(std::ostream& os, std::size_t line_width,
std::string message_, const char symbol = '.')
: os{os},
bar_width{line_width - overhead},
message{std::move(message_)},
full_bar{std::string(bar_width, symbol) + std::string(bar_width, ' ')}
{
if (message.size()+1 >= bar_width || message.find('\n') != message.npos) {
os << message << '\n';
message.clear();
} else {
message += ' ';
}
write(0.0);
}
// not copyable
progress_bar(const progress_bar&) = delete;
progress_bar& operator=(const progress_bar&) = delete;
~progress_bar()
{
write(1.0);
os << '\n';
}
void write(double fraction);
};
void progress_bar::write(double fraction)
{
// clamp fraction to valid range [0,1]
if (fraction < 0)
fraction = 0;
else if (fraction > 1)
fraction = 1;
auto width = bar_width - message.size();
auto offset = bar_width - static_cast<unsigned>(width * fraction);
os << '\r' << message;
os.write(full_bar.data() + offset, width);
os << " [" << std::setw(3) << static_cast<int>(100*fraction) << "%] " << std::flush;
}
// Test program
#include <chrono>
#include <iostream>
#include <thread>
int main()
{
progress_bar progress{std::clog, 70u, "Working"};
for (auto i = 0.0; i <= 100; i += 3.65) {
progress.write(i/100.0);
// simulate some work
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
If you want to develop this further, you can make it more elaborate (e.g. configurable start and end values, or sub-bars that forward scaled values to their parent bars). I'll leave that as an exercise for the interested reader.
time
to use to get the right rate? \$\endgroup\$std::ceil
or otherwise round up the percentage in a progress bar. A big pet peeve is having to continue waiting while the "progress" indicator shows 100% completion. Usestd::floor
. \$\endgroup\$