[serialization docs] Ping?

"Robert Ramey" <ramey@rrsd.com> writes:
David Abrahams [mailto:dave@boost-consulting.com] wrote:
The following message is a courtesy copy of an article that has been posted to gmane.comp.lib.boost.devel as well.
Hi Robert,
Are you going to reply to
http://article.gmane.org/gmane.comp.lib.boost.devel/131257
??
Thanks,
I considered your comments and re-wrote the section Archive Concept and it's checked into RC_1_33_0.
It's an improvement, but either you missed or intentionally discarded some of my more important remarks. The most important question was: **** Isn't something in an Archive required to call "serialize?" **** If not, how does an Archive work with Serializable types?
There are two Archive concepts. Given a set of C++ data structures consisting of Serializable types, a Saving Archive will create a sequence of bytes. Given a sequence of bytes created by a Saving Archive, a Loading Archive will reconstitute the orginal configuration of C++ data structures. Both Archive concepts are derived from the more basic concept of Archive.
This is confusing and self-contradictory. Your text clearly states that there are two Archive concepts and then goes on to describe three Archive concepts: Archive, Saving Archive, and Loading Archive.
Notation
In the following descriptions, it is assumed that: ^^^^^^^^^^^^^^^^^^^^ Strike this. You are telling the reader what these identifiers mean, not interpreting something about which you need to make some assumption.
• A is an Archive type • T is an Serializable Type • ar is an instance of type A. • x is an instance of type T. • u is an address.
An address is an abstract notion; it doesn't have any value in the type system. You have to say "u is any pointer"... but see below.
Common Type Definitions in all Archives
This should be a section for the "Archive" concept. These are not type definitions but valid expressions.
A::is_saving
An integral constant of type boost::mpl::bool_.
I'm sorry to be so blunt, but that's nonsense. A::is_saving is a type, not an integral constant, and boost::mpl::bool_ is a class template, not a type. If you mean to refer to the MPL Integral Constant concept, the correct thing to say would be: A boolean MPL Integral Constant with a link to the documentation for that concept.
Value is boost::mpl::bool_<true>
"Value?" This is also nonsense. The ::value member of an MPL Integral Constant is an integral constant, not a type, but boost::mpl::bool_<true> is a type. I normally
if the Archive is a Saving Archive (see below) and boost::mpl::bool_<false> otherwise.
A::is_loading
An integral constant of type boost::mpl::bool_. Value is boost::mpl::bool_<true> if the Archive is a Loading Archive (see below) and boost::mpl::bool_<false> otherwise.
Ditto.
Common Member Functions in all Archives
Again, these are not member functions, but valid expressions. Presumably "ar & x" can be implemented by a free function and "ar.template register_type<T>()" invokes a member function template.
ar & x
Returns a reference to ar. Appends the value of x along with other information to ar.
I think you should swap the two sentences; emphasis should be on effects and only secondarily on return value. But wait. Surely this expression, with the specified semantics, doesn't have to be valid for loading archives? Are you saying that all archives have to be able to save?
ar.template register_type<T>();
You should leave out "template." This is a little tricky, but that expression will never be valid in a context where A is not a dependent type. I realize you do need it when A is a dependent type, but the convention is to use the simpler notation. The author of an archive isn't going to be calling register_type anyway (especially not in a dependent context), so it won't help prevent him from making errors.
ar.register_type(u);
Append information about class T to the archive.
^--"s" If so then you need to say that u is an expression of type T*
This information is used to construct the correct class when a derived pointer is loaded. Invocation of this member function is referred to as "class registration". This is explained in detail in Special Considerations - Derived Pointers.
ar.library_version()
Returns an unsigned integer containing the version number of the serialization library that created the archive. This number will be incremented each time the library is altered in such a way that serialization could be altered for some type. For example, suppose the type used for a count of collection members is changed. The code that loads collections might be conditioned on the library version to make sure that libraries created by previous versions of the library can still be read.
Good!
Saving Archive
A Saving Archive is a refinement of the Archive Concept. ^--- strike "A." The Saving Archive concept refines Archive. "A saving archive" referes either to a model of Saving Archive or an instance of such a model.
Notation
In the following descriptions, it is assumed that:
• ar is an instance of a Saving Archive. • x is an instance of a Serializable Type. • u is an address.
A pointer again.
• count is an integer that can be converted to std::size_t. ^^^^^^^ no, it's an instance of any type that can be so converted. Your "strong typedef" isn't an integral type.
Common Member Functions
Valid expressions again.
ar << x
Returns a reference to ar. Appends the value of x along with other information to ar.
Doesn't this have to call serialize(...) somehow?
ar.save_binary(u, count)
Appends to the archive count bytes found at address.
Actually it must be Appends to ar the std::size_t(count) bytes found at address. but doesn't it have to call serialize(...) somehow?
Loading Archive
Notation
In the following descriptions, it is assumed that:
• ar is an instance of a Loading Archive. • x is an instance of a Serializable Type. • u, v are pointers to any type. • count is an integer that can be converted to std::size_t.
Common Member Functions in all Archives
ar >> x
Returns a reference to ar. Sets x with a value retrieved from ^^^^ I would say "to" the ar. ^^^ strike this.
Doesn't this have to call serialize(...) somehow?
ar.load_binary(u, count)
Retrieves from the archive count bytes and stores them in ^^^^^^^^^^ "ar" memory starting at address. ^^^^^^^ "u"
Doesn't this have to call serialize(...) somehow?
ar.reset_object_address(v, u)
Communicates to the archive that the object originally at address u has been moved to address v.
In order to facilitate loading of objects through a pointer and to elminate redundant loading of objects, this system implements object address tracking. Normally this is done automatically with no action required on the part of the user.
For an implementor of an archive, this whole statement is confusing. Do I need to implement object tracking, or is provided for me somehow by "this system?" Is it done automatically? If so, by whom? I think I know the answers to these questions, but I'm not sure a new reader will.
However, there are cases when an object must be de-serialized to a temporary location then moved to its final destination. This is common in loading collections.
In such cases, reset_object_address should be invoked to communicate the final address of the last item loaded. This permits the internal tables to be correctly maintained in these special cases.
ar.delete_created_pointers()
Deletes all objects created by the loading of pointers. This can be used to avoid memory leaks that might otherwise occur if pointers are being loaded and the archive load encounters an exception.
The following sentence belongs under "Archive Models."
There are archives based on text, binary and XML file formats but all have the above interface.
Given that all archives present the same public interface, specifcation of serialization is exactly the same for all archives. Archive classes have other members not mentioned here. However they are related to the internal functioning of the library and are not meant to be called by users of an archive.
Are you telling me, after all this, that there are additional requirements on archives not documented in the section on Archive concepts? I find this rather alarming. Haven't I made it abundantly clear that the concept documentation has to give at least a minimal and complete documentation of what's required of an Archive?
Implementation of new archives is discussed in New Archives - Implementation.
Which then goes on to describe some "input archive" idea that hasn't been defined, etc... <snip> Okay, I don't know what to say at this point. I'm about to give up, because I don't have the time to keep going over this. It sure seems like you want to do this your own way, rather than by following the established practices and norms. If you were just "creative" but still rigorous, it would be one thing, but it doesn't even seem like you're paying close attention to what you yourself are writing. What should I do?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
© Copyright Robert Ramey 2002-2004. 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)
You might want to update the copyright date. -- Dave Abrahams Boost Consulting www.boost-consulting.com

David Abrahams wrote: Note that points not explicitly responded to have just been fixed in accordance with your suggestion.
It's an improvement, but either you missed or intentionally discarded some of my more important remarks. The most important question was:
**** Isn't something in an Archive required to call "serialize?" ****
Strictly speaking no. Of course, I would expect all archive implementations to call "serialize" but maybe someone comes up with an archive implementation that just checks syntax or something like that. Someone might want to make an archive which only stored/loaded primitive types. Or someone might make an archive that was implemented solely in terms of save/load_binary. These would never call serialize. The point is that if one writes a program in accordance with the requirements stated here, the program will be a legal program and will compile.
If not, how does an Archive work with Serializable types?
That is defined by the particular archive implementation. There are 5 included and maybe more on the way. There may be one on the way that is only for output - perhaps useful for debug logging. These are not meant to be prohibited by these requirements.
Common Type Definitions in all Archives
This should be a section for the "Archive" concept. These are not type definitions but valid expressions.
A::is_saving
An integral constant of type boost::mpl::bool_.
I'm sorry to be so blunt, but that's nonsense. A::is_saving is a type, not an integral constant, and boost::mpl::bool_ is a class template, not a type. If you mean to refer to the MPL Integral Constant concept, the correct thing to say would be:
A boolean MPL Integral Constant
with a link to the documentation for that concept.
Value is boost::mpl::bool_<true>
OK what I meant to say is A boolean MPL Integral Constant for which the following expression is true boost::mpl::is_equal< A::is_loading, boost::mpl::bool_<true>
<snip> How would you suggest I phrase this?
Common Type Definitions in all Archives
Common Member Functions in all Archives
Again, these are not member functions, but valid expressions. Presumably "ar & x" can be implemented by a free function and "ar.template register_type<T>()" invokes a member function template.
I have to confess I plagerized this exact phrasing from my copy of STL Tutorial and Reference Guide - page 263. I felt it was a very close analogy and a good model. I did note that in this reference operators that could or would be implemented as free functions were included under the "wrong" heading but I presume this was done this way on purpose in order to improve clarity and reflect the fact that most common implmentations would be member functions. I could change the heading to "Common Operations for all Archives" or .. your suggestion here.
ar & x
Returns a reference to ar. Appends the value of x along with other information to ar.
I think you should swap the two sentences; emphasis should be on effects and only secondarily on return value.
Again I Plagerized this form the same source as above page 264. I'm not sure this is a big issue
But wait. Surely this expression, with the specified semantics, doesn't have to be valid for loading archives? Are you saying that all archives have to be able to save?
ar.template register_type<T>();
To deal with derived types not otherwise explicitly referred to, any invocation of this function must appear in both the saving AND loading archives in the same place. This is the mechanism by which saving and loading is kept in syncronization.
You should leave out "template." This is a little tricky, but that expression will never be valid in a context where A is not a dependent type. I realize you do need it when A is a dependent type, but the convention is to use the simpler notation.
Hmm - all Archives included with the library are templates. So, removing this will be including a "valid expression" which will fail to compile on all conforming compilers. I user who looks here to see what the valid syntax is for invoking this operation, and uses it to get a bug will be writing to tell me the manual is wrong. I stll could use ar.register_type(& t) - but that's confusing as well. So what's a good solution here?
The author of an archive isn't going to be calling register_type anyway (especially not in a dependent context), so it won't help prevent him from making errors.
This is extremely curious to me. This whole section started as the "Interface for users of the serialization library". I would expect that this is one section that would be of interest to users. register_type is essential to library users. So it has to be explained correctly right here.
. count is an integer that can be converted to std::size_t. ^^^^^^^ no, it's an instance of any type that can be so converted. OK Your "strong typedef" isn't an integral type.
I don't see how this is related. <snip repetition of the above but for loading archives>
The following sentence belongs under "Archive Models."
There are archives based on text, binary and XML file formats but all have the above interface.
Given that all archives present the same public interface, specifcation of serialization is exactly the same for all archives.
I think that should be pretty clear
Are you telling me, after all this, that there are additional requirements on archives not documented in the section on Archive concepts?
No.
Archive classes have other members not mentioned here.
For example, in order to use a text_oarchive, a user must invoke the appropriate constructor with the appropriate arguments.
However they are related to the internal functioning of the library and are not meant to be called by users of an archive.
Hmm - now that I read this I'm not sure what I really meant. I'll think about this.
I find this rather alarming. Haven't I made it abundantly clear that the concept documentation has to give at least a minimal and complete documentation of what's required of an Archive?
I don't believe anything so far conflicts with that.
Okay, I don't know what to say at this point. I'm about to give up, because I don't have the time to keep going over this. It sure seems like you want to do this your own way, rather than by following the established practices and norms. If you were just "creative" but still rigorous, it would be one thing, but it doesn't even seem like you're paying close attention to what you yourself are writing.
Actually that's not true. I think the record on this list and CVS shows multiple versions of trying to get this right. Other changes you have suggested have been incorporated once the issues have been clarified. I have based my various versions on several known "good" models. You have said yourself that the latest version is an improvement.
What should I do?
You're doing just great Dave, just hang in there.
Implementation of new archives is discussed in New Archives - Implementation.
Which then goes on to describe some "input archive" idea that hasn't been defined, etc...
<snip>
Assuming that "Archive Concept" is more or less resloved, the question arises regarding the rest of the documentation. Basically there are two "large" sections. Serializable Concept. Without having looked at it recently, I'm can assume this section doesn't meet your definition of formal documentation. I would guess this could be addressed along the lines of Archive Concept, but I do have a couple of reservations even before I look at it. Much of the documentation there explains things in terms of implementation behavior (how serialization of pointers works, etc) which doesn't seem to fit all what well with the definition of formal semantics. The whole question of how much semantics should be in a formal reference is murky to me. It seems that the idea of formal specification is to emulate something like the specification of and algebra - not a bad place to start. But specification of the rules for an algebra makes no reference at all to its semantics - hence its wide applicability. In the case of library specification we want to role library semantics into this. It seems to me that that might obscure the whole idea in the first place. Your question - when does the "serialize" funcition get called? and my response "what does it matter" illustrate differing understandings about this. I'm starting to wonder - if its really a formal definition - how can it have sematics? And if doesn't have semantics, how can it be useful? This is the reason I was so intrigued with Joaquin's post. It touches upon what to me is the crux of the issue. I'm not sure it resolves it - but it does address the proper place of semantics in the discussion. So, I'm still thinking about what needs to be done (if anything) regarding the "Serializable" type concept section. Don't feel obligated to coment on the above. Achive Implementation Here is the part of the documentation that I have been most concerned about. I changed "Archive User Interface" to "Archive Concept" without thinking too much about it. I don't remember any user having any question about this. So I wasn't too concerned. This part of the library really describes a "tool kit" for implementing the Achive Concepts. To make an analogy to your iterator library there are two things going. Definition of iterator (archive) concept - A user of an iterator instance need only look at this part of the documentation to use an iterator which models the concept. Definintion of the iterator (archive) CRTP classes for deriving one's own iterators (archive) This is a lot harder to explain and use. I know because I did use it. I should say that I was very pleased with the results as once I got things to compile, I had very few problems. So I've been planning to re-visit "Archive Implementation" in any case. But I don't think there is any quick fix. I just have to spend more time on it. And there are a couple of other issues. I had to add to the interface a tiny bit in order to support shared_ptr serialization. Its not a big thing but I didn't want to document it because I'm thinking its not quite right (though its good enough for shared_ptr) and I don't want anyone else to use it. There might be a couple of other things that might or might not need to be made public such as stack_construct<T>(Archive &ar). I'm still thinking about how to handle things BOOST_SERIALIZATION_NVP. This is optional for most archives but required for xml_archives. So does this mean an refinement on the concepts already described. It would look like it. So - I would like to: a) resolve the final issues regarding "Archive Concept" I don't think we're at all far apart in this. b) ship RC_1_33_0 (again). This is important to me as I've fixed the header sequence issue as well as a nasty bug. So I would like to see the new version shipped ASAP. c) Give me some time to go back to the things I mentioned above. I believe that the serialization library has proved quite useful to some people inspite of the lack of formality in its documentation and shipping of a corrected version shouldn't be held up for this. Lastly, I know I'm driving you crazy. I'm sorry about that. Please believe me that its not intentional on my part - its just comes naturally. I do sometimes think you underestimate the depth of my thinking on some of these subjects. Perhaps I'm confused about some of these things because I think about them too much. I don't know if its any consolation, but its stressful for me as well. But your input has been very helpful and added a lot to quality of the library and its documentation and that's why I put up with it. I sure hope you can laugh about this. Robert Ramey

