Compare commits

...

1 Commits

Author SHA1 Message Date
Gennaro Prota 58e3c6ef7e Store enums wider than int at full width instead of truncating
Enumerator serialization cast every value to `int` on save and back on
load. For an `enum` whose underlying type does not fit in an `int`, for
example a scoped enum on long long with a value above `INT_MAX`, that
cast silently truncated the value, and users could not intervene because
the archive routes every enum through this path before any user
serialization is consulted.

An enum is now saved through an integer sized to its underlying type:
int when the underlying type is no wider than int, otherwise the
underlying type itself. The common case (including scoped enums that do
not name a type, and narrow underlying types such as char, which stay on
the int path to avoid the archives' character handling) is byte for byte
identical to before, so existing archives and data are unaffected. Only
enums that genuinely need the extra width change on the wire, and those
were being corrupted anyway.

The archive library version is bumped to 21.

Fixes issue #353.
2026-07-22 08:40:26 +02:00
5 changed files with 150 additions and 8 deletions
+29 -4
View File
@@ -18,6 +18,7 @@
// iserializer.hpp: interface for serialization system.
// (C) Copyright 2002 Robert Ramey - http://www.rrsd.com .
// Copyright 2026 Gennaro Prota.
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)
@@ -35,6 +36,7 @@ namespace std{
} // namespace std
#endif
#include <boost/core/underlying_type.hpp>
#include <boost/static_assert.hpp>
#include <boost/mpl/eval_if.hpp>
@@ -46,10 +48,12 @@ namespace std{
#ifndef BOOST_SERIALIZATION_DEFAULT_TYPE_INFO
#include <boost/serialization/extended_type_info_typeid.hpp>
#endif
#include <boost/serialization/library_version_type.hpp>
#include <boost/serialization/throw_exception.hpp>
#include <boost/serialization/smart_cast.hpp>
#include <boost/serialization/static_warning.hpp>
#include <boost/type_traits/conditional.hpp>
#include <boost/type_traits/is_pointer.hpp>
#include <boost/type_traits/is_enum.hpp>
#include <boost/type_traits/is_const.hpp>
@@ -560,10 +564,31 @@ template<class Archive>
struct load_enum_type {
template<class T>
static void invoke(Archive &ar, T &t){
// convert integers to correct enum to load
int i;
ar >> boost::serialization::make_nvp(NULL, i);
t = static_cast< T >(i);
// Enums were always stored as int before archive library version 21;
// read that layout from any older archive so existing data keeps
// loading. From version 21 on, an enum is stored as an int if it fits
// in an int; otherwise as its underlying type (see save_enum_type).
if(ar.get_library_version()
< boost::serialization::library_version_type(21)
){
int i;
ar >> boost::serialization::make_nvp(NULL, i);
t = static_cast< T >(i);
return;
}
#ifndef BOOST_NO_UNDERLYING_TYPE
typedef typename boost::underlying_type< T >::type underlying_type;
typedef typename boost::conditional<
sizeof(underlying_type) <= sizeof(int),
int,
underlying_type
>::type load_type;
#else
typedef int load_type;
#endif
load_type lt;
ar >> boost::serialization::make_nvp(NULL, lt);
t = static_cast< T >(lt);
}
};
+24 -3
View File
@@ -18,6 +18,7 @@
// oserializer.hpp: interface for serialization system.
// (C) Copyright 2002 Robert Ramey - http://www.rrsd.com .
// Copyright 2026 Gennaro Prota.
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)
@@ -29,6 +30,8 @@
#include <boost/config.hpp>
#include <boost/core/underlying_type.hpp>
#include <boost/static_assert.hpp>
#include <boost/detail/workaround.hpp>
@@ -46,6 +49,7 @@
#include <boost/serialization/assume_abstract.hpp>
#include <boost/serialization/static_warning.hpp>
#include <boost/type_traits/conditional.hpp>
#include <boost/type_traits/is_pointer.hpp>
#include <boost/type_traits/is_enum.hpp>
#include <boost/type_traits/is_const.hpp>
@@ -488,9 +492,26 @@ struct save_enum_type
{
template<class T>
static void invoke(Archive &ar, const T &t){
// convert enum to integers on save
const int i = static_cast<int>(t);
ar << boost::serialization::make_nvp(NULL, i);
// Save an enum as an integer wide enough to hold every value of its
// underlying type. An underlying type no wider than int keeps the
// historical layout (a plain int), so existing archives and the
// common case are unaffected; a wider underlying type (for example a
// scoped enum on long long) is written at full width instead of being
// truncated to int. Archives that need the wider layout carry
// library version 21 or greater (see load_enum_type).
#ifndef BOOST_NO_UNDERLYING_TYPE
typedef typename boost::underlying_type< T >::type underlying_type;
typedef typename boost::conditional<
sizeof(underlying_type) <= sizeof(int),
int,
underlying_type
>::type save_type;
#else
// no way to query the underlying type: keep the historical int
typedef int save_type;
#endif
const save_type st = static_cast< save_type >(t);
ar << boost::serialization::make_nvp(NULL, st);
}
};
+3 -1
View File
@@ -85,9 +85,11 @@ BOOST_ARCHIVE_SIGNATURE(){
// was fully constructed.
// 19- Boost 1.76 April 2021
// 20- Boost 1.84 April 2021
// 21- enums are stored as int (if they fit) or as their underlying type,
// instead of always as int
BOOST_SYMBOL_VISIBLE boost::serialization::library_version_type
BOOST_ARCHIVE_VERSION(){
return boost::serialization::library_version_type(20);
return boost::serialization::library_version_type(21);
}
} // namespace archive
+1
View File
@@ -75,6 +75,7 @@ test-suite "serialization" :
[ test-bsl-run_files test_derived_class_ptr : A ]
[ test-bsl-run_files test_diamond ]
[ test-bsl-run_files test_diamond_complex ]
[ test-bsl-run_files test_enum ]
[ test-bsl-run_files test_filesystem_path ]
[ test-bsl-run_files test_forward_list : A : : [ requires cxx11_hdr_forward_list ] ] # BOOST_NO_CXX11_HDR_FORWARD_LIST
[ test-bsl-run_files test_forward_list_ptrs : A : : [ requires cxx11_hdr_forward_list ] ] # BOOST_NO_CXX11_HDR_FORWARD_LIST
+93
View File
@@ -0,0 +1,93 @@
/////////1/////////2/////////3/////////4/////////5/////////6/////////7/////////8
// test_enum.cpp
// Copyright 2026 Gennaro Prota
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE_1_0.txt or copy at
// https://www.boost.org/LICENSE_1_0.txt)
// Tests serialization of enumerations. In particular it checks that a
// scoped enum whose underlying type is wider than int round trips at full
// width, rather than being truncated to int on save.
#include <cstddef>
#include <cstdio>
#include <fstream>
#include <boost/config.hpp>
#if defined(BOOST_NO_STDC_NAMESPACE)
namespace std{
using ::remove;
}
#endif
#include <boost/serialization/nvp.hpp>
#include "test_tools.hpp"
// unscoped enum: underlying type is int, so the archived form is unchanged
// from earlier releases of the library
enum color { red, green = 3, blue = 100 };
#ifndef BOOST_NO_CXX11_SCOPED_ENUMS
// the value 2^32 does not fit in a 32 bit int; a plain cast to int on save
// used to truncate it
enum class wide : long long {
one = 1,
big = 1LL << 32
};
// a narrow underlying type: still stored through int, so unchanged on the
// wire, but it must round trip
enum class narrow : signed char {
minus_one = -1,
two = 2,
hundred = 100
};
#endif // BOOST_NO_CXX11_SCOPED_ENUMS
int test_main(int /* argc */, char * /* argv */[]){
const char * testfile = boost::archive::tmpnam(NULL);
BOOST_REQUIRE(NULL != testfile);
const color c_save = blue;
#ifndef BOOST_NO_CXX11_SCOPED_ENUMS
const wide w_save = wide::big;
const narrow n_save = narrow::minus_one;
#endif
{
test_ostream os(testfile, TEST_STREAM_FLAGS);
test_oarchive oa(os, TEST_ARCHIVE_FLAGS);
oa << boost::serialization::make_nvp("c", c_save);
#ifndef BOOST_NO_CXX11_SCOPED_ENUMS
oa << boost::serialization::make_nvp("w", w_save);
oa << boost::serialization::make_nvp("n", n_save);
#endif
}
color c_load = red;
#ifndef BOOST_NO_CXX11_SCOPED_ENUMS
wide w_load = wide::one;
narrow n_load = narrow::two;
#endif
{
test_istream is(testfile, TEST_STREAM_FLAGS);
test_iarchive ia(is, TEST_ARCHIVE_FLAGS);
ia >> boost::serialization::make_nvp("c", c_load);
#ifndef BOOST_NO_CXX11_SCOPED_ENUMS
ia >> boost::serialization::make_nvp("w", w_load);
ia >> boost::serialization::make_nvp("n", n_load);
#endif
}
BOOST_CHECK(c_save == c_load);
#ifndef BOOST_NO_CXX11_SCOPED_ENUMS
BOOST_CHECK(w_save == w_load);
// the point of the test: the value survived without being truncated
BOOST_CHECK(static_cast<long long>(w_load) == (1LL << 32));
BOOST_CHECK(n_save == n_load);
#endif
std::remove(testfile);
return EXIT_SUCCESS;
}