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);
}
}