"Robert Ramey" <ramey@rrsd.com> writes:
David Abrahams wrote:
Note that points not explicitly responded to have just been fixed in accordance with your suggestion.
It's an improvement, but either you missed or intentionally discarded some of my more important remarks. The most important question was: **** Isn't something in an Archive required to call "serialize?" ****
Strictly speaking no.
<snip>
The point is that if one writes a program in accordance with the requirements stated here, the program will be a legal program and will compile.
But that's not enough. I'm not interested in making an archive that just compiles; I want it to have some sensible semantics. What requirements do I have to fulfill in order to make the archive actually *work*? If you look at the iterator requirements, for example, you'll see that there are semantic requirements in place, not just syntactic ones. These requirements are needed so that the algorithms can say something about what they do. The sort() algorithm wouldn't be able to make any claims about its result if writing into a random access iterator were allowed to be a no-op. As I've said before, you will need eventually to describe the relationship between compatible loading and saving archives. It will be something like T x, y; // arbitrary operations on x to set its state sar & x; lar & y; Postcondition: y is equivalent to x [Someone else suggested a complex way to describe this idea without mentioning the word "equivalent" but I've no idea whether it hangs together, and at least this is good enough for the standard] You can't say anything like that unless loading and saving are required to do _something_. out of context:
Of course, I would expect all archive implementations to call "serialize" but maybe someone comes up with an archive implementation that just checks syntax or something like that. Someone might want to make an archive which only stored/loaded primitive types. Or someone might make an archive that was implemented solely in terms of save/load_binary. These would never call serialize.
Do not build in generality that you're not even sure is needed, especially if it costs you your ability to write concepts that tell people how to get sensible results. You can always introduce less-refined concepts that allow these kinds of things and provide weaker semantic guarantees.
If not, how does an Archive work with Serializable types?
That is defined by the particular archive implementation. There are 5 included and maybe more on the way.
You keep missing the point. I don't care about what you've included in the library. I'm implementing my own archive because what you've included doesn't do the job.
There may be one on the way that is only for output - perhaps useful for debug logging. These are not meant to be prohibited by these requirements.
If the requirements aren't strong enough to tell me how to do something useful, they're not much good.
Common Type Definitions in all Archives
This should be a section for the "Archive" concept.
From your later writing in this post you seem to have missed, ignored, or discarded this suggestion. Actually, it's stronger: an assertion. I don't believe it is optional: you need to specifically define an "Archive" concept of which Loading Archive and Saving Archive are refinements.
These are not type definitions but valid expressions.
A::is_saving An integral constant of type boost::mpl::bool_. I'm sorry to be so blunt, but that's nonsense. A::is_saving is a type, not an integral constant, and boost::mpl::bool_ is a class template, not a type. If you mean to refer to the MPL Integral Constant concept, the correct thing to say would be: A boolean MPL Integral Constant with a link to the documentation for that concept.
Value is boost::mpl::bool_<true>
OK what I meant to say is
A boolean MPL Integral Constant for which the following expression is true
boost::mpl::is_equal< A::is_loading, boost::mpl::bool_<true>
<snip>
How would you suggest I phrase this?
I believe that requirement is too strong, and it would be sufficient to say An MPL Integral Constant with a nonzero ::value member
Common Type Definitions in all Archives
Common Member Functions in all Archives
Again, these are not member functions, but valid expressions. Presumably "ar & x" can be implemented by a free function and "ar.template register_type<T>()" invokes a member function template.
I have to confess I plagerized this exact phrasing from my copy of STL Tutorial and Reference Guide - page 263.
In that case I have lost a lot of respect for that book. It was co-written by one of the founders of Generic Programming, but that might just mean he was the PhD adviser for the person who actually did the work. I suggest you try some others, like Josuttis', "The C++ Standard Library" or Austern's, "Generic Programming and the STL"
I felt it was a very close analogy and a good model. I did note that in this reference operators that could or would be implemented as free functions were included under the "wrong" heading but I presume this was done this way on purpose in order to improve clarity
Inaccuracy and inconsistency never improves clarity. It just makes me wonder what the author really meant.
and reflect the fact that most common implmentations would be member functions.
register_type is not a member function but a member function template. A template is not a function.
I could change the heading to "Common Operations for all Archives" or .. your suggestion here.
The heading should be "Archive Concept Requirements" with a note that each entry describes an expression that must be valid.
ar & x Returns a reference to ar. Appends the value of x along with other information to ar.
I think you should swap the two sentences; emphasis should be on effects and only secondarily on return value.
Again I Plagerized this form the same source as above page 264.
Doesn't make it right.
I'm not sure this is a big issue
I am. The phrasing makes it sound like the job of that expression is to return a reference to ar.
But wait. Surely this expression, with the specified semantics, doesn't have to be valid for loading archives? Are you saying that all archives have to be able to save?
You did say that points not explicitly responded to have just been fixed in accordance with your suggestion. but since I didn't make a suggestion here I can only assume that you ignored my questions. Or maybe you assume my questions to refer to the following line instead of the one above? For future reference, what I'm responding to is always above my response unless there is a colon folowing what I write. I meant: Surely the expression "ar & x" , with the semantics, "appends the value of x along with other information to ar", doesn't have to be valid for loading archives? This is the sort of thing that makes me think you're not really paying attention. Also, I just noticed "along with other information." If I'm writing an archive, how am I supposed to fulfill that requirement? Can I write any arbitrary "other information" I want? Suppose I write a zero-valued char? Suppose I don't write _any_ "other information?"
ar.template register_type<T>();
To deal with derived types not otherwise explicitly referred to, any invocation of this function must appear in both the saving AND loading archives in the same place. This is the mechanism by which saving and loading is kept in syncronization.
Sure, that's what I expected.
You should leave out "template." This is a little tricky, but that expression will never be valid in a context where A is not a dependent type. I realize you do need it when A is a dependent type, but the convention is to use the simpler notation.
Hmm - all Archives included with the library are templates.
Irrelevant.
So, removing this will be including a "valid expression" which will fail to compile on all conforming compilers.
No. some_archive_template<Whatever> ar; ar.template register_type<T>(); is ill-formed. some_archive_template<Whatever> ar; ar.register_type<T>(); is well-formed if T is a type and Whatever is a valid template argument to some_archive_template.
I user who looks here to see what the valid syntax is for invoking this operation, and uses it to get a bug will be writing to tell me the manual is wrong. I stll could use ar.register_type(& t) - but that's confusing as well. So what's a good solution here?
Do it the way I'm suggesting. If you like, add a footnote for the uninitiated explaining that when A is a dependent type, the "template" keyword may be required.
The author of an archive isn't going to be calling register_type anyway (especially not in a dependent context), so it won't help prevent him from making errors.
This is extremely curious to me. This whole section started as the "Interface for users of the serialization library". I would expect that this is one section that would be of interest to users.
If you want this to be directed only at users, take the "Concept" label off it and put that elsewhere. Go back to doing whatever you want here to informally explain archives to users, and in some other section, give me real, complete concept requirements that tell me what I need to fulfill in order to create a working archive.
register_type is essential to library users. So it has to be explained correctly right here.
The explanation I've suggested _is_ correct.
. count is an integer that can be converted to std::size_t. ^^^^^^^ no, it's an instance of any type that can be so converted.
OK Your "strong typedef" isn't an integral type. ^ did your mailer kill the newline here?
I don't see how this is related.
You're right; it's not.
Given that all archives present the same public interface, specifcation of serialization is exactly the same for all archives.
I think that should be pretty clear
Yes, it's clear. Not sure what your point is.
Are you telling me, after all this, that there are additional requirements on archives not documented in the section on Archive concepts?
No.
Maybe I've misinterpreted the contents of http://www.boost.org/libs/serialization/doc/archive_reference.html#implement... Is that not describing the _real_ requirements for a working archive?
Archive classes have other members not mentioned here.
For example, in order to use a text_oarchive, a user must invoke the appropriate constructor with the appropriate arguments.
Okay, that's reasonable, but doesn't merit a mention here; it's just confusing to do so and adds nothing.
However they are related to the internal functioning of the library and are not meant to be called by users of an archive.
Hmm - now that I read this I'm not sure what I really meant. I'll think about this.
OK.
I find this rather alarming. Haven't I made it abundantly clear that the concept documentation has to give at least a minimal and complete documentation of what's required of an Archive?
I don't believe anything so far conflicts with that.
Maybe I've been too hasty, but it seemed to me, from the lack of any requirement that the archive do something that interacts with the Serializable concept (and produce sensible semantics) and your reference to a separate section describing how to implement an archive, that you had been trying to make the "Archive Concept" section a "user-only" document, and had put the real requirements elsewhere.
Okay, I don't know what to say at this point. I'm about to give up, because I don't have the time to keep going over this. It sure seems like you want to do this your own way, rather than by following the established practices and norms. If you were just "creative" but still rigorous, it would be one thing, but it doesn't even seem like you're paying close attention to what you yourself are writing.
Actually that's not true.
I think the record on this list and CVS shows multiple versions of trying to get this right.
True, you have responded, but the record also shows that I've needed to ping you several times to keep from being ignored -- or so it seemed -- which has been discouraging.
Other changes you have suggested have been incorporated once the issues have been clarified. I have based my various versions on several known "good" models.
Maybe they're leading you astray. If you stop and think about some of the things I mentioned in this email I think you'll see that some of them just don't make sense except under the loosest, most informal interpretations.
You have said yourself that the latest version is an improvement.
What should I do?
You're doing just great Dave, just hang in there.
Well, thanks for the reassurance. I'll try.
Implementation of new archives is discussed in New Archives - Implementation. Which then goes on to describe some "input archive" idea that hasn't been defined, etc... <snip>
Assuming that "Archive Concept" is more or less resloved,
Not yet.
the question arises regarding the rest of the documentation. Basically there are two "large" sections.
Serializable Concept.
Without having looked at it recently, I'm can assume this section doesn't meet your definition of formal documentation.
I haven't looked at it yet.
I would guess this could be addressed along the lines of Archive Concept, but I do have a couple of reservations even before I look at it. Much of the documentation there explains things in terms of implementation behavior (how serialization of pointers works, etc) which doesn't seem to fit all what well with the definition of formal semantics.
The whole question of how much semantics should be in a formal reference is murky to me. It seems that the idea of formal specification is to emulate something like the specification of and algebra - not a bad place to start. But specification of the rules for an algebra makes no reference at all to its semantics - hence its wide applicability.
No, that's not true. An algebra's semantic rules describe invariants. The meanings of statements in an algebra is constrained their invariant transformations. a = b ==> b = a is semantic. Anyway, I don't think that trying to frame this question in terms of algebra is going to be helpful. Semantic requirements show up in concept definitions for entirely practical reasons.
In the case of library specification we want to role library semantics into this. It seems to me that that might obscure the whole idea in the first place. Your question - when does the "serialize" funcition get called? and my response "what does it matter" illustrate differing understandings about this.
Clearly.
I'm starting to wonder - if its really a formal definition - how can it have sematics? And if doesn't have semantics, how can it be useful?
I hate to be blunt, but I don't really care whether this meets your idea of what "formal" means. Let's not make this a philosophical argument. It's all about practical concerns.
This is the reason I was so intrigued with Joaquin's post. It touches upon what to me is the crux of the issue. I'm not sure it resolves it - but it does address the proper place of semantics in the discussion.
Joaquin's post takes an "innovative" approach to the problem of specifying semantics but it isn't at all clear to me that it holds water. The reason that "equivalent" is a fuzzy term in C++ comes down to the fact that two distinct objects always have detectably distinct addresses, so no two distinct objects can _truly_ be equivalent. Leaving aside that language corner, the idea of equivalence works perfectly well. I suggest you use that, and the established conventions from the literature, to describe semantics. You have, essentially, an emergency on your hands -- this is not the time to try untested approaches. First plug the dyke and then, if you have time, think about a rewrite.
So, I'm still thinking about what needs to be done (if anything) regarding the "Serializable" type concept section.
Don't feel obligated to coment on the above.
Too late ;-)
Achive Implementation
Here is the part of the documentation that I have been most concerned about.
I changed "Archive User Interface" to "Archive Concept" without thinking too much about it.
When was that? Maybe that was the root of the problem.
I don't remember any user having any question about this. So I wasn't too concerned.
This part of the library really describes a "tool kit" for implementing the Achive Concepts. To make an analogy to your iterator library there are two things going.
Definition of iterator (archive) concept - A user of an iterator instance need only look at this part of the documentation to use an iterator which models the concept.
And an implementor of an iterator need only look at the iterator requirements to _implement_ an iterator that models the concept. How do you think we came up with iterator_facade and iterator_adaptor, anyway? They weren't spun out of thin air: we carefully studied the iterator concept requirements.
Definintion of the iterator (archive) CRTP classes for deriving one's own iterators (archive) This is a lot harder to explain and use. I know because I did use it. I should say that I was very pleased with the results as once I got things to compile, I had very few problems.
So I've been planning to re-visit "Archive Implementation" in any case. But I don't think there is any quick fix. I just have to spend more time on it. And there are a couple of other issues. I had to add to the interface a tiny bit in order to support shared_ptr serialization. Its not a big thing but I didn't want to document it because I'm thinking its not quite right (though its good enough for shared_ptr) and I don't want anyone else to use it. There might be a couple of other things that might or might not need to be made public such as stack_construct<T>(Archive &ar).
I'm still thinking about how to handle things BOOST_SERIALIZATION_NVP. This is optional for most archives but required for xml_archives. So does this mean an refinement on the concepts already described. It would look like it.
So - I would like to:
a) resolve the final issues regarding "Archive Concept" I don't think we're at all far apart in this.
Remains to be seen.
b) ship RC_1_33_0 (again). This is important to me as I've fixed the header sequence issue as well as a nasty bug. So I would like to see the new version shipped ASAP. c) Give me some time to go back to the things I mentioned above.
Sounds fine to me.
I believe that the serialization library has proved quite useful to some people inspite of the lack of formality in its documentation and shipping of a corrected version shouldn't be held up for this.
It's not formality that's the problem: it's vagueness, inconsistency, and lack of a practical document that describes what an archive implementor needs to do.
Lastly, I know I'm driving you crazy. I'm sorry about that. Please believe me that its not intentional on my part - its just comes naturally.
:)
I do sometimes think you underestimate the depth of my thinking on some of these subjects. Perhaps I'm confused about some of these things because I think about them too much.
I'm beginning to think that's true. Maybe a little less abstract thought and a little more attention to practical detail would help.
I don't know if its any consolation, but its stressful for me as well.
I'm sure. I know I can be a really tough critic and I'm trying to restrain myself a little at least.
But your input has been very helpful and added a lot to quality of the library and its documentation and that's why I put up with it.
I sure hope you can laugh about this.
If I couldn't, it would be pretty sad. Thanks for your goodwill. -- Dave Abrahams Boost Consulting www.boost-consulting.com

