1
\$\begingroup\$

I have written this class which displays a progress bar reading from the file. In what possible ways could I have gone wrong with this definition? And can somebody suggest what wrongs I am committing here?

I construct the object of this class with an input file that I intend to read. The constructor calculates the file size and afterward, I call the method progress() for the display.

class progress_bar
{
        private:
                ifstream* pif;
                T _max_size;

        public:
                progress_bar(ifstream& rif)
                        {
                                pif=&rif;
                                rif.seekg(0,ios_base::end);
                                _max_size=rif.tellg();
                                rif.seekg(0,ios_base::beg);
                        }

                T know_max_length() const {return _max_size;}

                void progress () const
                        {
                                T curr_length=pif->tellg();
                                auto prog = static_cast<double>(curr_length) / _max_size;
                                int max(100);
                                int cur(ceil(prog*max));
                                cout << fixed << setprecision(2) << "\r   [" << string(cur, '-') << '>' << string(max  - cur, ' ') << "] " << 100 * prog << "%";
                                if (prog == 1.0) cout << endl;
                                else cout.flush();
                        }
                ~progress_bar()
                {
                        cout<<endl;
                }



};

This is working fine I am just trying to make the class more generic and bug-free.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Incorporating advice from an answer into the question violates the question-and-answer nature of this site. You could post improved code as a new question, as an answer, or as a link to an external site - as described in I improved my code based on the reviews. What next?. I have rolled back the edit, so the answers make sense again. \$\endgroup\$ Commented Apr 5, 2023 at 8:54

1 Answer 1

6
\$\begingroup\$

There's a lot of missing definitions, and an assumption that all the std identifiers are also in the global namespace. You shouldn't assume that the user has already included <cmath>, <fstream>, <iomanip>, <iostream> and <string>, nor that they have using namespace std; (which is generally considered quite unwise).

Why are we storing a pointer in pif rather than a reference? We always assume it's not null, and never reassign, so a std::ifstream& seems much more appropriate.

Why are we restricting the interface to accept only file streams? That makes it much harder to unit-test.

What's T? Since we assign it from tellg(), it appears we need it to be a std::basic_istream::pos_type. But given that this type is implementation-defined, we can't safely assume that it's an arithmetic type, so really we want to be using std::streamoff along with the implicit conversion from the pos_type.

The constructor that accepts std::ifstream ought to be declared explicit to stop it taking part in default conversions.

We take no account of seekg() failing (if we pass a non-seekable stream, for example). When that happens, we leave the stream in a failed state, potentially harming its other users. And when we measure file size, we should return the read pointer to its original position, which isn't necessarily the beginning of the stream. It's probably worth writing a separate function to measure the size.

When we produce output, we ought to be writing to std::clog rather than mixing with the program's output stream.

Output doesn't behave well once the read position exceeds the measured file size (which can happen when the file is appended to whilst we're reading it).

Constructing two new strings each time we output is likely inefficient. Better to use a view taken from a single string.

When we reach the end of file, we write a newline every time progress() is called, and again when we destroy the progress_bar. I think we only want the latter.


Modified code

#include <algorithm>
#include <cmath>
#include <istream>
#include <iomanip>
#include <iostream>
#include <string>
#include <string_view>

class progress_bar
{
private:
    std::istream& input;
    double const total;         // 0 if not measurable

public:
    explicit progress_bar(std::istream& input)
        : input{input},
          total{stream_length(input)}
    {
    }

    auto know_max_length() const
    {
        return total;
    }

    void progress() const
    {
        static auto const bar_width = 100u;
        static auto const bar_string_source =
            std::string(bar_width, '-') + ">" + std::string(bar_width, ' ');
        static const std::string_view bar_string = bar_string_source;

        if (total) {
            // we can calculate percent complete
            auto const position = static_cast<double>(input.tellg());
            auto const fraction = std::min(1.0, position / total);
            auto const done_width = static_cast<int>(std::ceil(fraction*bar_width));
            std::clog << std::fixed << std::setprecision(2)
                      << "\r   ["
                      << bar_string.substr(bar_width - done_width, bar_width)
                      << "] "
                      << 100 * fraction << "%" << std::flush;
        } else {
            // unknown total size
            auto pos = input.tellg() % bar_width;
            std::clog << std::fixed << std::setprecision(2)
                      << "\r   ["
                      << bar_string.substr(bar_width - pos, bar_width)
                      << "]" << std::flush;
        }
    }

    ~progress_bar()
    {
        std::clog << std::endl;
    }

private:
    // attempt to measure the length
    static double stream_length(std::istream& input)
    {
        auto initial_position = input.tellg();
        if (!input.seekg(0, std::ios_base::end)) {
            // seek failed: use 0 as sentinel value
            input.clear();
            return 0;
        }

        auto length = static_cast<double>(input.tellg());
        input.seekg(initial_position);
        return length;
    }
};
#include <chrono>
#include <sstream>
#include <thread>

int main()
{
    using namespace std::chrono_literals;

    auto input = std::stringstream{"Hello world"};
    auto const bar = progress_bar(input);

    bar.progress();
    while (input.get() != EOF) {
        bar.progress();
        std::this_thread::sleep_for(1s);
    }
}
\$\endgroup\$
3
  • \$\begingroup\$ I skipped adding the template definition in the question above. And that is template <typename T> \$\endgroup\$ Commented Apr 5, 2023 at 8:57
  • \$\begingroup\$ @G.Sliepen - I was going to mention that (and it's quite easy to remember where it was). Thanks for the reminder - I may update to include that. \$\endgroup\$ Commented Apr 5, 2023 at 11:00
  • \$\begingroup\$ @SyedShujaZaidiP19PH007, that seems an unusual thing to want to template with, given it could simply be always std::streamoff. It's more likely that you'd want to template to match the stream's template arguments (i.e. its CharT). \$\endgroup\$ Commented Apr 5, 2023 at 12:22

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.