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
8 changed files with 176 additions and 241 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);
}
};
+26 -82
View File
@@ -1,7 +1,6 @@
/////////1/////////2/////////3/////////4/////////5/////////6/////////7/////////8
// (C) Copyright 2002-4 Pavel Vozenilek .
// 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)
@@ -22,16 +21,13 @@
#endif
#include <boost/move/utility_core.hpp>
#include <boost/core/addressof.hpp>
#include <boost/utility/enable_if.hpp>
#include <boost/serialization/item_version_type.hpp>
#include <boost/serialization/library_version_type.hpp>
#include <boost/serialization/version.hpp>
#include <boost/serialization/split_free.hpp>
#include <boost/serialization/nvp.hpp>
#include <boost/serialization/serialization.hpp>
#include <boost/type_traits/is_pointer.hpp>
#include <boost/serialization/detail/is_default_constructible.hpp>
#include <boost/serialization/detail/stack_constructor.hpp>
// function specializations must be defined in the appropriate
// namespace - boost::serialization
@@ -39,91 +35,27 @@ namespace boost {
namespace serialization {
namespace detail {
// The value of an optional<T> is serialized like a single element of an
// STL container: when T is default constructible we serialize it in place,
// otherwise we route construction through save/load_construct_data so that
// types without a default constructor can be reconstructed on load. The
// default-constructible path is left untouched so that archives written by
// earlier versions of the library keep the same layout.
// save the value: T is default constructible
template<class Archive, class OT>
typename boost::enable_if<
typename detail::is_default_constructible<typename OT::value_type>,
void
>::type
save_value(Archive & ar, const OT & ot){
ar << boost::serialization::make_nvp("value", *ot);
}
// save the value: T is not default constructible
template<class Archive, class OT>
typename boost::disable_if<
typename detail::is_default_constructible<typename OT::value_type>,
void
>::type
save_value(Archive & ar, const OT & ot){
typedef typename OT::value_type value_type;
const value_type & v = *ot;
const boost::serialization::item_version_type item_version(
boost::serialization::version<value_type>::value
);
ar << BOOST_SERIALIZATION_NVP(item_version);
boost::serialization::save_construct_data_adl(
ar,
boost::addressof(v),
item_version
);
ar << boost::serialization::make_nvp("value", v);
}
// load the value: T is default constructible
template<class Archive, class OT>
typename boost::enable_if<
typename detail::is_default_constructible<typename OT::value_type>,
void
>::type
load_value(Archive & ar, OT & ot, const unsigned int version){
if(0 == version){
boost::serialization::item_version_type item_version(0);
boost::serialization::library_version_type library_version(
ar.get_library_version()
);
if(boost::serialization::library_version_type(3) < library_version){
ar >> BOOST_SERIALIZATION_NVP(item_version);
}
}
typename OT::value_type t;
ar >> boost::serialization::make_nvp("value", t);
ot = boost::move(t);
}
// load the value: T is not default constructible
template<class Archive, class OT>
typename boost::disable_if<
typename detail::is_default_constructible<typename OT::value_type>,
void
>::type
load_value(Archive & ar, OT & ot, const unsigned int /* version */){
typedef typename OT::value_type value_type;
boost::serialization::item_version_type item_version(0);
ar >> BOOST_SERIALIZATION_NVP(item_version);
detail::stack_construct<Archive, value_type> aux(ar, item_version);
ar >> boost::serialization::make_nvp("value", aux.reference());
ot = boost::move(aux.reference());
ar.reset_object_address(boost::addressof(*ot), aux.address());
}
// OT is of the form optional<T>
template<class Archive, class OT>
void save_impl(
Archive & ar,
const OT & ot
){
// It is an inherent limitation to the serialization of optional.hpp
// that the underlying type must be either a pointer or must have a
// default constructor. It's possible that this could change sometime
// in the future, but for now, one will have to work around it. This can
// be done by serialization the optional<T> as optional<T *>
#ifndef BOOST_NO_CXX11_HDR_TYPE_TRAITS
BOOST_STATIC_ASSERT(
boost::serialization::detail::is_default_constructible<typename OT::value_type>::value
|| boost::is_pointer<typename OT::value_type>::value
);
#endif
const bool tflag(ot);
ar << boost::serialization::make_nvp("initialized", tflag);
if (tflag){
save_value(ar, ot);
ar << boost::serialization::make_nvp("value", *ot);
}
}
@@ -140,7 +72,19 @@ void load_impl(
ot.reset();
return;
}
load_value(ar, ot, version);
if(0 == version){
boost::serialization::item_version_type item_version(0);
boost::serialization::library_version_type library_version(
ar.get_library_version()
);
if(boost::serialization::library_version_type(3) < library_version){
ar >> BOOST_SERIALIZATION_NVP(item_version);
}
}
typename OT::value_type t;
ar >> boost::serialization::make_nvp("value",t);
ot = boost::move(t);
}
} // detail
+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 -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
@@ -146,7 +147,6 @@ if ! $(BOOST_ARCHIVE_LIST) {
# we suppress tests of our dlls when using static libraries
[ test-bsl-run test_dll_simple : : dll_a : <link>static:<build>no ]
[ test-bsl-run test_dll_base_pointer : : dll_polymorphic_base dll_polymorphic_derived2 : <link>static:<build>no ]
# [ test-bsl-run test_dll_plugin : : dll_derived2 : <link>static:<build>no <target-os>linux:<linkflags>-ldl ]
[ test-bsl-run test_private_ctor ]
-75
View File
@@ -1,75 +0,0 @@
/////////1/////////2/////////3/////////4/////////5/////////6/////////7/////////8
// test_dll_base_pointer.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)
// Regression test for issue #117. A derived class defined in a shared
// library is serialized and deserialized through a pointer to its abstract
// base, from an executable built with hidden symbol visibility.
// Historically this failed on GCC/ELF: the per type serializer singletons
// were duplicated across the shared library boundary, so the reader could not
// map the class id back to the derived type. The round trip must recover an
// object of the derived type.
#include <fstream>
#include <cstdio>
#include <typeinfo>
#include <boost/config.hpp>
#include "test_tools.hpp"
#ifndef BOOST_NO_CXX11_SMART_PTR
#include <boost/archive/polymorphic_binary_oarchive.hpp>
#include <boost/archive/polymorphic_binary_iarchive.hpp>
#include <boost/archive/tmpdir.hpp>
#include <boost/smart_ptr/make_unique.hpp>
#include <boost/serialization/unique_ptr.hpp>
#include <boost/serialization/nvp.hpp>
// polymorphic_derived2 (and its base, polymorphic_base) live in the shared
// library this test links against
#define POLYMORPHIC_DERIVED2_IMPORT
#include "polymorphic_derived2.hpp"
int test_main(int /* argc */, char * /* argv */ []){
const char * testfile = boost::archive::tmpnam(NULL);
BOOST_REQUIRE(NULL != testfile);
std::unique_ptr<polymorphic_base> saved =
boost::make_unique<polymorphic_derived2>();
{
std::ofstream os(testfile, std::ios::binary);
boost::archive::polymorphic_binary_oarchive oa(os);
boost::archive::polymorphic_oarchive & poa = oa;
poa << boost::serialization::make_nvp("ptr", saved);
}
std::unique_ptr<polymorphic_base> loaded;
{
std::ifstream is(testfile, std::ios::binary);
boost::archive::polymorphic_binary_iarchive ia(is);
boost::archive::polymorphic_iarchive & pia = ia;
pia >> boost::serialization::make_nvp("ptr", loaded);
}
BOOST_CHECK(0 != loaded.get());
// the object recovered through the base pointer must be the derived type
BOOST_CHECK(0 != dynamic_cast<polymorphic_derived2 *>(loaded.get()));
std::remove(testfile);
return EXIT_SUCCESS;
}
#else
int test_main(int /* argc */, char * /* argv */ []){
return EXIT_SUCCESS;
}
#endif // BOOST_NO_CXX11_SMART_PTR
+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;
}
-75
View File
@@ -146,77 +146,6 @@ int test_move_only(){
}
#endif // move-only support available
// A type without a default constructor. It is reconstructed on load through
// save_construct_data / load_construct_data, which is exactly what an
// optional<ND> now relies on (see issue #121). m_i is the constructor
// argument, m_x is ordinary serialized state.
struct ND {
int m_i;
int m_x;
ND(int i, int x) : m_i(i), m_x(x) {}
explicit ND(int i) : m_i(i), m_x(0) {}
template<class Archive>
void serialize(Archive & ar, const unsigned int /* version */){
ar & boost::serialization::make_nvp("x", m_x);
}
bool operator==(const ND & rhs) const {
return m_i == rhs.m_i && m_x == rhs.m_x;
}
};
namespace boost {
namespace serialization {
template<class Archive>
void save_construct_data(
Archive & ar, const ND * p, const unsigned int /* version */
){
ar << boost::serialization::make_nvp("i", p->m_i);
}
template<class Archive>
void load_construct_data(
Archive & ar, ND * p, const unsigned int /* version */
){
int i;
ar >> boost::serialization::make_nvp("i", i);
::new(p) ND(i);
}
} // serialization
} // boost
template<template<class> class Optional>
int test_non_default_ctor(){
const char * testfile = boost::archive::tmpnam(NULL);
BOOST_REQUIRE(NULL != testfile);
const Optional<ND> o_empty;
const Optional<ND> o_value(ND(7, 42));
{
test_ostream os(testfile, TEST_STREAM_FLAGS);
test_oarchive oa(os, TEST_ARCHIVE_FLAGS);
oa << boost::serialization::make_nvp("o_empty", o_empty);
oa << boost::serialization::make_nvp("o_value", o_value);
}
// start each target in the opposite state, so load must both reset and
// reconstruct
Optional<ND> o_empty_a(ND(1, 1));
Optional<ND> o_value_a;
{
test_istream is(testfile, TEST_STREAM_FLAGS);
test_iarchive ia(is, TEST_ARCHIVE_FLAGS);
ia >> boost::serialization::make_nvp("o_empty", o_empty_a);
ia >> boost::serialization::make_nvp("o_value", o_value_a);
}
BOOST_CHECK(! o_empty_a);
BOOST_CHECK(static_cast<bool>(o_value_a));
BOOST_CHECK(o_value_a && *o_value == *o_value_a);
std::remove(testfile);
return EXIT_SUCCESS;
}
#include <boost/serialization/optional.hpp>
#ifndef BOOST_NO_CXX17_HDR_OPTIONAL
#include <optional>
@@ -233,9 +162,5 @@ int test_main( int /* argc */, char* /* argv */[] ){
test_move_only<std::optional>();
#endif
#endif
test_non_default_ctor<boost::optional>();
#ifndef BOOST_NO_CXX17_HDR_OPTIONAL
test_non_default_ctor<std::optional>();
#endif
return EXIT_SUCCESS;
}