David Abrahams wrote:
The point is that if one writes a program in accordance with the requirements stated here, the program will be a legal program and will compile.
But that's not enough. I'm not interested in making an archive that just compiles; I want it to have some sensible semantics. What requirements do I have to fulfill in order to make the archive actually *work*?
If you look at the iterator requirements, for example, you'll see that there are semantic requirements in place, not just syntactic ones. These requirements are needed so that the algorithms can say something about what they do. The sort() algorithm wouldn't be able to make any claims about its result if writing into a random access iterator were allowed to be a no-op.
I can see that, but then one has an ostream_iterator which is a forward iterator. It does something quite different from other forward iterators such as one associated with a std::list. I'm sure I could dig up other examples. I really did think about what one should say about the sematics. And when I considered possibilities like an archive used for data logging, it seemed that there was less and less I could say about the semantics without saying something incorrect. So I moved to saying as little as possible.
As I've said before, you will need eventually to describe the relationship between compatible loading and saving archives. It will be something like
T x, y; // arbitrary operations on x to set its state sar & x; lar & y;
Postcondition: y is equivalent to x
[Someone else suggested a complex way to describe this idea without mentioning the word "equivalent" but I've no idea whether it hangs together, and at least this is good enough for the standard]
You can't say anything like that unless loading and saving are required to do _something_.
<snip> I really couldn't give a good response in a reasonably short time.
Of course, I would expect all archive implementations to call "serialize" but maybe someone comes up with an archive implementation that just checks syntax or something like that. Someone might want to make an archive which only stored/loaded primitive types. Or someone might make an archive that was implemented solely in terms of save/load_binary. These would never call serialize.
Do not build in generality that you're not even sure is needed, especially if it costs you your ability to write concepts that tell people how to get sensible results. You can always introduce less-refined concepts that allow these kinds of things and provide weaker semantic guarantees.
My thought was not to build in any requirement that wasn't necessary.
Common Type Definitions in all Archives
This should be a section for the "Archive" concept.
That was my intention. I didn't make a subtitle "Archive Concept" because I already had the chapter titled "Archive Concept"
OK what I meant to say is
A boolean MPL Integral Constant for which the following expression is true
boost::mpl::is_equal< A::is_loading, boost::mpl::bool_<true>
<snip>
How would you suggest I phrase this?
I believe that requirement is too strong, and it would be sufficient to say
An MPL Integral Constant with a nonzero ::value member
So if somewhere in the implementation I use: boost::mpl::is_equal< A::is_loading, boost::mpl::bool_<true> but the archive has A::is_loading defined as boost::mpl::int_<4> it will still work? If you say its true, I don't doubt it. But I would have hoped for a compile time error here.
register_type is not a member function but a member function template. A template is not a function.
I could change the heading to "Common Operations for all Archives" or .. your suggestion here.
The heading should be "Archive Concept Requirements" with a note that each entry describes an expression that must be valid.
How about just "Requirements" as its already part of a section titled "Saving Archive (Concept)"?
Surely the expression "ar & x" , with the semantics, "appends the value of x along with other information to ar", doesn't have to be valid for loading archives?
whoops - OK I see that now.
This is the sort of thing that makes me think you're not really paying attention.
Well I confess the symetry and similarity of the text between the passages makes it easy to overlook this kind of stuff. This is expecially so when one has written it himself.
Also, I just noticed "along with other information." If I'm writing an archive, how am I supposed to fulfill that requirement? Can I write any arbitrary "other information" I want? Suppose I write a zero-valued char? Suppose I don't write _any_ "other information?"
This "other information" is an implementation detail of the particular archive class. The user of any archive implementing the concept doesn't have to know what other information is in the archive. I do believe I'm begining to see what the origin of this problem is. I'll address it below after a few details are out of the way.
You should leave out "template." This is a little tricky, but that expression will never be valid in a context where A is not a dependent type. I realize you do need it when A is a dependent type, but the convention is to use the simpler notation.
Hmm - all Archives included with the library are templates.
Irrelevant.
So, removing this will be including a "valid expression" which will fail to compile on all conforming compilers.
No.
some_archive_template<Whatever> ar; ar.template register_type<T>();
is ill-formed.
some_archive_template<Whatever> ar; ar.register_type<T>();
is well-formed if T is a type and Whatever is a valid template argument to some_archive_template.
I don't see this. I am looking at Appendix C.13.6 of The C++ Programming Language by Stroustrup page 858. It seems that he has our exact example there: <begin quote> class Memory { public: template<class T> T* get_new(); ... }; template<class Allocator> void f(Allocator & m){ int * p1 = m.get_new<int>(); // syntax error: int after less-than operator int * p2 = m.template get_new<int>(); // explicit qualification ... } Explicit qualification of get_new() is necesary because its template parameter cannot be deduced. In the case the template prefix must be used to inform the compiler (and the human reader) that get_new is a member template so taht explicit qualification with the desired type is possible. Without qualification with template, we would get a syntax error because < would be assumed to be the less-than operator <end quote> Some compilers don't accept the ar.template synatax so that's where I came to be where we are.
I user who looks here to see what the valid syntax is for invoking this operation, and uses it to get a bug will be writing to tell me the manual is wrong. I stll could use ar.register_type(& t) - but that's confusing as well. So what's a good solution here?
Do it the way I'm suggesting. If you like, add a footnote for the uninitiated explaining that when A is a dependent type, the "template" keyword may be required.
The author of an archive isn't going to be calling register_type anyway (especially not in a dependent context), so it won't help prevent him from making errors.
This is extremely curious to me. This whole section started as the "Interface for users of the serialization library". I would expect that this is one section that would be of interest to users.
If you want this to be directed only at users, take the "Concept" label off it and put that elsewhere. Go back to doing whatever you want here to informally explain archives to users, and in some other section, give me real, complete concept requirements that tell me what I need to fulfill in order to create a working archive.
OK !!
The explanation I've suggested _is_ correct.
Are you telling me, after all this, that there are additional requirements on archives not documented in the section on Archive concepts?
No.
Maybe I've misinterpreted the contents of http://www.boost.org/libs/serialization/doc/archive_reference.html#implement...
Is that not describing the _real_ requirements for a working archive?
Archive classes have other members not mentioned here.
For example, in order to use a text_oarchive, a user must invoke the appropriate constructor with the appropriate arguments.
Okay, that's reasonable, but doesn't merit a mention here; it's just confusing to do so and adds nothing.
However they are related to the internal functioning of the library and are not meant to be called by users of an archive.
Hmm - now that I read this I'm not sure what I really meant. I'll think about this.
OK.
I find this rather alarming. Haven't I made it abundantly clear that the concept documentation has to give at least a minimal and complete documentation of what's required of an Archive?
I don't believe anything so far conflicts with that.
Maybe I've been too hasty, but it seemed to me, from the lack of any requirement that the archive do something that interacts with the Serializable concept (and produce sensible semantics) and your reference to a separate section describing how to implement an archive, that you had been trying to make the "Archive Concept" section a "user-only" document, and had put the real requirements elsewhere.
correct. But I didn't think of them as "real requirements", I thought of them as "implementation details" from the perspective of the user.
True, you have responded, but the record also shows that I've needed to ping you several times to keep from being ignored -- or so it seemed -- which has been discouraging.
Its hard to respond as quickly as you would like. A lot of question require that I reflect on why I originally did something a certain way. I didn't necessarily write it down because I can't know ahead of time which things are going have to be given this kind of treatment. On the other hand for the poster its easy to say "Why don't you do X" but to respond can take a lot of time thinking through all the implications and re-discovering why one didn't do it in the first place. In the recent case handling two-phase lookup I did that in february and in this case I think it was May. And I do have other things going on besides the serialization library.
Maybe they're leading you astray. If you stop and think about some of the things I mentioned in this email I think you'll see that some of them just don't make sense except under the loosest, most informal interpretations.
No, that's not true. An algebra's semantic rules describe invariants. The meanings of statements in an algebra is constrained their invariant transformations.
a = b ==> b = a
is semantic.
As much as I'd like to respond to that it would lead us too far astray
In the case of library specification we want to role library semantics into this. It seems to me that that might obscure the whole idea in the first place. Your question - when does the "serialize" funcition get called? and my response "what does it matter" illustrate differing understandings about this.
Clearly.
It's all about practical concerns.
I'll go with that
This is the reason I was so intrigued with Joaquin's post. It touches upon what to me is the crux of the issue. I'm not sure it resolves it - but it does address the proper place of semantics in the discussion.
First plug the dyke and then, if you have time, think about a rewrite.
see below
I changed "Archive User Interface" to "Archive Concept" without thinking too much about it.
When was that? Maybe that was the root of the problem.
That was several months ago. No one seemed to notice - or care until now.
It's not formality that's the problem: it's vagueness, inconsistency, and lack of a practical document that describes what an archive implementor needs to do.
Maybe - But I haven't yet heard from anyone who wants to do this but failed because of this. Anyway I see now that my view of the role of different sections of the documentation is way out of sync with what you expect. To summarize my thiniking was a) "Archive Concept" defines what a user has to know in order to make a type a serializable type. That is what expressions are valid on an Archive. This started as basically a class interface in the traditional C++ documentation model. It seemed a natural generalization and formalization to move it to a "Concept" as a list of valid expressions. This fixed the problem that it really wasn't a class. b) Given a), "Serializable Concept" defines what a type must have (requiremetns) in order to be serializable. This is not stated in a formal table. In fact its somewhat different in that there are several disjoint sets of requirements that a type can fullfill and still be "Serializable" c) "Archive Implementation" would describe how a) was implemented and how to leverage on the included code. This is a very informal narrative. This also started out as basically notes on a class implementation. This kind of broke down as things got factored into interface and implementation from which template classes are derived using CRTP. This may well lend itself to a formal treatment but its nowhere near that now. I never saw documentation for any thing like this. I always found that disconcerting. But now I feel better about it. So I think I made a big mistake when you first raised the issue. (this is why I hate to be pressured to respond too soon!) I think what I should have done and what we should do now is: a) change the title of the section "Archive Concept" back to "Archive User Interface" or "Archive Interface". and restore the original content which is based of function prototypes. b) change the title of the section titled "Serializable Concept" back to "Serializable types". c) The question of formal documentation for the serialization library can be considered at leisure. Robert Ramey

