3

The boost serialization library fails to handle special values for doubles correctly when using text archives. That is, trying to deserialize NaN, +inf or -inf will lead to an error (see e.g. this topic).

Therefore I want to write a wrapper class/method, similar to make_array or make_binary_object (see boost doc) to handle those values. I want to use it like this:

class MyClass {

public:
    double value;

    template <class Archive>
    void serialize(Archive &ar, const unsigned int){
        ar & Double_wrapper(value);
    }
};

However, I fail to understand how wrapper classes work internally. Especially I do not understand, how they manage to preserve the connection to the original variable (in this case value) when deserializing.

I tried to write the wrapper like this:

#include <boost/serialization/split_member.hpp>
#include <boost/serialization/wrapper.hpp>
#include <boost/serialization/tracking.hpp>
#include <limits>
#include <cmath>

class Double_wrapper {

private:
    enum double_type {DT_NONE, DT_NAN, DT_INF, DT_NINF};

    double& value;

public:
    Double_wrapper(double& val):value(val){}
    Double_wrapper(Double_wrapper const& rhs):value(rhs.value) {}

private:
    friend class boost::serialization::access;
    template<class Archive>
    void save(Archive & ar, const unsigned int) const {
        double_type flag = DT_NONE;
        double val = value;

        if (!std::isfinite(val)) {
            if (std::isnan(val))    {
                flag = DT_NAN;
            } else if (val > 0) {
                flag = DT_INF;
            } else {
                flag = DT_NINF;
            }
            val = 0;
        }

        ar & val;
        ar & flag;
    }
    template<class Archive>
    void load(Archive & ar, const unsigned int) const {
        double_type flag;

        ar & value;
        ar & flag;

        switch (flag) {
            case DT_NONE:   break;
            case DT_NAN:    value = std::numeric_limits<double>::quiet_NaN(); break;
            case DT_INF:    value = std::numeric_limits<double>::infinity(); break;
            case DT_NINF:   value = -std::numeric_limits<double>::infinity();
        }
    }

    BOOST_SERIALIZATION_SPLIT_MEMBER()
};

BOOST_CLASS_IS_WRAPPER(Double_wrapper)
BOOST_CLASS_TRACKING(Double_wrapper, boost::serialization::track_never)

However, this is more the result of a trial and error process, than of understanding how wrappers work. From this part of the doc I concluded, that I would need to declare the class as a wrapper. But it doesn't seem to work.

When I try to use above code with this MWE

#include <boost/serialization/split_member.hpp>
#include <boost/serialization/wrapper.hpp>
#include <boost/serialization/tracking.hpp>
#include <limits>
#include <cmath>

#include <boost/archive/tmpdir.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/text_oarchive.hpp>
#include <fstream>
#include <iostream>

class Double_wrapper {

private:
    enum double_type {DT_NONE, DT_NAN, DT_INF, DT_NINF};

    double& value;

public:
    Double_wrapper(double& val):value(val){}
    Double_wrapper(Double_wrapper const& rhs):value(rhs.value) {}

private:
    friend class boost::serialization::access;
    template<class Archive>
    void save(Archive & ar, const unsigned int) const {
        double_type flag = DT_NONE;
        double val = value;

        if (!std::isfinite(val)) {
            if (std::isnan(val))    {
                flag = DT_NAN;
            } else if (val > 0) {
                flag = DT_INF;
            } else {
                flag = DT_NINF;
            }
            val = 0;
        }

        ar & val;
        ar & flag;
    }
    template<class Archive>
    void load(Archive & ar, const unsigned int) const {
        double_type flag;

        ar & value;
        ar & flag;

        switch (flag) {
            case DT_NONE:   break;
            case DT_NAN:    value = std::numeric_limits<double>::quiet_NaN(); break;
            case DT_INF:    value = std::numeric_limits<double>::infinity(); break;
            case DT_NINF:   value = -std::numeric_limits<double>::infinity();
        }
    }

    BOOST_SERIALIZATION_SPLIT_MEMBER()
};

BOOST_CLASS_IS_WRAPPER(Double_wrapper)
BOOST_CLASS_TRACKING(Double_wrapper, boost::serialization::track_never)

///////////////////////////////////////////////////////////////////////////////////////

class MyClass {

public:
    double value;

    template <class Archive>
    void serialize(Archive &ar, const unsigned int){
        ar & Double_wrapper(value);
    }
};

///////////////////////////////////////////////////////////////////////////////////////

int main() {

    MyClass tmp;
    tmp.value = std::numeric_limits<double>::quiet_NaN();

    std::cout << "value=" << tmp.value << std::endl;

    std::string filename(boost::archive::tmpdir());
    filename += "/tmp.txt";

    //Output
    std::ofstream ofs(filename.c_str(), std::ios_base::out);
    boost::archive::text_oarchive oar(ofs);
    oar << tmp;

    ofs.close();

    //Input
    MyClass newtmp;
    std::ifstream ifs(filename.c_str(), std::ios_base::in);
    boost::archive::text_iarchive iar(ifs);
    iar >> newtmp;

    std::cout << "value=" << newtmp.value << std::endl;

}

it fails. It gives me the error

