Skip to content

Commit 1aea686

Browse files
committed
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.
1 parent 44ed58f commit 1aea686

5 files changed

Lines changed: 150 additions & 8 deletions

File tree

include/boost/archive/detail/iserializer.hpp

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
// iserializer.hpp: interface for serialization system.
1919

2020
// (C) Copyright 2002 Robert Ramey - http://www.rrsd.com .
21+
// Copyright 2026 Gennaro Prota.
2122
// Distributed under the Boost Software License, Version 1.0.
2223
// (See accompanying file LICENSE_1_0.txt or copy at
2324
// http://www.boost.org/LICENSE_1_0.txt)
@@ -35,6 +36,7 @@ namespace std{
3536
} // namespace std
3637
#endif
3738

39+
#include <boost/core/underlying_type.hpp>
3840
#include <boost/static_assert.hpp>
3941

4042
#include <boost/mpl/eval_if.hpp>
@@ -46,10 +48,12 @@ namespace std{
4648
#ifndef BOOST_SERIALIZATION_DEFAULT_TYPE_INFO
4749
#include <boost/serialization/extended_type_info_typeid.hpp>
4850
#endif
51+
#include <boost/serialization/library_version_type.hpp>
4952
#include <boost/serialization/throw_exception.hpp>
5053
#include <boost/serialization/smart_cast.hpp>
5154
#include <boost/serialization/static_warning.hpp>
5255

56+
#include <boost/type_traits/conditional.hpp>
5357
#include <boost/type_traits/is_pointer.hpp>
5458
#include <boost/type_traits/is_enum.hpp>
5559
#include <boost/type_traits/is_const.hpp>
@@ -560,10 +564,32 @@ template<class Archive>
560564
struct load_enum_type {
561565
template<class T>
562566
static void invoke(Archive &ar, T &t){
563-
// convert integers to correct enum to load
564-
int i;
565-
ar >> boost::serialization::make_nvp(NULL, i);
566-
t = static_cast< T >(i);
567+
// Enumerators were always stored as int before archive library version
568+
// 21; read that layout from any older archive so existing data keeps
569+
// loading. From version 21 on, an enumerator is stored as an int if it
570+
// fits in an int; otherwise as the enum's underlying type (see
571+
// save_enum_type).
572+
if(ar.get_library_version()
573+
< boost::serialization::library_version_type(21)
574+
){
575+
int i;
576+
ar >> boost::serialization::make_nvp(NULL, i);
577+
t = static_cast< T >(i);
578+
return;
579+
}
580+
#ifndef BOOST_NO_UNDERLYING_TYPE
581+
typedef typename boost::underlying_type< T >::type underlying_type;
582+
typedef typename boost::conditional<
583+
sizeof(underlying_type) <= sizeof(int),
584+
int,
585+
underlying_type
586+
>::type load_type;
587+
#else
588+
typedef int load_type;
589+
#endif
590+
load_type lt;
591+
ar >> boost::serialization::make_nvp(NULL, lt);
592+
t = static_cast< T >(lt);
567593
}
568594
};
569595

include/boost/archive/detail/oserializer.hpp

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
// oserializer.hpp: interface for serialization system.
1919

2020
// (C) Copyright 2002 Robert Ramey - http://www.rrsd.com .
21+
// Copyright 2026 Gennaro Prota.
2122
// Distributed under the Boost Software License, Version 1.0.
2223
// (See accompanying file LICENSE_1_0.txt or copy at
2324
// http://www.boost.org/LICENSE_1_0.txt)
@@ -29,6 +30,8 @@
2930

3031
#include <boost/config.hpp>
3132

33+
#include <boost/core/underlying_type.hpp>
34+
3235
#include <boost/static_assert.hpp>
3336
#include <boost/detail/workaround.hpp>
3437

@@ -46,6 +49,7 @@
4649
#include <boost/serialization/assume_abstract.hpp>
4750
#include <boost/serialization/static_warning.hpp>
4851

52+
#include <boost/type_traits/conditional.hpp>
4953
#include <boost/type_traits/is_pointer.hpp>
5054
#include <boost/type_traits/is_enum.hpp>
5155
#include <boost/type_traits/is_const.hpp>
@@ -488,9 +492,25 @@ struct save_enum_type
488492
{
489493
template<class T>
490494
static void invoke(Archive &ar, const T &t){
491-
// convert enum to integers on save
492-
const int i = static_cast<int>(t);
493-
ar << boost::serialization::make_nvp(NULL, i);
495+
// Save an enumerator as an integer wide enough to hold every value of
496+
// the enum's underlying type. An underlying type no wider than int
497+
// keeps the historical layout (a plain int), so existing archives are
498+
// unaffected; a wider underlying type is written at full width instead
499+
// of being truncated to int. Archives that need the wider layout carry
500+
// library version 21 or greater (see load_enum_type).
501+
#ifndef BOOST_NO_UNDERLYING_TYPE
502+
typedef typename boost::underlying_type< T >::type underlying_type;
503+
typedef typename boost::conditional<
504+
sizeof(underlying_type) <= sizeof(int),
505+
int,
506+
underlying_type
507+
>::type save_type;
508+
#else
509+
// no way to query the underlying type: keep the historical int
510+
typedef int save_type;
511+
#endif
512+
const save_type st = static_cast< save_type >(t);
513+
ar << boost::serialization::make_nvp(NULL, st);
494514
}
495515
};
496516

src/basic_archive.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,11 @@ BOOST_ARCHIVE_SIGNATURE(){
8585
// was fully constructed.
8686
// 19- Boost 1.76 April 2021
8787
// 20- Boost 1.84 April 2021
88+
// 21- enumerators are stored as int (if they fit) or as the enum's underlying
89+
// type, instead of always as int
8890
BOOST_SYMBOL_VISIBLE boost::serialization::library_version_type
8991
BOOST_ARCHIVE_VERSION(){
90-
return boost::serialization::library_version_type(20);
92+
return boost::serialization::library_version_type(21);
9193
}
9294

9395
} // namespace archive

test/Jamfile.v2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ test-suite "serialization" :
7676
[ test-bsl-run_files test_derived_class_ptr : A ]
7777
[ test-bsl-run_files test_diamond ]
7878
[ test-bsl-run_files test_diamond_complex ]
79+
[ test-bsl-run_files test_enum ]
7980
[ test-bsl-run_files test_filesystem_path ]
8081
[ test-bsl-run_files test_forward_list : A : : [ requires cxx11_hdr_forward_list ] ] # BOOST_NO_CXX11_HDR_FORWARD_LIST
8182
[ test-bsl-run_files test_forward_list_ptrs : A : : [ requires cxx11_hdr_forward_list ] ] # BOOST_NO_CXX11_HDR_FORWARD_LIST

test/test_enum.cpp

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/////////1/////////2/////////3/////////4/////////5/////////6/////////7/////////8
2+
// test_enum.cpp
3+
4+
// Copyright 2026 Gennaro Prota
5+
// Distributed under the Boost Software License, Version 1.0.
6+
// (See accompanying file LICENSE_1_0.txt or copy at
7+
// https://www.boost.org/LICENSE_1_0.txt)
8+
9+
// Tests serialization of enumerators. In particular it checks that a value of
10+
// a scoped enum whose underlying type is wider than int round trips at full
11+
// width, rather than being truncated to int on save.
12+
13+
#include <cstddef>
14+
#include <cstdio>
15+
#include <fstream>
16+
17+
#include <boost/config.hpp>
18+
19+
#if defined(BOOST_NO_STDC_NAMESPACE)
20+
namespace std{
21+
using ::remove;
22+
}
23+
#endif
24+
25+
#include <boost/serialization/nvp.hpp>
26+
#include "test_tools.hpp"
27+
28+
// underlying type is int, so the archived form is unchanged from earlier
29+
// releases of the library
30+
enum color { red, green = 3, blue = 100 };
31+
32+
#ifndef BOOST_NO_CXX11_SCOPED_ENUMS
33+
34+
// the value 2^32 does not fit in a 32 bit int; a plain cast to int on save
35+
// used to truncate it
36+
enum class wide : long long {
37+
one = 1,
38+
big = 1LL << 32
39+
};
40+
41+
// a narrow underlying type: still stored through int, so unchanged on the
42+
// wire, but it must round trip
43+
enum class narrow : signed char {
44+
minus_one = -1,
45+
two = 2,
46+
hundred = 100
47+
};
48+
49+
#endif // BOOST_NO_CXX11_SCOPED_ENUMS
50+
51+
int test_main(int /* argc */, char * /* argv */[]){
52+
const char * testfile = boost::archive::tmpnam(NULL);
53+
BOOST_REQUIRE(NULL != testfile);
54+
55+
const color c_save = blue;
56+
#ifndef BOOST_NO_CXX11_SCOPED_ENUMS
57+
const wide w_save = wide::big;
58+
const narrow n_save = narrow::minus_one;
59+
#endif
60+
{
61+
test_ostream os(testfile, TEST_STREAM_FLAGS);
62+
test_oarchive oa(os, TEST_ARCHIVE_FLAGS);
63+
oa << boost::serialization::make_nvp("c", c_save);
64+
#ifndef BOOST_NO_CXX11_SCOPED_ENUMS
65+
oa << boost::serialization::make_nvp("w", w_save);
66+
oa << boost::serialization::make_nvp("n", n_save);
67+
#endif
68+
}
69+
color c_load = red;
70+
#ifndef BOOST_NO_CXX11_SCOPED_ENUMS
71+
wide w_load = wide::one;
72+
narrow n_load = narrow::two;
73+
#endif
74+
{
75+
test_istream is(testfile, TEST_STREAM_FLAGS);
76+
test_iarchive ia(is, TEST_ARCHIVE_FLAGS);
77+
ia >> boost::serialization::make_nvp("c", c_load);
78+
#ifndef BOOST_NO_CXX11_SCOPED_ENUMS
79+
ia >> boost::serialization::make_nvp("w", w_load);
80+
ia >> boost::serialization::make_nvp("n", n_load);
81+
#endif
82+
}
83+
BOOST_CHECK(c_save == c_load);
84+
#ifndef BOOST_NO_CXX11_SCOPED_ENUMS
85+
BOOST_CHECK(w_save == w_load);
86+
// the point of the test: the value survived without being truncated
87+
BOOST_CHECK(static_cast<long long>(w_load) == (1LL << 32));
88+
BOOST_CHECK(n_save == n_load);
89+
#endif
90+
91+
std::remove(testfile);
92+
return EXIT_SUCCESS;
93+
}

0 commit comments

Comments
 (0)