"Robert Ramey" <ramey@rrsd.com> writes:
David Abrahams wrote:
The point is that if one writes a program in accordance with the requirements stated here, the program will be a legal program and will compile.
But that's not enough. I'm not interested in making an archive that just compiles; I want it to have some sensible semantics. What requirements do I have to fulfill in order to make the archive actually *work*?
If you look at the iterator requirements, for example, you'll see that there are semantic requirements in place, not just syntactic ones. These requirements are needed so that the algorithms can say something about what they do. The sort() algorithm wouldn't be able to make any claims about its result if writing into a random access iterator were allowed to be a no-op.
I can see that, but then one has an ostream_iterator which is a forward iterator.
No, it's an output iterator.
It does something quite different from other forward iterators such as one associated with a std::list.
No surprise, since it's not a forward iterator. It doesn't need to meet the semantic guarantees of forward iterator. It still meets the semantic guarantees of output iterator.
I'm sure I could dig up other examples. I really did think about what one should say about the sematics. And when I considered possibilities like an archive used for data logging, it seemed that there was less and less I could say about the semantics without saying something incorrect. So I moved to saying as little as possible.
As I've said, that's not very useful.
Of course, I would expect all archive implementations to call "serialize" but maybe someone comes up with an archive implementation that just checks syntax or something like that. Someone might want to make an archive which only stored/loaded primitive types. Or someone might make an archive that was implemented solely in terms of save/load_binary. These would never call serialize.
Do not build in generality that you're not even sure is needed, especially if it costs you your ability to write concepts that tell people how to get sensible results. You can always introduce less-refined concepts that allow these kinds of things and provide weaker semantic guarantees.
My thought was not to build in any requirement that wasn't necessary.
Those requirements _are_ necessary if you want loading and saving to do anything reasonable.
Common Type Definitions in all Archives
This should be a section for the "Archive" concept.
That was my intention. I didn't make a subtitle "Archive Concept" because I already had the chapter titled "Archive Concept"
Call the chapter "Archive Concepts," then.
OK what I meant to say is
A boolean MPL Integral Constant for which the following expression is true
boost::mpl::is_equal< A::is_loading, boost::mpl::bool_<true>
<snip>
How would you suggest I phrase this?
I believe that requirement is too strong, and it would be sufficient to say
An MPL Integral Constant with a nonzero ::value member
So if somewhere in the implementation I use:
boost::mpl::is_equal< A::is_loading, boost::mpl::bool_<true>
I don't know why you would ever do that when you could just use A::is_loading directly, and there is no mpl::is_equal. I assume you mean equal_to.
but the archive has A::is_loading defined as boost::mpl::int_<4> it will still work? If you say its true, I don't doubt it. But I would have hoped for a compile time error here.
In that case http://www.boost.org/libs/mpl/doc/refmanual/equal-to.html are pretty clear on semantics; you can look this up as easily as I can. If you really want to use that test you have to say An MPL Integral Constant with a ::value member equal to true
register_type is not a member function but a member function template. A template is not a function.
I could change the heading to "Common Operations for all Archives" or .. your suggestion here.
The heading should be "Archive Concept Requirements" with a note that each entry describes an expression that must be valid.
How about just "Requirements" as its already part of a section titled "Saving Archive (Concept)"?
Well, as you have pointed out, register_type is required for loading archives too.
Also, I just noticed "along with other information." If I'm writing an archive, how am I supposed to fulfill that requirement? Can I write any arbitrary "other information" I want? Suppose I write a zero-valued char? Suppose I don't write _any_ "other information?"
This "other information" is an implementation detail of the particular archive class.
Then it's not a requirement, right?
The user of any archive implementing the concept doesn't have to know what other information is in the archive.
Irrelevant.
I do believe I'm begining to see what the origin of this problem is. I'll address it below after a few details are out of the way.
You should leave out "template." This is a little tricky, but that expression will never be valid in a context where A is not a dependent type. I realize you do need it when A is a dependent type, but the convention is to use the simpler notation.
Hmm - all Archives included with the library are templates.
Irrelevant.
And incorrect AFAICT. http://www.boost.org/libs/serialization/doc/archives.html#archive_models shows a bunch of non-template archive models, doesn't it? Oh, no it doesn't, those are just freestanding constructor signatures, which would be invalid if written that way. Ugh. Okay, so...
So, removing this will be including a "valid expression" which will fail to compile on all conforming compilers.
No.
some_archive_template<Whatever> ar; ar.template register_type<T>();
is ill-formed.
some_archive_template<Whatever> ar; ar.register_type<T>();
is well-formed if T is a type and Whatever is a valid template argument to some_archive_template.
I don't see this. I am looking at Appendix C.13.6 of The C++ Programming Language by Stroustrup page 858. It seems that he has our exact example there:
<begin quote> class Memory { public: template<class T> T* get_new(); ... };
template<class Allocator> void f(Allocator & m){ int * p1 = m.get_new<int>(); // syntax error: int after less-than operator int * p2 = m.template get_new<int>(); // explicit qualification ... }
That's a context in which Allocator is a dependent type. Let me be perfectly clear. Try compiling this: struct foo {}; void f() { boost::serialization::text_oarchive<std::ofstream> ar; ar.template register_type<foo>(); } On a conforming compiler, it will fail. Now remove the template keyword and try again.
I find this rather alarming. Haven't I made it abundantly clear that the concept documentation has to give at least a minimal and complete documentation of what's required of an Archive?
I don't believe anything so far conflicts with that.
Maybe I've been too hasty, but it seemed to me, from the lack of any requirement that the archive do something that interacts with the Serializable concept (and produce sensible semantics) and your reference to a separate section describing how to implement an archive, that you had been trying to make the "Archive Concept" section a "user-only" document, and had put the real requirements elsewhere.
correct. But I didn't think of them as "real requirements", I thought of them as "implementation details" from the perspective of the user.
Are they requirements or aren't they? It would be conceptually purer if a model of the Archive concept wasn't required to be derived from a library-provided class template, but I could live with that derivation requirement if the other requirements you specified were complete and meaningful.
An algebra's semantic rules describe invariants. The meanings of statements in an algebra is constrained their invariant transformations.
a = b ==> b = a
is semantic.
As much as I'd like to respond to that it would lead us too far astray
Good; I don't want to talk about algebras anyway.
It's not formality that's the problem: it's vagueness, inconsistency, and lack of a practical document that describes what an archive implementor needs to do.
Maybe - But I haven't yet heard from anyone who wants to do this but failed because of this.
Matthias did. He only succeeded after getting information from you that wasn't present in the docs.
I think what I should have done and what we should do now is:
a) change the title of the section "Archive Concept" back to "Archive User Interface" or "Archive Interface". and restore the original content which is based of function prototypes. b) change the title of the section titled "Serializable Concept" back to "Serializable types". c) The question of formal documentation for the serialization library can be considered at leisure.
Grrr! You're very close to having reasonable concept documentation now; I don't know why you'd throw that out. As I've said, "formal" documentation isn't the issue; the question is whether the docs can be practically used by someone who wants to build a new archive type. -- Dave Abrahams Boost Consulting www.boost-consulting.com