error: invalid initialization of non-const reference of type ‘Double_wrapper&’ from an rvalue of type ‘Double_wrapper’

for the line

ar & Double_wrapper(value);

So I am not sure what to do. It seems that using references doesn't work. Will pointers do the trick? Does this work at all?

Any help and/or explanation would be greatly appreciated!

I am using boost version 1.58 on Ubuntu.


Update The code seems to work with vc++ as mentioned in the comments. However, interpreting the statements made in this thread seems to suggest, that it in fact does not, since

The reason MSVC might have accepted it, nonetheless, could be because MSVC has an (evil) non-standard extension that extends lifetimes of temporaries, when bound to a non-const reference.

If I understand this correctly, it should not work anymore when shutting down the program after saving and trying to deserialize in a new instance.


Update As suggested by John Zwinck, there may be a workaround by replacing the call

ar & Double_wrapper(value);

by

Double_wrapper wrapper(value);
ar & wrapper;

However, this does not seem to be the intended behavior of a wrapper object for boost serialization. Moreover, it is unclear (to me) whether this solution is stable (I need it to run with every c++ compiler).

It seems to work on my computer with g++ 5.4.0 and clang++ 3.8.0. Furthermore it works with vc++ on Rextester (boost 1.6).

It produces an archive exception when run with g++ 4.9.3 on rextester (boost 1.54). I wasn't able to test it with clang 3.7 on rextester or g++ 6.1.0 on coliru yet due to (presumably unrelated) linker errors.

Community
  • 1
  • 1
cero
  • 158
  • 7
  • Why are you using a const_cast in Double_wrapper::load(...). Your value is not constant so from what I understand this would be undefined behavior, the compiler can't deal with it. static_cast works. [only const_cast may be used to cast away (remove) constness](http://en.cppreference.com/w/cpp/language/const_cast) – lakeweb Sep 19 '16 at 17:07
  • You're right, thank you. The value inside of Double_wrapper was const before and I did not remove the const_cast. I will remove the const_cast's in the question. However, either way the behavior does not change. – cero Sep 19 '16 at 17:51
  • You are still getting the same error with the above code? It compiles and runs fine, (with a oar.close( ) after output), on Microsoft's V140. – lakeweb Sep 19 '16 at 18:13
  • Yes, I do. That is interesting. So it may be a compiler-dependent problem? Thank you very much so far, I will check it again tomorrow (11.30 pm here) and post the compiler flags I use. In some thread I read, that there may be a problem with ld_library_path, but I ignored this until now. Maybe I will have a look at this. – cero Sep 19 '16 at 21:27
  • Just to be sure we were not crossing up, I copied exactly what you have now and it compiled without an error or warning, and ran fine, with the ofs.close( ); – lakeweb Sep 20 '16 at 00:50
  • Just because, I tried it on g++ [coliru](http://coliru.stacked-crooked.com/a/50e7aadcf896fe64). I'll take a look at errors and see if I can ferret this out. – lakeweb Sep 20 '16 at 01:24
  • My compiler flags seem to have no influence on the result (running without any flags produces the same error). I could confirm, using [Rextester](http://rextester.com/live/YWDTIV21879), that it runs fine with vc++. With rextester's [gcc](http://rextester.com/live/TJYU89127) and clang I get the error message error: no match for ‘operator&’ (operand types are boost::archive::text_oarchive’ and ‘Double_wrapper’) ar & Double_wrapper(value); But this seems to be the same problem as before. (I added the ofs.close and included iostream in the MWE above) – cero Sep 20 '16 at 08:24

1 Answers1

1

I think instead of this:

    ar & Double_wrapper(value);

You should do this:

    Double_wrapper wrapper(value);
    ar & wrapper;

Reason being that your error message complains about the fact that you're using an rvalue where it wants an lvalue.

John Zwinck
  • 239,568
  • 38
  • 324
  • 436
  • Thank you! This looks easy enough to be sufficient for what I am planning to do. I was aware of the issue, but thought I couldn't change it without splitting the serialization in two parts. However, I am still unsure about two points: 1. Why does it work as I expected for e.g. make_array or make_binary_object? What is the difference? 2. Why does this work at all? Shouldn't the deserialization create a completely new object? How does it preserve the reference then? Is there any specification about this? – cero Sep 20 '16 at 12:53
  • Also note that the macro BOOST_CLASS_IS_WRAPPER is not needed then. I think this macro should be the key to using the wrapper as I want to, but I don't understand how it works (and why it doesn't). – cero Sep 20 '16 at 14:41
  • Hi John, I tried that yesterday on [Coliru](http://coliru.stacked-crooked.com/a/5efa428e8d12c537). But it made boost blowup with gcc. It worked with the MS compiler. Cero, did it work with your compiler? – lakeweb Sep 20 '16 at 15:17
  • I was not buying it. Double_wrapper is constructed on the stack and is not an rvalue from what I understand. So to be sure I added ~Double_wrapper( ) to the class and set a break point. Is it possible that gcc is getting confused here? – lakeweb Sep 20 '16 at 16:18
  • Yes, this works with my compiler. Not with gcc and clang on rextester though. I use g++ 5.4.0. – cero Sep 21 '16 at 07:53