Compare commits

..

1 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
5 changed files with 22 additions and 149 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>
-15
View File
@@ -92,21 +92,6 @@ so that instead of the above, we could write:
BOOST_CLASS_VERSION(my_class, 2)
</code></pre>
which expands to the code above.
<p>
<code style="white-space: normal">BOOST_CLASS_VERSION</code> writes a full
specialization, so it applies to a concrete class. To assign a version to a
class <i>template</i> a partial specialization is needed instead, which
<code style="white-space: normal">BOOST_CLASS_TEMPLATE_VERSION</code>
provides. The template parameter list and the specialized type are each
passed parenthesized, as they normally contain commas:
<pre><code>
BOOST_CLASS_TEMPLATE_VERSION((class T, class U), (my_class&lt;T, U&gt;), 2)
</code></pre>
This assigns version 2 to every instantiation of
<code style="white-space: normal">my_class</code>. Non-type template
parameters are written the same way, for example
<code style="white-space: normal">(class T, std::size_t N)</code> paired with
<code style="white-space: normal">(my_class&lt;T, N&gt;)</code>.
<h3><a name="level">Implementation Level</a></h3>
In the same manner as the above, the "level" of implementation of serialization is
-29
View File
@@ -72,7 +72,6 @@ const int version<T>::value;
#include <boost/mpl/less.hpp>
#include <boost/mpl/comparison.hpp>
#include <boost/preprocessor/punctuation/remove_parens.hpp>
// specify the current version number for the class
// version numbers limited to 8 bits !!!
@@ -103,32 +102,4 @@ struct version<T > \
} \
}
// Specify the current version number for a class template. Unlike
// BOOST_CLASS_VERSION, which writes a full specialization, this writes a
// partial one, so it works for a template rather than a concrete class.
// The template parameter list and the specialized type are each passed
// parenthesized (they usually contain commas):
//
// BOOST_CLASS_TEMPLATE_VERSION((class T, class U), (my_class<T, U>), 2)
//
// version numbers limited to 8 bits !!!
#define BOOST_CLASS_TEMPLATE_VERSION(TEMPLATE_PARAMETERS, TYPE, N) \
namespace boost { \
namespace serialization { \
template< BOOST_PP_REMOVE_PARENS(TEMPLATE_PARAMETERS) > \
struct version< BOOST_PP_REMOVE_PARENS(TYPE) > \
{ \
typedef mpl::int_<N> type; \
typedef mpl::integral_c_tag tag; \
BOOST_STATIC_CONSTANT(int, value = version::type::value); \
BOOST_MPL_ASSERT(( \
boost::mpl::less< \
boost::mpl::int_<N>, \
boost::mpl::int_<256> \
> \
)); \
}; \
} \
}
#endif // BOOST_SERIALIZATION_VERSION_HPP
-1
View File
@@ -64,7 +64,6 @@ test-suite "serialization" :
[ test-bsl-run_files test_binary ]
[ test-bsl-run_files test_class_info_save ]
[ test-bsl-run_files test_class_info_load ]
[ test-bsl-run_files test_class_template_version ]
[ test-bsl-run_files test_bitset ]
[ test-bsl-run_files test_complex ]
[ test-bsl-run_files test_contained_class : A ]
-102
View File
@@ -1,102 +0,0 @@
/////////1/////////2/////////3/////////4/////////5/////////6/////////7/////////8
// test_class_template_version.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 BOOST_CLASS_TEMPLATE_VERSION, which assigns a serialization version
// to a class template through a partial specialization of the version trait.
#include <cstddef>
#include <cstdio>
#include <fstream>
#include <boost/config.hpp>
#if defined(BOOST_NO_STDC_NAMESPACE)
namespace std{
using ::remove;
}
#endif
#include <boost/static_assert.hpp>
#include <boost/serialization/nvp.hpp>
#include <boost/serialization/version.hpp>
#include "test_tools.hpp"
// a class template with two type parameters; serialize records the version
// it is given on load so the round trip can be checked
template<class T, class U>
struct pair_holder {
T first;
U second;
unsigned int loaded_version;
pair_holder() : first(), second(), loaded_version(0) {}
pair_holder(T f, U s) : first(f), second(s), loaded_version(0) {}
template<class Archive>
void serialize(Archive & ar, const unsigned int version){
loaded_version = version;
ar & boost::serialization::make_nvp("first", first);
ar & boost::serialization::make_nvp("second", second);
}
};
BOOST_CLASS_TEMPLATE_VERSION((class T, class U), (pair_holder<T, U>), 5)
// a class template mixing a type parameter and a non-type parameter
template<class T, std::size_t N>
struct sized_holder {
T value;
};
BOOST_CLASS_TEMPLATE_VERSION((class T, std::size_t N), (sized_holder<T, N>), 3)
// a template we do not version, to confirm the default is still 0
template<class T>
struct unversioned {
T value;
};
int test_main(int /* argc */, char * /* argv */[]){
// the macro must set the compile time version for every instantiation of
// the template, regardless of the actual arguments
BOOST_STATIC_ASSERT(
(boost::serialization::version<pair_holder<int, double> >::value == 5)
);
BOOST_STATIC_ASSERT(
(boost::serialization::version<pair_holder<char, long> >::value == 5)
);
// works for a non-type parameter too
BOOST_STATIC_ASSERT(
(boost::serialization::version<sized_holder<int, 4> >::value == 3)
);
// an unversioned template still defaults to 0
BOOST_STATIC_ASSERT(
(boost::serialization::version<unversioned<int> >::value == 0)
);
const char * testfile = boost::archive::tmpnam(NULL);
BOOST_REQUIRE(NULL != testfile);
const pair_holder<int, double> saved(7, 2.5);
{
test_ostream os(testfile, TEST_STREAM_FLAGS);
test_oarchive oa(os, TEST_ARCHIVE_FLAGS);
oa << boost::serialization::make_nvp("ph", saved);
}
pair_holder<int, double> loaded;
{
test_istream is(testfile, TEST_STREAM_FLAGS);
test_iarchive ia(is, TEST_ARCHIVE_FLAGS);
ia >> boost::serialization::make_nvp("ph", loaded);
}
BOOST_CHECK(loaded.first == saved.first);
BOOST_CHECK(loaded.second == saved.second);
// the version set by the macro must be delivered to serialize on load
BOOST_CHECK(loaded.loaded_version == 5);
std::remove(testfile);
return EXIT_SUCCESS;
}