David Abrahams wrote:
That's a context in which Allocator is a dependent type. Let me be perfectly clear. Try compiling this:
struct foo {}; void f() { boost::serialization::text_oarchive<std::ofstream> ar; ar.template register_type<foo>(); }
On a conforming compiler, it will fail. Now remove the template keyword and try again.
OK, I see this now. But I don't think the example above represents common or useful usage. But it turns out there are several places within the library where the ar really is a template. One place is within the implementation of serialization. This is would be no big deal to me as its inside the implementation. There are examples and tests and I'm sure user programs which do the following. struct X {}; struct Y : public X {}; template<class Archive> void serialize(Archive & ar, X & x, const unsigned int version){ ar.register_type<Y>(); // will fail on any conforming compiler ar.template register_type<Y>(); // will compile on a conforming compler - will fail on others ar.register_type(static_cast<Y *>(NULL)); // works everywhere. } Of course a user could do void serialize(boost::serialization::text_oarchive<std::ofstream> & ar, X & x, const unsigned int version){ ar.register_type<Y>(); // compiles fine ar.template register_type<Y>(); // fails to compile ar.register_type(static_cast<Y *>(NULL)); // works everywhere. } But I would argue that he is needlessly giving up generality. So my advice to user it to always use the third choice above so that he can just forget about it an move on.
Maybe I've been too hasty, but it seemed to me, from the lack of any requirement that the archive do something that interacts with the Serializable concept (and produce sensible semantics) and your reference to a separate section describing how to implement an archive, that you had been trying to make the "Archive Concept" section a "user-only" document, and had put the real requirements elsewhere.
correct. But I didn't think of them as "real requirements", I thought of them as "implementation details" from the perspective of the user.
Are they requirements or aren't they? It would be conceptually purer if a model of the Archive concept wasn't required to be derived from a library-provided class template, but I could live with that derivation requirement if the other requirements you specified were complete and meaningful.
Again here is the crux of the problem. The information in the section is not "real requirements". It is description of how I implemented the Archive Concept(s) as decribed in the first section. It is here that the sematics are really defined. And of course the exact semantics vary according to the particular archive. For example, the xml_archives include a tag with the variable name - other archives don't. However they all implement the Archive Concept(s). So again here is how I came here. The main task for the user of the library is to make his types "Serializable". Of necessity, this requires a description of the interface to an "Achive" class which is normally passed as a template. The in turn requires that the "Archive Concept(s)" be defined so that the user may know what operations he can invoke on the "Archive" class passed as an argument. The SOLE function of the "Archive Concept" documentation is to enumerate those operations. This is all that is required for a user to write a "serialize" function which he can know with certainty will function with any conforming implementation. Now comes the problem of how to implement an archive which implements the concept. Of course it must support the enumerated operations. Hopefully it does something useful, but that's an issue totally separate from the definition of the Archive Concept. The section titled "Archive Implementation" describes how I managed to make a set of archives which fullfilled the requirement of the "Archive Concept" They are not part of the Archive Concept itself. There is no requirement that an Archive implementor use any of the information in this section or any of the archive code included in the library.
Matthias did. [failed to make a new archive] He only succeeded after getting information from you that wasn't present in the docs.
But this is not due to the problems of "Archive Concept(s). Mattias leveraged on my implementation of other archives. And he did manage to do it with very little help from me. (I assume it wasn't much as I don't specifically remember getting any quesitons from him). FWIW other users have made thier own archives based on the information in this section - I think there is one even loaded to the vault. In any case, I've acknowledged that the section "Archive Implementation" is not satisfactory as it stands for the purpose for which it was intended. I've looked at it in terms of formal concepts and in light of the iterator library but still have not been able to formalize in a way that would be accurate and useful. This will take some time.
I think what I should have done and what we should do now is:
a) change the title of the section "Archive Concept" back to "Archive User Interface" or "Archive Interface". and restore the original content which is based of function prototypes. b) change the title of the section titled "Serializable Concept" back to "Serializable types". c) The question of formal documentation for the serialization library can be considered at leisure.
Grrr!
You're very close to having reasonable concept documentation now; I don't know why you'd throw that out.
I see now we're much farther apart than I thought we were.
As I've said, "formal" documentation isn't the issue; the question is whether the docs can be practically used by someone who wants to build a new archive type.
In order of frequence of usage, I see the documents as being used to describe how to: a) make one's types serializable. ("Archive Concepts" and "Serializable Concept") b) use archives which are included. ("Archive Models") c) make one's own archive implementation leveraging on the code included. ("Archive Concept" and "Archive Implementation") d) make one's own archive implementation from scratch. No one has ever attempted this. ("Archive Concept") I realize you don't buy this and expect to see information which is included in "Archive Implementation" moved to "Archive Concepts". But in my view that muddles the whole functional organization which I was so careful to preserve above. Its been this way since the beginning. What's changed is that the descriptions are couched in terms of "Concept" and "valid expressions" rather than function prototypes a before. At this point part of the documentation uses one approach and other parts use the other and this is confusing. That's why I would like to go back to were we started and just elmininate the terms "Concept" which imply a method which hasn't been used in a consistent and correct way throughout the document. I've also come to believe that the notion of "Semantics" is quite subjective and needs to be thought through more. So that is the basis of my suggestion for roling things back to the begining and just removing the word "Concept" as its use isn't really correct in that context. Robert Ramey

"Robert Ramey" <ramey@rrsd.com> writes:
David Abrahams wrote:
That's a context in which Allocator is a dependent type. Let me be perfectly clear. Try compiling this:
struct foo {}; void f() { boost::serialization::text_oarchive<std::ofstream> ar; ar.template register_type<foo>(); }
On a conforming compiler, it will fail. Now remove the template keyword and try again.
OK, I see this now. But I don't think the example above represents common or useful usage.
But it turns out there are several places within the library where the ar really is a template.
Irrelevant, as I've said many times. ar is a template in the example I gave above. I did that specifically to dispel this misconception.
There are examples and tests and I'm sure user programs which do the following.
struct X {}; struct Y : public X {};
template<class Archive> void serialize(Archive & ar, X & x, const unsigned int version){ ar.register_type<Y>(); // will fail on any conforming compiler ar.template register_type<Y>(); // will compile on a conforming compler - will fail on others ar.register_type(static_cast<Y *>(NULL)); // works everywhere. }
Yes.
Of course a user could do
void serialize(boost::serialization::text_oarchive<std::ofstream> & ar, X & x, const unsigned int version){ ar.register_type<Y>(); // compiles fine ar.template register_type<Y>(); // fails to compile ar.register_type(static_cast<Y *>(NULL)); // works everywhere. }
But I would argue that he is needlessly giving up generality.
Yes.
So my advice to user it to always use the third choice above so that he can just forget about it an move on.
I think (Y*)0 would be a lot cleaner, but it's up to you what you want to recommend.
Maybe I've been too hasty, but it seemed to me, from the lack of any requirement that the archive do something that interacts with the Serializable concept (and produce sensible semantics) and your reference to a separate section describing how to implement an archive, that you had been trying to make the "Archive Concept" section a "user-only" document, and had put the real requirements elsewhere.
correct. But I didn't think of them as "real requirements", I thought of them as "implementation details" from the perspective of the user.
Are they requirements or aren't they? It would be conceptually purer if a model of the Archive concept wasn't required to be derived from a library-provided class template, but I could live with that derivation requirement if the other requirements you specified were complete and meaningful.
Again here is the crux of the problem. The information in the section is not "real requirements".
Okay, then they're not requirements. This isn't complicated. It's not even a problem with a crux. :) -- Dave Abrahams Boost Consulting www.boost-consulting.com

"Robert Ramey" <ramey@rrsd.com> writes:
In order of frequence of usage, I see the documents as being used to describe how to:
a) make one's types serializable. ("Archive Concepts" and "Serializable Concept")
b) use archives which are included. ("Archive Models")
c) make one's own archive implementation leveraging on the code included. ("Archive Concept" and "Archive Implementation")
d) make one's own archive implementation from scratch. No one has ever attempted this. ("Archive Concept")
That's exactly right.
I realize you don't buy this
???
and expect to see information which is included in "Archive Implementation" moved to "Archive Concepts".
Certainly not, unless "Archive Implementation" contains requirements for archives. You haven't been very clear about whether it does or not, but I think your last statement is that it doesn't.
But in my view that muddles the whole functional organization which I was so careful to preserve above. Its been this way since the beginning.
What's changed is that the descriptions are couched in terms of "Concept" and "valid expressions" rather than function prototypes a before. At this point part of the documentation uses one approach and other parts use the other and this is confusing.
?? Concepts aren't supposed to be documented in the same way as classes. There's nothing confusing about that.
That's why I would like to go back to were we started and just elmininate the terms "Concept"
Terms?
which imply a method
Method?
which hasn't been used in a consistent and correct way throughout the document.
I've also come to believe that the notion of "Semantics" is quite subjective and needs to be thought through more.
Ugh.
So that is the basis of my suggestion for roling things back to the begining and just removing the word "Concept" as its use isn't really correct in that context.
Finally, and recently, it does seem correct, but the concepts are just a little too weak to be useful. It needs to give some semantic guarantees. Or, more accurately, you need an additional two-type concept that associates loading and saving archives with one another. -- Dave Abrahams Boost Consulting www.boost-consulting.com

David Abrahams wrote:
"Robert Ramey" <ramey@rrsd.com> writes:
In order of frequence of usage, I see the documents as being used to describe how to:
a) make one's types serializable. ("Archive Concepts" and "Serializable Concept")
b) use archives which are included. ("Archive Models")
c) make one's own archive implementation leveraging on the code included. ("Archive Concept" and "Archive Implementation")
d) make one's own archive implementation from scratch. No one has ever attempted this. ("Archive Concept")
That's exactly right.
I realize you don't buy this
???
and expect to see information which is included in "Archive Implementation" moved to "Archive Concepts".
Certainly not, unless "Archive Implementation" contains requirements for archives. You haven't been very clear about whether it does or not, but I think your last statement is that it doesn't.
But in my view that muddles the whole functional organization which I was so careful to preserve above. Its been this way since the beginning.
What's changed is that the descriptions are couched in terms of "Concept" and "valid expressions" rather than function prototypes a before. At this point part of the documentation uses one approach and other parts use the other and this is confusing.
?? Concepts aren't supposed to be documented in the same way as classes. There's nothing confusing about that.
That's why I would like to go back to were we started and just elmininate the terms "Concept"
Terms?
term
which imply a method
Method?
of documentation
which hasn't been used in a consistent and correct way throughout the document.
I've also come to believe that the notion of "Semantics" is quite subjective and needs to be thought through more.
Ugh.
So that is the basis of my suggestion for roling things back to the begining and just removing the word "Concept" as its use isn't really correct in that context.
Finally, and recently, it does seem correct, but the concepts are just a little too weak to be useful. It needs to give some semantic guarantees. Or, more accurately, you need an additional two-type concept that associates loading and saving archives with one another.
and its not that obvious to me how to do that. Especially on short notice which is my view of we're dealing with here. There is another concern that I have. In spite of all of the above, the part of the documentation which I think needs the most work is "Archive Implementation". This is fairly complicated undertaking. I do suspect that it will result in a refinement of "Archive Concept" as this task progresses. Robert Ramey

David Abrahams <dave <at> boost-consulting.com> writes: [...]
As I've said before, you will need eventually to describe the relationship between compatible loading and saving archives. It will be something like
T x, y; // arbitrary operations on x to set its state sar & x; lar & y;
Postcondition: y is equivalent to x
[...]
Joaquin's post takes an "innovative" approach to the problem of specifying semantics but it isn't at all clear to me that it holds water.
I can do little to argument against that criticism. If you have specific concerns about the approach please do bring them here.
The reason that "equivalent" is a fuzzy term in C++ comes down to the fact that two distinct objects always have detectably distinct addresses, so no two distinct objects can _truly_ be equivalent.
As far I know, the only definitions for (object) equivalence in the standard are given in connection with strict weak orderings induced by comparison functors. Beside that, I failed to find any reference about what two objects being "equivalent" means.
Leaving aside that language corner, the idea of equivalence works perfectly well.
For the sake of the discussion, let's assume that "a and b are equivalent" is somehow defined as / related to "a==b". My thesis is that there are serious objections against this definition of equivalence in the context of serialization: 1. A serializable type need not be equality comparable. 2. "a==b" is a C++ expression, so implying that a and b are objects living inside the same program. If I save an object a on my PC, pass the file to you and you load it a year later as b on your Linux box, what is "a==b" supposed to mean? 3. A serializable type can be implemented without observing the "a==b" rule: for instance, a list-like container can load the elements in reverse order --I understand this is a perfectly legitimate implementation that shouldn't be banned because of the "a==b" restriction. One can argue that (1) and (2) can be overcome with a "fuzzier" definition of equivalence relying on the reader's intuition about this relationship, but (3), IMHO, breaks down any hope of attaching equivalence to serialization semantics: ultimately, archives are not responsible for holding the equivalence rule, as they relay to user provided serialize() functions. So, from my point of view, the real task of an input/output archive pair is to ensure that, when a T::serialize function is invoked on loading, the input context (i.e, permissible >> ops on the input archive) is a replica of the output sequence. This rule recursively descends to primitive (in the serialization sense) types, where an equivalence rule can actually be provided. My (skectchy) proposal is merely a formalization of this idea.
I suggest you use that, and the established conventions from the literature, to describe semantics. You have, essentially, an emergency on your hands -- this is not the time to try untested approaches. First plug the dyke and then, if you have time, think about a rewrite.
Without wanting to sound harsh, I think that what you propose as established conventions for describing serialization semantics hold little real information and, worse yet, can mislead readers to assume that Boost.Serialization is constrained by the equivalence rule when it is not (cf. point 3. above.) The current docs are better in this respect since at least they don't assert false semantic rules. Best regards, Joaquín M López Muñoz Telefónica, Investigación y Desarrollo

