12
\$\begingroup\$

I made a simple progress bar with percentage counter for console applications in C++. Below is my code:

#include <iostream>
#include <string>
#include <thread>
#include <chrono>
#include <cmath>

void show_progress_bar(int time, const std::string &message, char symbol)
{
    std::string progress_bar;
    const double progress_level = 1.42;

    std::cout << message << "\n\n";

    for (double percentage = 0; percentage <= 100; percentage += progress_level)
    {
        progress_bar.insert(0, 1, symbol);
        std::cout << "\r [" << std::ceil(percentage) << '%' << "] " << progress_bar;
        std::this_thread::sleep_for(std::chrono::milliseconds(time));       
    }
    std::cout << "\n\n";
}

Some explanation about my code. I've used three parameters in order to make a header called "progress_bar.h", so whenever I need to use in my programs I can change the time of the display of the progress bar, the message used, for example "Loading...", "Generating report...", etc, and also the symbol. Sometimes I've used a simple '*', other times I've used the character with ASCII code 254 (it's a black square). So, basically my idea was to have a general progress bar in order to be used whenever I need, and in different ways.

On the other hand, the variable progress_level has the value given above in order to prevent the progress bar for jumping to the next line (which was an ugly thing). That made to change the variable percentage to a double type. To be able to print integer values I've used the ceil function.

My questions:

  1. How good is in general my progress bar? What can be changed in order to make look my code cleaner?

  2. Is there a simpler (better) way of using strings in order to make a progress bar like the above (i.e. showing a percentage counter)? (I know of other ways to make progress bars, but I'm mostly interested in ones using strings).

\$\endgroup\$
3
  • 3
    \$\begingroup\$ How is the caller supposed to know what value of time to use to get the right rate? \$\endgroup\$ Commented Feb 1, 2018 at 18:23
  • \$\begingroup\$ @200_success hmm, yeah that's something I didn't think of. \$\endgroup\$
    – Xam
    Commented Feb 1, 2018 at 19:28
  • 4
    \$\begingroup\$ Please don't use 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. Use std::floor. \$\endgroup\$ Commented Feb 1, 2018 at 22:19

2 Answers 2

16
\$\begingroup\$

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.

\$\endgroup\$
0
2
\$\begingroup\$

A further suggestion to "An alternative approach":

Since the destructor also adds a new line to the screen, if you call two progress bars in the same function they will appear in the reverse order on your screen

For example:

int main()
{
    progress_bar progress1{std::clog, 70u, "Progress bar 1"};

    for (auto i = 0.0;  i <= 100;  i += 3.65) {
        progress1.write(i/100.0);
        // simulate some work
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }

    progress_bar progress2{std::clog, 70u, "Progress bar 2"};

    for (auto i = 0.0;  i <= 100;  i += 3.65) {
        progress2.write(i/100.0);
        // simulate some work
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

That will display:

Progress bar 2 ............................................ [100%]
Progress bar 1 ............................................ [100%]

because of the order the local objects are deleted.

Instead I suggest you create the objects with dynamic allocations and then delete them:

int main()
{

    progress_bar* progress1 = new progress_bar(std::clog, 70u, "Progress bar 1"); 

    for (auto i = 0.0;  i <= 100;  i += 3.65) {
        progress1->write(i/100.0);
        // simulate some work
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
    delete progress1;

    progress_bar* progress2 = new progress_bar(std::clog, 70u, "Progress bar 2"); 

    for (auto i = 0.0;  i <= 100;  i += 3.65) {
        progress2.write(i/100.0);
        // simulate some work
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
    delete progress2;
}

This will display:

Progress bar 1 ............................................ [100%]
Progress bar 2 ............................................ [100%]
\$\endgroup\$
1
  • 3
    \$\begingroup\$ Or just use a separate scope for each local progress_bar. \$\endgroup\$ Commented Jan 30, 2019 at 11:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.