This works perfectly. It gives me the exact results I want…
This code only “works perfectly” on your particular machine. It is not portable.
Consider reading in a bunch of unsigned long
s. The core of your code (pretending we’re only reading a single unsigned long
and not a bunch of them) is basically this:
auto i = 0uL;
ifs.read(reinterpret_cast<char*>(&i), sizeof(unsigned long));
There are two problems here:
sizeof(unsigned long)
is implementation-defined. I believe it is 4 on Windows; it’s 8 on Linux. The same data file will read differently on different machines.
- The endianness of
unsigned long
is also implementation-defined. If the data in your file is big-endian, and your machine is big-endian, then (probably) no problem. But if the endianness is not the same, you’re going to get garbage.
There are also other issues you will run into if you try to read into more complex types. And “more complex” doesn’t mean all that complex. Just consider the humble int
. Even if you get endianness right, there’s no guarantee that the sign bit will be in the same place across different implementations. Once you get into double
s and such, all bets are off.
Does the code above have any problems allocating/releasing memory?
Yes, because you are using naked new
and delete
. That is bad C++.
So long as you stick to simple types like unsigned int
you will probably never have a problem. However, look at the structure of your code:
T* buffer = new T[num];
ifs.read((char*)buffer, sizeof(T) * num); // <---
for(int i = 0; i < num; ++i) {
output.push_back(buffer[i]); // <---
}
delete[] buffer;
What happens if that call to push_back()
throws? You may argue that it can’t, because you’ve already reserved the memory. But allocation failures are not the only way a push_back()
can fail. Not only that, but ifs.read()
might throw as well. If a throw happens, that buffer is leaked.
This is so easily fixed by just using a smart pointer:
T* buffer = std::make_unique_for_overwrite<T[]>(num); // or std::unique_ptr<T[]>{new T[num]};
ifs.read((char*)buffer.get(), sizeof(T) * num);
for(int i = 0; i < num; ++i) {
output.push_back(buffer[i]);
}
// buffer automatically deletes
Are there any obvious ways I can make it run faster?
The most obvious way I can see is to forgo the buffer. Just do:
template<class T> std::vector<T> getFromBin(std::ifstream& ifs, int num) {
std::vector<T> output;
output.resize(num);
ifs.read(reinterpret_cast<char*>(output.data()), sizeof(T) * num);
return output;
}
This is still the wrong way to read binary data for the reasons I mentioned above… but it is the exact same behaviour of your current code, without the unnecessary buffer.
(As an aside, even if the buffer were necessary, manually writing a loop and doing a bunch of push_back()
s would be silly in this situation. Just do output.assign(buffer, buffer * num);
.)
Incidentally:
std::ifstream ifs(file_name, std::ios::binary | std::ios::ate);
std::streampos total_bytes(ifs.tellg());
ifs.seekg(0, std::ios::beg);
This is not the right way to determine the size of a file. I’ve actually written articles about this, it’s such a common error. Simply put:
- The number returned by
tellg()
is absolutely useless for any purpose other than to seekg()
to the same location. In particular, if you tellg()
, then write some data, then tellg()
again, the difference between those two results is not necessarily the same as the number of bytes written.
- The “end” of the file is not necessarily what you think it is. On some platforms, particularly those with record-based file systems, there can be a chunk of extra data at the “end” of the file. So, for example, even if
tellg()
could actually tell you the correct number of bytes, if you seekg()
to the end of the file, you would still get the wrong number for the size.
There is no portable way to determine the file size other than by trying to actually read to the end, and counting the bytes you read. In other words, this is the only portable way to get the file size using IOstreams:
auto pos = ifs.tellg();
if (not ifs.ignore(std::numeric_limits<std::streamsize>::max()))
throw "read failure";
auto total_bytes = ifs.gcount();
if (not ifs.seekg(pos))
throw "seek failure";
// total_bytes now has the actual, correct number of bytes you can read
// between the current position and the end of the file.
As you can imagine, for a huge file, manually reading through the whole thing just to count bytes is a colossal waste of time. That’s why the pattern of checking the whole file’s size is not a good fit for IOstreams. If you don’t need to know the whole file’s size up front—and your use case, where you are reading chunk-by-chunk doesn’t seem to require knowing the whole file’s size—then don’t bother with it.
As a side note, if you want to know a file’s size portably, you can use std::filesystem::file_size()
. But don’t do this:
auto total_bytes = std::filesystem::file_size(path);
auto ifs = std::ifstream{path, std::ios_base::binary};
// now assume the file size is total_bytes
There is a race condition between reading the file size and opening it. In short: you may read the file size, then the OS may pause your program to give time to another program, that program may delete the file and replace it with a totally new one, then the OS switches back to your program and you open the file stream.
The bottom line is that a stream interface is not really designed for the use pattern of getting the total size, and then reading it. A stream may not even know the total size… for example, if it is a network stream, and it is giving you data as it downloads without knowing the total download size. Yeah, sure, it should usually work for a file, on a local filesystem (but not always!), but the stream interface is for much more than just that.
The biggest issue I see with your current interface is that it doesn’t seem to have any idea that errors might occur, nor does it have a sensible way of handling them.
For example, let’s consider this simplified form:
template<class T> std::vector<T> getFromBin(std::ifstream& ifs, int num) {
std::vector<T> output;
output.resize(num);
ifs.read(reinterpret_cast<char*>(output.data()), sizeof(T) * num);
return output;
}
What happens if the read fails? You will happily return a vector half-full of data, and half-full of random garbage. The user has to write:
auto b = getFromBin<unsigned int>(ifs, 100000);
// is b good? don't know, have to check here:
if (not ifs)
throw "problem";
It would make much more sense to do the checking in the function:
template<class T> std::vector<T> getFromBin(std::ifstream& ifs, int num) {
std::vector<T> output;
output.resize(num);
if (not ifs.read(reinterpret_cast<char*>(output.data()), sizeof(T) * num))
throw ...;
return output;
}
That way you always know that when the function successfully returns, the data is good.
As for those Valgrind errors, as far as I can see, they are not related to any problems with memory allocation. As it says: All heap blocks were freed -- no leaks are possible
. There is no way I can even begin to guess what’s going on without seeing the rest of your program, knowing the compiler and standard library you are using, and knowing the way you called Valgrind.
std::vector
to read bytes into with I/O, but copying oneT
at a time with.push_back
from a new buffer is almost certainly worse than just growing and letting a stupid compiler zero the bytes unnecessarily, then reading into that space. At best this copy loop will optimize to a call tomemcpy
, but maybe not with the growth check and possible reallocation after some data-dependent number of elements copied. \$\endgroup\$