Joaquin M Lopez Munoz <joaquin@tid.es> writes:
David Abrahams <dave <at> boost-consulting.com> writes:
[...]
As I've said before, you will need eventually to describe the relationship between compatible loading and saving archives. It will be something like
T x, y; // arbitrary operations on x to set its state sar & x; lar & y;
Postcondition: y is equivalent to x
[...]
Joaquin's post takes an "innovative" approach to the problem of specifying semantics but it isn't at all clear to me that it holds water.
I can do little to argument against that criticism. If you have specific concerns about the approach please do bring them here.
It's not intended to be a criticism, and no offense was intended. Probably the quotation marks were misplaced, so, sorry for that. I didn't have time to evaluate what you wrote, so I really have no idea whether it holds water. That said, instinct tells me there's no way to formulate this issue that really avoids the issue that no two distinct objects in C++ are truly equivalent.
The reason that "equivalent" is a fuzzy term in C++ comes down to the fact that two distinct objects always have detectably distinct addresses, so no two distinct objects can _truly_ be equivalent.
As far I know, the only definitions for (object) equivalence in the standard are given in connection with strict weak orderings induced by comparison functors. Beside that, I failed to find any reference about what two objects being "equivalent" means.
Correct, it is not defined. You're expected to understand it because it's a plain english word.
Leaving aside that language corner, the idea of equivalence works perfectly well.
For the sake of the discussion, let's assume that "a and b are equivalent" is somehow defined as / related to "a==b".
Well, that would be one convenient definition, but since lots of types aren't even syntactically EqualityComparable, that isn't much help. "Assuming" it basically skirts the equivalence problem.
My thesis is that there are serious objections against this definition of equivalence in the context of serialization:
1. A serializable type need not be equality comparable.
Hey, sounds familiar!
2. "a==b" is a C++ expression, so implying that a and b are objects living inside the same program. If I save an object a on my PC, pass the file to you and you load it a year later as b on your Linux box, what is "a==b" supposed to mean?
Exactly.
3. A serializable type can be implemented without observing the "a==b" rule: for instance, a list-like container can load the elements in reverse order --I understand this is a perfectly legitimate implementation that shouldn't be banned because of the "a==b" restriction.
I'm not sure it should be considered legit under any Archive concept that will be defined by the library. Is it a useful semantics? Beware premature generalization!
One can argue that (1) and (2) can be overcome with a "fuzzier" definition of equivalence relying on the reader's intuition about this relationship, but (3), IMHO, breaks down any hope of attaching equivalence to serialization semantics
Only if you think (3) is important. And if you do, as I wrote elsewhere, you can always make a weaker concept than Archive, that allows (3).
ultimately, archives are not responsible for holding the equivalence rule,
It doesn't matter whether they're _ultimately_ responsible, if Serializable also gives sensible guarantees.
as they relay to user provided serialize() functions.
But that's not what Robert is saying; he's saying they don't have to even do that!
So, from my point of view, the real task of an input/output archive pair is to ensure that, when a T::serialize function is invoked on loading, the input context (i.e, permissible >> ops on the input archive) is a replica of the output sequence.
This rule recursively descends to primitive (in the serialization sense) types, where an equivalence rule can actually be provided. My (skectchy) proposal is merely a formalization of this idea.
That's an interesting rule. So essentially you are saying that the output archive needs to record enough structure to ensure that the input archive can read the same sequence of types? What if the user serializes an aggregate struct X containing two ints? Is the corresponding input archive required to be able to read two ints as part of reading an X?
I suggest you use that, and the established conventions from the literature, to describe semantics. You have, essentially, an emergency on your hands -- this is not the time to try untested approaches. First plug the dyke and then, if you have time, think about a rewrite.
Without wanting to sound harsh, I think that what you propose as established conventions for describing serialization semantics hold little real information and, worse yet, can mislead readers to assume that Boost.Serialization is constrained by the equivalence rule when it is not (cf. point 3. above.) The current docs are better in this respect since at least they don't assert false semantic rules.
I guess it depends whether you want something useful or just something minimally restrictive. Equivalence, even if not defined in the standard, is still useful. If we took out requirements such as If a==b and (a,b) is in the domain of == then *a is equivalent to *b. from the input iterator requrements, and t = u T& t is equivalent to u from the assignable requirements, I assert that input iterators and the algorithms would be much less useful. That said, I find your approach interesting. My instinct about the need for equivalence here might be wrong, although I would still need almost every Serializable type to provide an equivalence guarantee. -- Dave Abrahams Boost Consulting www.boost-consulting.com

