From 15011010cb2baf5656dc1a5ad53eff2abd569ce9 Mon Sep 17 00:00:00 2001 From: Gennaro Prota Date: Tue, 21 Jul 2026 18:54:18 +0200 Subject: [PATCH] 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`, 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 #353. --- include/boost/archive/detail/iserializer.hpp | 34 ++++++- include/boost/archive/detail/oserializer.hpp | 26 +++++- src/basic_archive.cpp | 4 +- test/Jamfile.v2 | 1 + test/test_enum.cpp | 93 ++++++++++++++++++++ 5 files changed, 150 insertions(+), 8 deletions(-) create mode 100644 test/test_enum.cpp diff --git a/include/boost/archive/detail/iserializer.hpp b/include/boost/archive/detail/iserializer.hpp index 59c3ba615..6a35b9992 100644 --- a/include/boost/archive/detail/iserializer.hpp +++ b/include/boost/archive/detail/iserializer.hpp @@ -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 #include #include @@ -46,10 +48,12 @@ namespace std{ #ifndef BOOST_SERIALIZATION_DEFAULT_TYPE_INFO #include #endif +#include #include #include #include +#include #include #include #include @@ -560,10 +564,32 @@ template struct load_enum_type { template 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); + // Enumerators 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 enumerator is stored as an int if it + // fits in an int; otherwise as the enum's 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); } }; diff --git a/include/boost/archive/detail/oserializer.hpp b/include/boost/archive/detail/oserializer.hpp index 1a9bddec8..e0d568e8c 100644 --- a/include/boost/archive/detail/oserializer.hpp +++ b/include/boost/archive/detail/oserializer.hpp @@ -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 +#include + #include #include @@ -46,6 +49,7 @@ #include #include +#include #include #include #include @@ -488,9 +492,25 @@ struct save_enum_type { template static void invoke(Archive &ar, const T &t){ - // convert enum to integers on save - const int i = static_cast(t); - ar << boost::serialization::make_nvp(NULL, i); + // Save an enumerator as an integer wide enough to hold every value of + // the enum's underlying type. An underlying type no wider than int + // keeps the historical layout (a plain int), so existing archives are + // unaffected; a wider underlying type 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); } }; diff --git a/src/basic_archive.cpp b/src/basic_archive.cpp index 313728ecd..f2e137f54 100644 --- a/src/basic_archive.cpp +++ b/src/basic_archive.cpp @@ -85,9 +85,11 @@ BOOST_ARCHIVE_SIGNATURE(){ // was fully constructed. // 19- Boost 1.76 April 2021 // 20- Boost 1.84 April 2021 +// 21- enumerators are stored as int (if they fit) or as the enum's 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 diff --git a/test/Jamfile.v2 b/test/Jamfile.v2 index 6800c81d4..ad144877b 100644 --- a/test/Jamfile.v2 +++ b/test/Jamfile.v2 @@ -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 diff --git a/test/test_enum.cpp b/test/test_enum.cpp new file mode 100644 index 000000000..058f57492 --- /dev/null +++ b/test/test_enum.cpp @@ -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 enumerators. In particular it checks that a value of +// a scoped enum whose underlying type is wider than int round trips at full +// width, rather than being truncated to int on save. + +#include +#include +#include + +#include + +#if defined(BOOST_NO_STDC_NAMESPACE) +namespace std{ + using ::remove; +} +#endif + +#include +#include "test_tools.hpp" + +// 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(w_load) == (1LL << 32)); + BOOST_CHECK(n_save == n_load); + #endif + + std::remove(testfile); + return EXIT_SUCCESS; +}