You're completely bypassing all the serialization support from Boost Serialization by dealing with your object as raw data (make_binary_object
on a void*
).
That's UB because non of your types are trivial and bitwise-copyable. It's also useless, because you could just use ostream
/istream
directly and be vastly more efficient even if it could work.
I have the strongest suspicion you have experience programming C, but not with C++. The above signs (not realizing C++ classes cannot be bitwise copied) and others (not correctly managing lifetime with constructors/destructors, superfluous .c_str()
calls, non-const-correct print member functions (instead of operator<<
) etc reinforce that impression.
For that reason I'll sketch up your example in C++ style so you can compare notes. First off, use Boost Serialization to serialize your types!
struct MyInfo {
int x;
char z[10]; // TODO prefer `std::array` over C-style arrays
template <typename Ar>
void serialize(Ar& ar, unsigned /*version*/) {
ar & x & boost::serialization::make_array(z, sizeof(z));
}
};
Likewise, for the other types:
class MyCustomData {
protected:
MyInfo* _data = nullptr;
std::string _str1;
friend struct boost::serialization::access;
template <typename Ar> void serialize(Ar& ar, unsigned) { ar& _data& _str1; }
// ....
class MyOtherCustomData : public MyCustomData {
std::string _str2;
friend struct boost::serialization::access;
template <typename Ar> void serialize(Ar& ar, unsigned) {
ar & boost::serialization::base_object<MyCustomData>(*this) & _str2;
}
Note how simple! With just that in place you can already simplify the save/load functions:
template <typename T>
bool saveData(std::string const& path, T const& data, std::string& err) {
if (std::ofstream fout{path}) {
boost::archive::binary_oarchive archive(fout);
archive << data;
return true;
} else {
err = "can't open file " + path;
return false;
}
}
template <typename T>
bool loadData(std::string const& path, T& data, std::string& err) {
if (std::ifstream fin{path}) {
boost::archive::binary_iarchive archive(fin);
archive >> data;
return true;
} else {
err = "can't open file " + path;
return false;
}
}
Which are already far easier and safer to use: Live On Coliru
{
MyCustomData obj1("my info 1", 1, "info1");
MyOtherCustomData obj2("other", "my info 2", 3, "info2");
saveData ("data1.dat", obj1, err);
std::cout << "saved: " << quoted(err) << std::endl;
saveData ("data2.dat", obj2, err);
std::cout << "saved: " << quoted(err) << std::endl;
}
// READ
{
MyCustomData obj3;
MyOtherCustomData obj4;
// read from file
loadData("data1.dat", obj3, err);
std::cout << "loaded: " << obj3 << " (" << quoted(err) << ")" << std::endl;
// read from file
loadData("data2.dat", obj4, err);
std::cout << "loaded: " << obj4 << " (" << quoted(err) << ")" << std::endl;
}
Printing
saved: ""
saved: ""
loaded: my info 1, data: 1, info1 ("")
loaded: other, my info 2, data: 3, info2 ("")
Fixing The Rest
There are still problems!
You seem to be treating the char[10]
as a small string. In reality, std::string
already has the allocation optimization (Meaning of acronym SSO in the context of std::string for details), so you should probably just use it.
Right now, it incurs a whole bunch of problems, in that
- you print it, just assuming there will be a NUL-character terminating the data in the first 10 bytes
- you copy 10 bytes, regardless of the source. At best you get indeterminate data, with some luck you get UB again if you accidentally (indirectly) depend on the values. On some platforms reading beyond the bounds of the static literal data (
"info1"
for example) just terminates the program with a segment violation.
Also, ironically, you seem to want to avoid the dynamic allocation associated with std::string
, but you introduce one unncessarily for new MyInfo
at the same time?
The error-reporting looks like a prime situation to use exceptions. Indeed, you needn't raise them yourself, because Boost Serialization also uses exceptions:
template <typename T> void saveData(std::string const& path, T const& data) {
std::ofstream fout{path};
boost::archive::binary_oarchive archive(fout);
archive << data;
}
template <typename T> void loadData(std::string const& path, T& data) {
std::ifstream fin{path};
boost::archive::binary_iarchive archive(fin);
archive >> data;
}
Your MyCustomData
type is copyable, but when you do it will result in double-free of _data
. Oops, just use Rule of Zero:
Live On Coliru
#include <cstdlib>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <boost/archive/binary_iarchive.hpp>
#include <boost/archive/binary_oarchive.hpp>
#include <boost/serialization/access.hpp>
#include <boost/serialization/array.hpp>
#include <boost/serialization/unique_ptr.hpp>
// some data
struct MyInfo {
int x;
std::array<char, 10> z;
template <typename Ar> void serialize(Ar& ar, unsigned /*version*/) { ar & x & z; }
};
class MyCustomData {
protected:
std::unique_ptr<MyInfo> _data = nullptr;
std::string _str1;
friend struct boost::serialization::access;
template <typename Ar> void serialize(Ar& ar, unsigned) { ar & _data & _str1; }
friend std::ostream& operator<<(std::ostream& os, MyCustomData const& mcd) { return os << mcd.to_string(); }
public:
MyCustomData(std::string s1 = {}, int x = {}, std::string_view z = {})
: _data(new MyInfo{x, {}})
, _str1(std::move(s1)) //
{
auto n = std::min(z.size(), _data->z.size());
std::copy_n(z.data(), n, _data->z.data());
}
virtual ~MyCustomData() = default;
virtual std::string to_string() const {
std::string res(_str1);
res += ", data: " + std::to_string(_data->x) + ", ";
auto safe_n = strnlen(_data->z.data(), _data->z.size());
res.append(_data->z.data(), safe_n);
return res;
}
};
class MyOtherCustomData : public MyCustomData {
std::string _str2;
friend struct boost::serialization::access;
template <typename Ar> void serialize(Ar& ar, unsigned) {
ar & boost::serialization::base_object<MyCustomData>(*this) & _str2;
}
public:
MyOtherCustomData(std::string const& a = {}, std::string const& b = {}, int x = {}, std::string_view z = {})
: MyCustomData(b, x, z)
, _str2(a) {}
std::string to_string() const override {
return _str2 + ", " + MyCustomData::to_string();
}
};
template <typename T> void saveData(std::string const& path, T const& data) {
std::ofstream fout{path};
boost::archive::binary_oarchive archive(fout);
archive << data;
}
template <typename T> void loadData(std::string const& path, T& data) {
std::ifstream fin{path};
boost::archive::binary_iarchive archive(fin);
archive >> data;
}
int main()
{
std::string err;
try {
// SAVE
{
MyCustomData obj1("my info 1", 1, "info1");
MyOtherCustomData obj2("other", "my info 2", 3, "info2");
saveData ("data1.dat", obj1);
std::cout << "data1.data written\n";
saveData ("data2.dat", obj2);
std::cout << "data2.data written\n";
}
{
MyCustomData obj3;
loadData("data1.dat", obj3);
std::cout << "loaded: " << obj3 << std::endl;
}
{
MyOtherCustomData obj4;
loadData("data2.dat", obj4);
std::cout << "loaded: " << obj4 << std::endl;
}
} catch(std::exception const& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
}
Of course, leveraging C++'s standard library is a far better idea than rolling your own "SSO string" and adding unnecessary allocation in the process. Down to 92 LoC and having none of the issues previously existing (no more manual string bounds checking, copying etc):
#include <boost/archive/binary_iarchive.hpp>
#include <boost/archive/binary_oarchive.hpp>
#include <boost/serialization/access.hpp>
#include <fstream>
#include <iostream>
// some data
struct MyInfo {
int x = 0;
std::string z;
template <typename Ar> void serialize(Ar& ar, unsigned /*version*/) { ar& x& z; }
};
class MyCustomData {
protected:
MyInfo _data = {};
std::string _str1;
friend struct boost::serialization::access;
template <typename Ar> void serialize(Ar& ar, unsigned) { ar & _data & _str1; }
public:
MyCustomData(std::string s1 = {}, int x = {}, std::string_view z = {})
: _data{x, std::string{z}}
, _str1(std::move(s1)) {}
virtual ~MyCustomData() = default; // keep for deletion through base class!
virtual std::string to_string() const {
return _str1 + ", data: " + std::to_string(_data.x) + ", " + _data.z;
}
};
class MyOtherCustomData : public MyCustomData {
std::string _str2;
friend struct boost::serialization::access;
template <typename Ar> void serialize(Ar& ar, unsigned) {
ar & boost::serialization::base_object<MyCustomData>(*this) & _str2;
}
public:
MyOtherCustomData(std::string const& a = {}, std::string const& b = {}, int x = {}, std::string_view z = {})
: MyCustomData(b, x, z)
, _str2(a) {}
std::string to_string() const override { return _str2 + ", " + MyCustomData::to_string(); }
};
template <typename T> void saveData(std::string const& path, T const& data) {
std::ofstream fout{path};
boost::archive::binary_oarchive archive(fout);
archive << data;
}
template <typename T> void loadData(std::string const& path, T& data) {
std::ifstream fin{path};
boost::archive::binary_iarchive archive(fin);
archive >> data;
}
static inline std::ostream& operator<<(std::ostream& os, MyCustomData const& mcd) {
return os << mcd.to_string();
}
int main() try {
{
MyCustomData obj1("my info 1", 1, "info1");
saveData("data1.dat", obj1);
std::cout << "data1.data written\n";
}
{
MyOtherCustomData obj2("other", "my info 2", 3, "info2");
saveData("data2.dat", obj2);
std::cout << "data2.data written\n";
}
{
MyCustomData obj3;
loadData("data1.dat", obj3);
std::cout << "loaded: " << obj3 << std::endl;
}
{
MyOtherCustomData obj4;
loadData("data2.dat", obj4);
std::cout << "loaded: " << obj4 << std::endl;
}
} catch (std::exception const& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
Still printing
data1.data written
data2.data written
loaded: my info 1, data: 1, info1
loaded: other, my info 2, data: 3, info2