David Abrahams ha escrito:
Joaquin M Lopez Munoz <joaquin@tid.es> writes:
David Abrahams <dave <at> boost-consulting.com> writes:
[...]
As I've said before, you will need eventually to describe the relationship between compatible loading and saving archives. It will be something like
T x, y; // arbitrary operations on x to set its state sar & x; lar & y;
Postcondition: y is equivalent to x
[...]
Joaquin's post takes an "innovative" approach to the problem of specifying semantics but it isn't at all clear to me that it holds water.
I can do little to argument against that criticism. If you have specific concerns about the approach please do bring them here.
It's not intended to be a criticism, and no offense was intended. Probably the quotation marks were misplaced, so, sorry for that. I didn't have time to evaluate what you wrote, so I really have no idea whether it holds water. That said, instinct tells me there's no way to formulate this issue that really avoids the issue that no two distinct objects in C++ are truly equivalent.
The reason that "equivalent" is a fuzzy term in C++ comes down to the fact that two distinct objects always have detectably distinct addresses, so no two distinct objects can _truly_ be equivalent.
As far I know, the only definitions for (object) equivalence in the standard are given in connection with strict weak orderings induced by comparison functors. Beside that, I failed to find any reference about what two objects being "equivalent" means.
Correct, it is not defined. You're expected to understand it because it's a plain english word.
Leaving aside that language corner, the idea of equivalence works perfectly well.
For the sake of the discussion, let's assume that "a and b are equivalent" is somehow defined as / related to "a==b".
Well, that would be one convenient definition, but since lots of types aren't even syntactically EqualityComparable, that isn't much help. "Assuming" it basically skirts the equivalence problem.
My thesis is that there are serious objections against this definition of equivalence in the context of serialization:
1. A serializable type need not be equality comparable.
Hey, sounds familiar!
2. "a==b" is a C++ expression, so implying that a and b are objects living inside the same program. If I save an object a on my PC, pass the file to you and you load it a year later as b on your Linux box, what is "a==b" supposed to mean?
Exactly.
3. A serializable type can be implemented without observing the "a==b" rule: for instance, a list-like container can load the elements in reverse order --I understand this is a perfectly legitimate implementation that shouldn't be banned because of the "a==b" restriction.
I'm not sure it should be considered legit under any Archive concept that will be defined by the library. Is it a useful semantics? Beware premature generalization!
In my serialization stuff for Boost.MultiIndex I actually have a serializable type that does not conform to the equivalence rule. Its layout kinda looks like: template<typename Value> struct node { value v; template<class Archive> void serialize(Archive& ar,const unsigned int) { // do nothing } } I use this weird construct to make node trackable, but no contents information is dumped to the archive (that is taken care of somewhere else in the program). In case you're curious, this arises in connection with serialization of iterators. So, yes, there are actual uses of serialization not conforming to the equivalence rule. I guess one can also figure out other possible scenarios breaking the equivalence rule, like for instance a struct where some fields are serialized whereas others are local.
One can argue that (1) and (2) can be overcome with a "fuzzier" definition of equivalence relying on the reader's intuition about this relationship, but (3), IMHO, breaks down any hope of attaching equivalence to serialization semantics
Only if you think (3) is important. And if you do, as I wrote elsewhere, you can always make a weaker concept than Archive, that allows (3).
ultimately, archives are not responsible for holding the equivalence rule,
It doesn't matter whether they're _ultimately_ responsible, if Serializable also gives sensible guarantees.
This can lead to circular definitions, see my last paragraph in this post.
as they relay to user provided serialize() functions.
But that's not what Robert is saying; he's saying they don't have to even do that!
IMHO an archive should guarantee that loading/saving an UDT executes the associated load/save functions. Failing to do would devoid the Archive concept of most useful purposes. A do-nothing archive (i.e the logging example) could be covered by a more relaxed concept, if someone finds that useful.
So, from my point of view, the real task of an input/output archive pair is to ensure that, when a T::serialize function is invoked on loading, the input context (i.e, permissible >> ops on the input archive) is a replica of the output sequence.
This rule recursively descends to primitive (in the serialization sense) types, where an equivalence rule can actually be provided. My (skectchy) proposal is merely a formalization of this idea.
That's an interesting rule. So essentially you are saying that the output archive needs to record enough structure to ensure that the input archive can read the same sequence of types?
Yes.
What if the user serializes an aggregate struct X containing two ints? Is the corresponding input archive required to be able to read two ints as part of reading an X?
Not only that: X::save is actually *required* to load those two ints. Consider the following sample: #include <boost/config.hpp> #include <boost/archive/text_oarchive.hpp> #include <boost/archive/text_iarchive.hpp> #include <iostream> #include <sstream> struct foo { foo(int a=0,int b=0):a(a),b(b){} int a,b; BOOST_SERIALIZATION_SPLIT_MEMBER() template<class Archive> void save(Archive& ar,const unsigned int)const { ar<<a; ar<<b; } template<class Archive> void load(Archive& ar,const unsigned int) { ar>>a; // we do not load b!! } }; int main() { const foo x0(1,2),x1(3,4); std::ostringstream oss; { boost::archive::text_oarchive oa(oss); oa<<x0; oa<<x1; } foo y0,y1; std::istringstream iss(oss.str()); boost::archive::text_iarchive ia(iss); ia>>y0; ia>>y1; std::cout<<"y0.a="<<y0.a<<std::endl; std::cout<<"y1.a="<<y1.a<<std::endl; return 0; } Note that foo::save only loads the first int. The program outputs y0.a=1 y1.a=2 which is incorrect (y1.a should be 3), so serialization of foo is not correctly implemented. For XML archive types my hunch is that the program would throw.
I suggest you use that, and the established conventions from the literature, to describe semantics. You have, essentially, an emergency on your hands -- this is not the time to try untested approaches. First plug the dyke and then, if you have time, think about a rewrite.
Without wanting to sound harsh, I think that what you propose as established conventions for describing serialization semantics hold little real information and, worse yet, can mislead readers to assume that Boost.Serialization is constrained by the equivalence rule when it is not (cf. point 3. above.) The current docs are better in this respect since at least they don't assert false semantic rules.
I guess it depends whether you want something useful or just something minimally restrictive. Equivalence, even if not defined in the standard, is still useful. If we took out requirements such as
If a==b and (a,b) is in the domain of == then *a is equivalent to *b.
from the input iterator requrements, and
t = u T& t is equivalent to u
from the assignable requirements, I assert that input iterators and the algorithms would be much less useful.
Well, of course users of Boost.Serialization (specially if they do not write any serialize function of their own but merely use serialization capabilities of 3rd party types) expect this fuzzy equivalence rule to be held. My point is that meeting that expectation is up to each serializable type implementer, and shouldn't be enforced by the concepts section. If Robert does not have the time/will to pursue a more formal approach, I think the equivalence rule could be relaxed to something like: T x, y; // arbitrary operations on x to set its state sar & x; lar & y; Postconditions: *For primitive serializable types, y is equivalent to x. *For pointer types, bla bla *Other types are expected to implement serialization in such a manner that y is equivalent to x, but this is not guaranteed. As an "intuition oriented" guide this does not harm, but please note that the former does not define what a compatible input/ouput archive pair is: archive compatibility is defined in terms of object equivalence, and object equivalence relies on archive compatibility, we got a circularity here. The recursively descending approach breaks that vicious circle --if I'm not missing something. To sum it up, what I propose, in nonformalese, is: * An input archive iar is compatible with an output archive oar if 1. iar allows a sequence of >> ops matching the corresponding << ops made upon oar (matching defined in terms of types involved and nesting depth of the call.) 2. For primitive serialization types, the restored copies are equivalent to their original (expand on this, specially with respect to pointers.) * A type T is serializable if it is primitive serializable or else it defines the appropriate serialize (load/save) function such that the sequence of >> ops in load() match the << ops in save(). [This is not a requirement] For each serializable type, the implementor can define "equivalence" in terms of its constituent types. For instance, for std::vector: Given a std::vector<T> out, where T is serializable, and a restored copy in, then in(i).size()==out(i).size() and each in(i)[j] is a restored copy of out(i)[j]. Sorry for the long post. Best, Joaquín M López Muñoz Telefónica, Investigación y Desarrollo
That said, I find your approach interesting. My instinct about the need for equivalence here might be wrong, although I would still need almost every Serializable type to provide an equivalence guarantee.
-- Dave Abrahams Boost Consulting www.boost-consulting.com
_______________________________________________ Unsubscribe & other changes: http://lists.boost.org/mailman/listinfo.cgi/boost

Joaquín Mª López Muñoz <joaquin@tid.es> writes: <snip long quote> Please don't overquote.
2. "a==b" is a C++ expression, so implying that a and b are objects living inside the same program. If I save an object a on my PC, pass the file to you and you load it a year later as b on your Linux box, what is "a==b" supposed to mean?
Exactly.
3. A serializable type can be implemented without observing the "a==b" rule: for instance, a list-like container can load the elements in reverse order --I understand this is a perfectly legitimate implementation that shouldn't be banned because of the "a==b" restriction.
I'm not sure it should be considered legit under any Archive concept that will be defined by the library. Is it a useful semantics? Beware premature generalization!
In my serialization stuff for Boost.MultiIndex I actually have a serializable type that does not conform to the equivalence rule. Its layout kinda looks like:
template<typename Value> struct node { value v;
template<class Archive> void serialize(Archive& ar,const unsigned int) { // do nothing } }
I use this weird construct to make node trackable, but no contents information is dumped to the archive (that is taken care of somewhere else in the program). In case you're curious, this arises in connection with serialization of iterators.
I can't imagine why you'd need that; a hint would help me to understand better. Are you saying there's no sense in which a deserialized node<T> will be equivalent to the one that has been serialized? I realize they have different "value" members, but sometimes those kinds of differences disappear under the right concept of equivalence. For example, if Value is a pointer, we don't expect it to have the same bits.
So, yes, there are actual uses of serialization not conforming to the equivalence rule.
If so, that may kill off my argument.
I guess one can also figure out other possible scenarios breaking the equivalence rule, like for instance a struct where some fields are serialized whereas others are local.
Okay.
as they relay to user provided serialize() functions.
But that's not what Robert is saying; he's saying they don't have to even do that!
IMHO an archive should guarantee that loading/saving an UDT executes the associated load/save functions.
That makes sense. I'm beginning to be convinced that you have it right.
Failing to do would devoid the Archive concept of most useful purposes. A do-nothing archive (i.e the logging example) could be covered by a more relaxed concept, if someone finds that useful.
Well, the do-nothing Saving Archive doesn't have to have a corresponding Loading Archive. It's the notion of correspondence that we're concerned here, not necessarily an intrinsic property of Archives.
So, from my point of view, the real task of an input/output archive pair is to ensure that, when a T::serialize function is invoked on loading, the input context (i.e, permissible >> ops on the input archive) is a replica of the output sequence.
This rule recursively descends to primitive (in the serialization sense) types, where an equivalence rule can actually be provided. My (skectchy) proposal is merely a formalization of this idea.
That's an interesting rule. So essentially you are saying that the output archive needs to record enough structure to ensure that the input archive can read the same sequence of types?
Yes.
What if the user serializes an aggregate struct X containing two ints? Is the corresponding input archive required to be able to read two ints as part of reading an X?
Not only that: X::save is actually *required* to load those two ^^^^ ^^^^ ?? ints.
Consider the following sample:
#include <boost/config.hpp> #include <boost/archive/text_oarchive.hpp> #include <boost/archive/text_iarchive.hpp> #include <iostream> #include <sstream>
struct foo { foo(int a=0,int b=0):a(a),b(b){}
int a,b;
BOOST_SERIALIZATION_SPLIT_MEMBER()
template<class Archive> void save(Archive& ar,const unsigned int)const { ar<<a; ar<<b; }
template<class Archive> void load(Archive& ar,const unsigned int) { ar>>a; // we do not load b!! } };
int main() { const foo x0(1,2),x1(3,4);
std::ostringstream oss; { boost::archive::text_oarchive oa(oss); oa<<x0; oa<<x1; }
foo y0,y1;
std::istringstream iss(oss.str()); boost::archive::text_iarchive ia(iss); ia>>y0; ia>>y1;
std::cout<<"y0.a="<<y0.a<<std::endl; std::cout<<"y1.a="<<y1.a<<std::endl;
return 0; }
Note that foo::save only loads the first int. The program outputs
y0.a=1 y1.a=2
which is incorrect (y1.a should be 3), so serialization of foo is not correctly implemented. For XML archive types my hunch is that the program would throw.
Okay, so it would be sufficient to add int x; ar >> x; to foo::load, right? Otherwise it seems you're treading back into the domain of equivalence.
Well, of course users of Boost.Serialization (specially if they do not write any serialize function of their own but merely use serialization capabilities of 3rd party types) expect this fuzzy equivalence rule to be held. My point is that meeting that expectation is up to each serializable type implementer, and shouldn't be enforced by the concepts section.
That may make it hard to describe the semantics of generic code that uses Serializable types with Archives. But then, I guess people can invent a stronger concept if necessary.
If Robert does not have the time/will to pursue a more formal approach, I think the equivalence rule could be relaxed to something like:
T x, y; // arbitrary operations on x to set its state sar & x; lar & y;
Postconditions: *For primitive serializable types, y is equivalent to x. *For pointer types, bla bla *Other types are expected to implement serialization in such a manner that y is equivalent to x, but this is not guaranteed.
I really prefer your operational approach now. I don't think it's hard to describe in a reasonably formal way, and the loosened equivalence you describe above really isn't worth very much.
* An input archive iar is compatible with an output archive oar if 1. iar allows a sequence of >> ops matching the corresponding << ops made upon oar (matching defined in terms of types involved and nesting depth of the call.)
Is the nesting depth of the call really relevant?
2. For primitive serialization types, the restored copies are equivalent to their original (expand on this, specially with respect to pointers.) * A type T is serializable if it is primitive serializable or else it defines the appropriate serialize (load/save) function such that the sequence of >> ops in load() match the << ops in save().
[This is not a requirement] For each serializable type, the implementor can define "equivalence" in terms of its constituent types. For instance, for std::vector:
Given a std::vector<T> out, where T is serializable, and a restored copy in, then in(i).size()==out(i).size() and each in(i)[j] is a restored copy of out(i)[j].
I don't think this latter part is worth much. I think it might be worth defining an EquivalentSerializable concept, though. -- Dave Abrahams Boost Consulting www.boost-consulting.com

David Abrahams ha escrito:
Joaquín Mª López Muñoz <joaquin@tid.es> writes:
In my serialization stuff for Boost.MultiIndex I actually have a serializable type that does not conform to the equivalence rule. Its layout kinda looks like:
template<typename Value> struct node { value v;
template<class Archive> void serialize(Archive& ar,const unsigned int) { // do nothing } }
I use this weird construct to make node trackable, but no contents information is dumped to the archive (that is taken care of somewhere else in the program). In case you're curious, this arises in connection with serialization of iterators.
I can't imagine why you'd need that; a hint would help me to understand better.
It's a little hard to grasp; this particular issue took me literally weeks of thinking, but I'll try to explain it a little more. Beware this doesn't add much to our current discussion, you might want to skip: Suppose we are implemeting serialization for a custom container: save and load are straight enough: class container{ save(...) { for(const_iterator it=begin,it_end=end();it!=it_end;++it){ ar<<*it; } } load(...) { clear(); for(iterator it=begin,it_end=end();it!=it_end;++it){ value_type v; ar>>v; push_back(v); ar.reset_object_address(&v,&back()); } } }; Now we want to add serialization for iterators. One ugly way would be as follows: class iterator{ save(...){ ar<<&(operator*()); // save pointer to element } load(...){ value_type* pv; ar>>pv; node* pn; // cast from pv to pn: possibly nonportable. assign(pn); } }; This is potentially nonportable and, besides, won't work for nontracked value_types. What we want is to archive pointers to the internal nodes, rather than the values: class iterator { save(...){ ar<<node_ptr; } load(...){ node* pn; ar>>pn; node_ptr=pn; } }; But for this to work, nodes must be serialized first so that they can be tracked later. class container{ save(...) { for(const_iterator it=begin,it_end=end();it!=it_end;++it){ ar<<*it; // save value ar<<*it.node_ptr; // save node } } load(...) { clear(); for(iterator it=begin,it_end=end();it!=it_end;++it){ value_type v; ar>>v; push_back(v); ar.reset_object_address(&v,&back()); ar>>*(--end()).node_ptr; // "load" node } } }; That's the purpose of node serialization stuff. The implementation does nothing except signalling Boost.Serialization where later node pointers must point to.
Are you saying there's no sense in which a deserialized node<T> will be equivalent to the one that has been serialized?
In a sense, restored nodes are equivalent to their originals, but I'd say it is a convoluted sense.
Well, the do-nothing Saving Archive doesn't have to have a corresponding Loading Archive. It's the notion of correspondence that we're concerned here, not necessarily an intrinsic property of Archives.
Correct, I got it wrong there.
What if the user serializes an aggregate struct X containing two ints? Is the corresponding input archive required to be able to read two ints as part of reading an X?
Not only that: X::save is actually *required* to load those two ^^^^ ^^^^ ??
I meant X::load, sorry.
Note that foo::save only loads the first int. The program outputs
I meant foo::load, again.
y0.a=1 y1.a=2
which is incorrect (y1.a should be 3), so serialization of foo is not correctly implemented. For XML archive types my hunch is that the program would throw.
Okay, so it would be sufficient to add
int x; ar >> x;
to foo::load, right? Otherwise it seems you're treading back into the domain of equivalence.
Correct.
* An input archive iar is compatible with an output archive oar if 1. iar allows a sequence of >> ops matching the corresponding << ops made upon oar (matching defined in terms of types involved and nesting depth of the call.)
Is the nesting depth of the call really relevant?
Ummm... No, we can drop that: the nesting thing is redundant with the requirement on serializable types about matching of << and >> ops. On the other hand, XML archives do enforce the nesting abidance, but this is more of an implementation artifact.
2. For primitive serialization types, the restored copies are equivalent to their original (expand on this, specially with respect to pointers.) * A type T is serializable if it is primitive serializable or else it defines the appropriate serialize (load/save) function such that the sequence of >> ops in load() match the << ops in save().
[This is not a requirement] For each serializable type, the implementor can define "equivalence" in terms of its constituent types. For instance, for std::vector:
Given a std::vector<T> out, where T is serializable, and a restored copy in, then in(i).size()==out(i).size() and each in(i)[j] is a restored copy of out(i)[j].
I don't think this latter part is worth much. I think it might be worth defining an EquivalentSerializable concept, though.
One can do the following: Let T be a serializable type and Pred an associated equality predicate inducing an equivalence relationship on T. Then T is said to be EquivalentSerializable (under Pred) if p(x,y)==true for all p of type Pred, and x and y of type T such that y is a restored copy of x. This leaves to the implementor of an UDT the open task of giving the appropriate associated equality predicate (by default we can assume std::equal_to). Then we can rewrite the postcondition on std::vector as if T is EquivalentSerializable under Pred, std::vector<T> is EquivalentSerializable. (The statement is a little more complex if we take a Pred other than the default.) Of course, this EquivalentSerializable concept does not save us the task of first providing archive compatibilty and Serializable concepts the hard way, and it is only applicable intraprogram. Does this sound good to you? Joaquín M López Muñoz Telefónica, Investigación y Desarrollo

Joaquín Mª López Muñoz <joaquin@tid.es> writes:
David Abrahams ha escrito:
Joaquín Mª López Muñoz <joaquin@tid.es> writes:
In my serialization stuff for Boost.MultiIndex I actually have a serializable type that does not conform to the equivalence rule. Its layout kinda looks like:
template<typename Value> struct node { value v;
template<class Archive> void serialize(Archive& ar,const unsigned int) { // do nothing } }
I use this weird construct to make node trackable, but no contents information is dumped to the archive (that is taken care of somewhere else in the program). In case you're curious, this arises in connection with serialization of iterators.
I can't imagine why you'd need that; a hint would help me to understand better.
It's a little hard to grasp; this particular issue took me literally weeks of thinking, but I'll try to explain it a little more. Beware this doesn't add much to our current discussion, you might want to skip:
Suppose we are implemeting serialization for a custom container: save and load are straight enough:
class container{ save(...) { for(const_iterator it=begin,it_end=end();it!=it_end;++it){ ar<<*it; } } load(...) { clear(); for(iterator it=begin,it_end=end();it!=it_end;++it){ value_type v; ar>>v; push_back(v); ar.reset_object_address(&v,&back());
Assuming value_type is default constructible and push_back doesn't invalidate any addresses of other objects, I guess so. But in that case I'd still preallocate enough elements and deserialize them in place.
} } };
Now we want to add serialization for iterators. One ugly way would be as follows:
class iterator{ save(...){ ar<<&(operator*()); // save pointer to element } load(...){ value_type* pv; ar>>pv; node* pn; // cast from pv to pn: possibly nonportable. assign(pn);
Clearly. The "right thing to do" is to serialize all the nodes as part of serializing the container. Then this "just works," no?
} };
This is potentially nonportable and, besides, won't work for nontracked value_types. What we want is to archive pointers to the internal nodes, rather than the values:
Right.
class iterator { save(...){ ar<<node_ptr; } load(...){ node* pn; ar>>pn; node_ptr=pn; } };
But for this to work, nodes must be serialized first so that they can be tracked later.
class container{ save(...) { for(const_iterator it=begin,it_end=end();it!=it_end;++it){ ar<<*it; // save value ar<<*it.node_ptr; // save node
Why wouldn't your node just implement serialization that serializes its contained value?
} } load(...) { clear(); for(iterator it=begin,it_end=end();it!=it_end;++it){ value_type v; ar>>v; push_back(v); ar.reset_object_address(&v,&back()); ar>>*(--end()).node_ptr; // "load" node } } };
That's the purpose of node serialization stuff. The implementation does nothing except signalling Boost.Serialization where later node pointers must point to.
Well, I could probably get this if I thought hard enough about it, but I don't yet. Of course I could be missing something, it seems like a hack to me. Serializing and deserializing the nodes directly seems a lot cleaner.
* An input archive iar is compatible with an output archive oar if 1. iar allows a sequence of >> ops matching the corresponding << ops made upon oar (matching defined in terms of types involved and nesting depth of the call.)
Is the nesting depth of the call really relevant?
Ummm... No, we can drop that: the nesting thing is redundant with the requirement on serializable types about matching of << and >> ops. On the other hand, XML archives do enforce the nesting abidance, but this is more of an implementation artifact.
Good. It gets simpler.
2. For primitive serialization types, the restored copies are equivalent to their original (expand on this, specially with respect to pointers.) * A type T is serializable if it is primitive serializable or else it defines the appropriate serialize (load/save) function such that the sequence of >> ops in load() match the << ops in save().
[This is not a requirement] For each serializable type, the implementor can define "equivalence" in terms of its constituent types. For instance, for std::vector:
Given a std::vector<T> out, where T is serializable, and a restored copy in, then in(i).size()==out(i).size() and each in(i)[j] is a restored copy of out(i)[j].
I don't think this latter part is worth much. I think it might be worth defining an EquivalentSerializable concept, though.
One can do the following:
Let T be a serializable type and Pred an associated equality predicate inducing an equivalence relationship on T. Then T is said to be EquivalentSerializable (under Pred) if
p(x,y)==true
for all p of type Pred
You said Pred was a predicate; now you're saying it's a type. I think you were right the first time. You'll never satisfy that for all p of type bool(*)(int,int) for example.
and x and y of type T such that y is a restored copy of x.
This leaves to the implementor of an UDT the open task of giving the appropriate associated equality predicate (by default we can assume std::equal_to).
I think you mean ==
Then we can rewrite the postcondition on std::vector as
if T is EquivalentSerializable under Pred, std::vector<T> is EquivalentSerializable.
Nope. You have to say under what predicate it is EquivalentSerializable. And when a nonstandard predicate is used for T there may not be any such predicate for the vector.
(The statement is a little more complex if we take a Pred other than the default.) Of course, this EquivalentSerializable concept does not save us the task of first providing archive compatibilty and Serializable concepts the hard way
Of course not.
and it is only applicable intraprogram.
That's only true if you consider Pred to be a callable C++ predicate rather than a logical one.
Does this sound good to you?
Yes and no. It's crafty, but you have a pretty big gaping hole as demonstrated by the vector example. I would be very happy with the good old fuzzy notion of equivalence here, but if you can close the hole, I don't mind adding predicates to the mix. Okay, how about this: the predicate is tightly bound to the type. So the predicate for vector<T> is defined to be that the two vectors have the same length and that each corresponding element of the two vectors satisfies the predicate that's bound to T. -- Dave Abrahams Boost Consulting www.boost-consulting.com

David Abrahams ha escrito:
Joaquín Mª López Muñoz <joaquin@tid.es> writes:
Suppose we are implemeting serialization for a custom container: save and load are straight enough:
class container{ save(...) { for(const_iterator it=begin,it_end=end();it!=it_end;++it){ ar<<*it; } } load(...) { clear(); for(iterator it=begin,it_end=end();it!=it_end;++it){ value_type v; ar>>v; push_back(v); ar.reset_object_address(&v,&back());
Assuming value_type is default constructible and push_back doesn't invalidate any addresses of other objects, I guess so. But in that case I'd still preallocate enough elements and deserialize them in place.
Some details are missing for brevity of exposition. v is not default constructed, but rather constructed via serialization::load_construct_data_adl. Elements are actually stable in multi_index_container. push_back() is just pseudocode for your convenience, insertion is done by other means. The preallocation scheme does not work for many reasons: one of them is that an associative (set-like) index cannot be prefed elements without knowing what their values will be. If you'd like to see the real stuff, have a look at methods save_ and load_ in boost/multi_index_container.hpp.
} } };
Now we want to add serialization for iterators. One ugly way would be as follows:
class iterator{ save(...){ ar<<&(operator*()); // save pointer to element } load(...){ value_type* pv; ar>>pv; node* pn; // cast from pv to pn: possibly nonportable. assign(pn);
Clearly. The "right thing to do" is to serialize all the nodes as part of serializing the container. Then this "just works," no?
} };
This is potentially nonportable and, besides, won't work for nontracked value_types. What we want is to archive pointers to the internal nodes, rather than the values:
Right.
class iterator { save(...){ ar<<node_ptr; } load(...){ node* pn; ar>>pn; node_ptr=pn; } };
But for this to work, nodes must be serialized first so that they can be tracked later.
class container{ save(...) { for(const_iterator it=begin,it_end=end();it!=it_end;++it){ ar<<*it; // save value ar<<*it.node_ptr; // save node
Why wouldn't your node just implement serialization that serializes its contained value?
I knew you'd ask that :) It wouldn't work on loading. Take a look again at the loading part of the container: /* 1 */ value_type v; /* 2 */ ar>>v; /* 3 */ push_back(v); /* 4 */ ar.reset_object_address(&v,&back()); /* 5 */ ar>>*(--end()).node_ptr; // "load" node It is not until /* 3 */ is executed that the node comes into existence. But for the node to be created, we must first know its associated value --remember, I just cannot insert a node with a "dummy" value into an associative container, the value determines crucially where the node ends up into the container. Your next question could be: well, why don't you load the node out of place, with value and all and then link it into the container? This would work, but would force me to implement a different insertion method than currently used, where the value is not assigned to the node until the node is properly linked.
} } load(...) { clear(); for(iterator it=begin,it_end=end();it!=it_end;++it){ value_type v; ar>>v; push_back(v); ar.reset_object_address(&v,&back()); ar>>*(--end()).node_ptr; // "load" node } } };
That's the purpose of node serialization stuff. The implementation does nothing except signalling Boost.Serialization where later node pointers must point to.
Well, I could probably get this if I thought hard enough about it, but I don't yet. Of course I could be missing something, it seems like a hack to me. Serializing and deserializing the nodes directly seems a lot cleaner.
Hopefully this is answered in my previous paragraph.
Let T be a serializable type and Pred an associated equality predicate inducing an equivalence relationship on T. Then T is said to be EquivalentSerializable (under Pred) if
p(x,y)==true
for all p of type Pred
You said Pred was a predicate; now you're saying it's a type. I think you were right the first time. You'll never satisfy that for all p of type bool(*)(int,int) for example.
A predicate is a type, at least according to http://www.sgi.com/tech/stl/BinaryPredicate.html (BTW, it is BinaryPredicate what I meant rather than UnaryPredicate.) I'm naming types with uppercase first letter, objects lowercase.
and x and y of type T such that y is a restored copy of x.
This leaves to the implementor of an UDT the open task of giving the appropriate associated equality predicate (by default we can assume std::equal_to).
I think you mean ==
== is not a (SGI sense) BinaryPredicate: it is not even a type. I really mean std::equal_to (std::equal_to<T>, to be precise.)
Then we can rewrite the postcondition on std::vector as
if T is EquivalentSerializable under Pred, std::vector<T> is EquivalentSerializable.
Nope. You have to say under what predicate it is EquivalentSerializable. And when a nonstandard predicate is used for T there may not be any such predicate for the vector.
I think we agree here: when I said above "by default we can assume std::equal_to" what I meant is: EquivalentSerializable is short for EquivalentSerializable under std::equal_to. Otherwise we'd say EquivalentSerializable under Pred and specify Pred. So I think my statement about std::vector is correct and addresses your concerns.
(The statement is a little more complex if we take a Pred other than the default.)
This addresses your point about a "nonstandard predicate". I hope you're getting me.
Of course, this EquivalentSerializable concept does
not save us the task of first providing archive compatibilty and Serializable concepts the hard way
Of course not.
and it is only applicable intraprogram.
That's only true if you consider Pred to be a callable C++ predicate rather than a logical one.
I feel comfortable referring to C++ entities alone. Such a logical predicate accepting arguments from different program executions would involve moving up to a higher ontological level, which is a murky area. You know, Ockham razor's and all that stuff. This is related to my objection #2 a couple of posts before. BTW, there's a method by which we can extend EquivalentSerializable to the interprogram domain without invoking non-C++ predicates. But we can defer this to a later time.
Does this sound good to you?
Yes and no. It's crafty, but you have a pretty big gaping hole as demonstrated by the vector example. I would be very happy with the good old fuzzy notion of equivalence here, but if you can close the hole, I don't mind adding predicates to the mix.
Okay, how about this: the predicate is tightly bound to the type. So the predicate for vector<T> is defined to be that the two vectors have the same length and that each corresponding element of the two vectors satisfies the predicate that's bound to T.
Yes, this is an acceptable refinement. If I'm getting you, you propose to fix the Pred type for each Serializable type. My proprosal is more of a "concept template" ranging over a free Pred type. Let me rewrite down this idea explicitly, so that we can agree we are talking about the same thing: * Let T be a Serializable type and Pred an associated BinaryPredicate type (http://www.sgi.com/tech/stl/BinaryPredicate.html) inducing an equivalence relationship on T. Then T is said to be *EquivalentSerializable under Pred* if p(x,y)==true for all p of type Pred, and x and y of type T such that y is a restored copy of x. * A Serializable type T may provide a designated type X such that T is EquivalentSerializable under X. In this case, we use the notation SerializationEquality[T] to refer to such X. * We say that T is *EquivalentSerializable* if SerializationEquality[T] is provided. * [vector example] if T is EquivalentSerializable, then SerializationEquality[std::vector<T>] is the type: struct { bool operator()(const std::vector<T>& x,const std::vector<T>& y){ return x.size()==y.size()&& std::equal(x.begin(),x.end(),y.begin(),SerializationEquality[T]()); } }; Is this what you propose? Well, I'd say your idea and mine are mere variations on the same theme, I couldn't say which one is more convenient. Joaquín M López Muñoz Telefónica, Investigación y Desarrollo

Joaquín Mª López Muñoz ha escrito:
Then we can rewrite the postcondition on std::vector as
if T is EquivalentSerializable under Pred, std::vector<T> is EquivalentSerializable.
Nope. You have to say under what predicate it is EquivalentSerializable. And when a nonstandard predicate is used for T there may not be any such predicate for the vector.
I think we agree here: when I said above "by default we can assume std::equal_to" what I meant is: EquivalentSerializable is short for EquivalentSerializable under std::equal_to. Otherwise we'd say EquivalentSerializable under Pred and specify Pred. So I think my statement about std::vector is correct and addresses your concerns.
Oh, my bad. The statement has an unintended "under Pred". What I meant to write is: f T is EquivalentSerializable, std::vector<T> is EquivalentSerializable. which works with the default types for the associated Pred's. Joaquín M López Muñoz Telefónica, Investigación y Desarrollo

Just a tiny note reqarding this equivalence question. One question that comes up again and again is that of const class members. struct X { const int m_i; int m_j; X(i) : m_i(i){} }; Some users want to use: template<class Archive> void serialize(Archive & ar, X & x, const unsigned int version){ ar & const_cast<int &>(m_i); ar & m_j; } while others believe the above is incorrect and use: template<class Archive> void serialize(Archive & ar, X & x, const unsigned int version){ ar & m_j; } while still others use save/load_construct_data(...) Is the loaded version going to be "==" to the saved version? Depends upon how "==" is implemented for X. Is the loaded version "equivalent" ? Regardless of the answer to these questions, I know that the serialization library is used to user's satisfaction by users who have differing answers to these questions. At first I thought I was going to have to impose a answers to these questions in order to finish the implementation. But since that turned out to be unnecessary, I just left it at the discretion of the user. This has left somethings ambiguous, but it has permited me to actually finish the package to the satisfaction of a varied group of users. Robert Ramey
participants (4)
-
David Abrahams
-
Joaquin M Lopez Munoz
-
Joaquín Mª López Muñoz
-
Robert Ramey