Compare commits

..

3 Commits

Author SHA1 Message Date
Gennaro Prota bb67164668 Document that a derived archive must call init() itself
Since Boost 1.73, the CRTP base archive classes no longer call `init()`
from their constructors: doing so downcast this to the most derived
class while it was still being constructed, which is undefined behavior
and tripped the sanitizers. The derivation guide still showed the old
pattern, in which a class derived from one of the `xxx_oarchive_impl`
templates relied on the base to write the archive header. Following it
now yields an archive that never writes its header.

The guide now states that the most derived class is responsible for
calling `init()` in its own constructor body, honoring `no_header`, and
shows how, noting that the log_archive example is empty only because it
suppresses the header.

This also corrects the curiously recurring template argument in the
further-derivation sketch, which named `xml_oarchive` instead of
`log_archive`.

Refs issue #182.
2026-07-22 10:56:10 +02:00
Gennaro Prota 7132a62f52 Serialize optional<T> for T's that have no default constructor
The value of an `optional<T>` is now handled like a single element of a
standard library container: when `T` is default constructible, it is
serialized in place, exactly as before; otherwise, construction is
routed through `save_construct_data`/`load_construct_data` so that `T`
can be reconstructed on load. This lets `optional<T>` be serialized for
types that cannot or should not provide a default constructor
(const-qualified members, third party classes, and so on).

The default-constructible path is left byte for byte unchanged, so
archives written by earlier versions keep the same layout. The
construct-data path writes an extra `item_version` plus whatever
`save_construct_data` emits, which is new wire content only for types
that previously could not be serialized at all, so no existing archive
is affected.

Loading reconstructs the value with `detail::stack_construct`, which
placement-constructs the object through `load_construct_data` before the
value is read. This is unlike the pre-2017 implementation, which
serialized into unconstructed storage (`detail::stack_allocate`), so the
reliability concern that motivated the earlier restriction does not
apply here.

Closes issue #121.
2026-07-22 06:34:56 +00:00
Gennaro Prota f689b360b1 Add a regression test for cross-library base pointer deserialization
A derived class defined in a shared library is round-tripped through a
`unique_ptr` to its abstract base, across the library boundary, using a
binary-backed polymorphic archive, from an executable built with hidden
symbol visibility. The test reuses the existing `dll_polymorphic_base`
and `dll_polymorphic_derived2` libraries.

On GCC/ELF the per-type serializer singletons were duplicated across the
boundary, so the reader could not map the class id back to the derived
type and recovered the wrong object.

Refs issue #117.
2026-07-22 06:34:45 +00:00
3 changed files with 179 additions and 28 deletions
+22 -2
View File
@@ -70,8 +70,28 @@ class log_archive :
<li><i>Note the</i> <code style="white-space: normal">log_archive</code> <i>between the</i> &lt;&gt;
This is required so that base classes can downcast their <code style="white-space: normal">this</code> pointer
to the most derived class. This is referred to as <b>C</b>uriously <b>R</b>ecurring
<b>T</b>emplate <b>P</b>attern (<b>CRTP</b>) <a href="bibliography.html#11">[11]</a>.
<b>T</b>emplate <b>P</b>attern (<b>CRTP</b>) <a href="bibliography.html#11">[11]</a>.
It is used to implement static polymorphism.
<li><i>The most derived class is responsible for calling</i> <code style="white-space: normal">init()</code>.
The archive header (signature and version) is written by <code style="white-space: normal">init()</code>.
Before Boost 1.73, the <b>CRTP</b> base called <code style="white-space: normal">init()</code> from its own
constructor, but that downcast <code style="white-space: normal">this</code> to the most derived class
while that class was still being constructed, which is undefined behavior. As of 1.73, the base
classes no longer do this, so a derived archive that wants the standard header must call
<code style="white-space: normal">init()</code> itself, in its own constructor body, after the base
sub-objects have been constructed, honoring the <code style="white-space: normal">no_header</code> flag:
<pre><code>
log_archive(std::ostream &amp; os, unsigned int flags = 0) :
xml_oarchive_impl&lt;log_archive&gt;(os, flags)
{
if(0 == (flags &amp; boost::archive::no_header))
init();
}
</code></pre>
The <code style="white-space: normal">log_archive</code> shown in this example passes
<code style="white-space: normal">no_header</code>, so it has no header to write and its constructor
body is empty; see <code style="white-space: normal">xml_oarchive</code>, or the
<code style="white-space: normal">portable_binary_oarchive</code> example, for archives that do write one.
<li><i>Base classes need to be explicitly given access to the derived class.</i>
This can be done by making members public or by including friend declarations for
the base classes.
@@ -135,7 +155,7 @@ class log_archive :
{
public:
log_archive(std::ostream &amp; os, unsigned int flags = 0) :
log_archive_impl&lt;xml_oarchive&gt;(os, flags)
log_archive_impl&lt;log_archive&gt;(os, flags)
{}
};
</code></pre>
+82 -26
View File
@@ -1,6 +1,7 @@
/////////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)
@@ -21,13 +22,16 @@
#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/type_traits/is_pointer.hpp>
#include <boost/serialization/serialization.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
@@ -35,27 +39,91 @@ 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){
ar << boost::serialization::make_nvp("value", *ot);
save_value(ar, ot);
}
}
@@ -72,19 +140,7 @@ void load_impl(
ot.reset();
return;
}
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_value(ar, ot, version);
}
} // detail
+75
View File
@@ -146,6 +146,77 @@ 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>
@@ -162,5 +233,9 @@ 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;
}