Hi Everyone,
This is my partial review of PFR library. I have read the docs, watched
Antony's talk, played with some toy examples, and had a brief look at the
implementation of pfr::tuple_size. I spent like 14 hours in total on this.
I have studied the subject of tuple-like access to class objects a while
back, which resulted in this proposal:
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3326.pdf.
I like the library, I think it has the potential to address a small but a
well understood demand.
While I would like to see it included in Boost, I believe it should not be
accepted just yet, in its current shape. I disagree with parts of the
design (injecting the interface of user types) as explained below. Missing
documentation parts (I mean the concept "Baseless Aggregate") is also an
issue for me. First, it might be an indication of a flaw in the design.
Second, it might cover some bugs in the implementation. Third, making
another review with updated docs might attract more reviews and may make
people appreciate the library more.
Alternatively, I could have called it a conditional acceptance with
everything listed below as conditions. Since reviewers are not casting
votes but give recommendations instead, the Review Manager may want to take
this into account.
I think that this type of the library is mostly about the documentation and
design (like, the scope). The implementation will necessarily be a number
of hacks to provide the magic. Since I question parts of the design, I
didn't find it necessary to review the implementation in detail.
The following list may appear long, and look like a long list of
complaints, so I need to make this clear up front. I really like the
library, and I intend to use it next time I have a need for the tuple-like
access to PODs, even download it from GitHub. I recommend the rejection
because I see the second review as an opportunity for the library to become
even better.
DOCUMENTATION
Documentation does explain how to use the library, and after a while it
becomes easy to use every function, at least the functions that I tried.
But it is missing some important parts, therefore it confuses the potential
users as to what its scope is, and gives an (false) impression that it is
defective. (Some of the below points may not hold anymore, as I have
noticed the documentation has changed during the review period.)
1. The library name is misleading. It uses the name "reflection" but it
doesn't really implement reflection. A reflection would be a way of
querying the system for different properties of declarations in the
program. This is not the case here.
2. The intro page misses the opportunity to present the scope of the
library, its power, and the minimal example of how to use it and how it
makes the life of the programmer easier. It is a very good library, and it
would be a great loss if it was overlooked due to not being advertised
effectively. I volunteer to provide the content of the initial page that I
believe would meet the criteria.
3. The library buds on a basic concept of "Base-less aggregate". It must
document it up front. And if possible try to statically detect if the types
satisfy it to the extent that this is doable. After reading the docs, the
user has to have a clear picture which types will and which won't work with
the library.
4. It should contain a couple of motivating use cases, both for flat and
precise representation. It is not enough to show how they work. The user
must also see why they would be useful for anyone. Some users immediately
see that this library addresses their use, others are confused, and cannot
see any purpose in having it.
5. One use case for flat reflection is when a couple of aggregates have a
common subset:
```
struct Person {
std::string firstName;
std::string lastName;
int age;
};
struct Employee {
Person person;
double salary;
};
struct Prisoner {
Person person
int cellNumber;
};
std::set>;
std::set>;
std::set>;
```
Just saying, "use flat variants when you want to flatten your aggregate" is
not enough to make the user see the capabilities of this library. I think
that this use case should be documented.
6. I believe that individual operations are underspecified. For instance:
i. pfr::write (http://apolukhin.github.io/magic_get/boost/pfr/write.html)
has a section "Description", but it doesn't say what the function is doing.
To some extent, we can guess that from the signature but some important
questions remain unanswered: What format of the serialized output does the
library guarantee? Does it guarantee any format at all? Do you leave
yourself a freedom to change it in the future without notice? Can users
customize it? If so, where is it specified? (This would be a no-problem if
the library didn't provide stream insertion operations.)
ii. The library says it can inject relational operators (and others) but
it doesn't specify what their semantics are.
7. The means of specifying the semantics of the functions could be improved
(i.e., made shorter and more precise) if you specified
prf::structure_to_tuple (and pfr::flat_structure_to_tuple) very precisely
as std::tie() on the corresponding aggregate elements, and then define the
semantics of every other function in terms of prf::structure_to_tuple.
Instead, I can see that prf::structure_to_tuple is underspecified (
http://apolukhin.github.io/magic_get/boost/pfr/structure_to_tuple.html).
Section "Description" does not describe what the function is doing.
DESIGN
1. I am missing the concept "Base-less aggregate" that is a basic building
block of this library. I would expect it to appear in one of the front
pages of the docs, and static checks in the code that would mention this
name, so that I am immediately informed if I fail to satisfy the concept.
I realize that defining a type trait for this is impossible, but even if it
is a line of code deep inside the implementation that breaks (like,
decomposing the aggregate into a structure binding), this line should have
a comment saying something like "if compilation breaks on this line, it is
likely because your type does not satisfy the requirements of a
BaselessAggregate."
2. I appreciate functions that this library defines in namespace
boost::pfr, such as pfr::less or pfr::for_each_field or
pfr::structure_to_tuple; but I do not appreciate that this library injects
functions into global namespace or my namespace or even that it provides
operators (like <<, which are naturally part of my classes' interface) for
my types in namespace boost::pfr. I really consider this harmful, causing
subtle ADL-related bugs. If I want my types logged, I would rather expect
to be able to type:
```
// in my namespace
inline std::ostream& operator<<(std::ostream& o, MyAggregate const& a)
{
return boost::fpr::write(o, a);
}
```
Same with relational operators: rather than library injecting them (and
potentially causing ADL problems or ODR violations, especially in C++20), I
would rather use a named PFR function where needed:
```
sort(myAggregates.begin(), myAggregates.end(), boost::fpr::less<>{});
std::set> map;
```
This is a bit more verbose, but:
i. Still does not require me to list members anywhere (the primary
advantage of this library).
ii. There is no possibility of getting any ADL- or ODR-related bug.
The ODR-violation bugs are really nasty. You do everything fine in your
.cpp file and you have no clue why the program desn't work as you expect.
And this is because someone else in another .cpp file has put different
declarations. And if you have 100.000 cpp files, you may not be able to
find the cause for weeks. Offering these operators encourages this type of
bugs.
3. If we eliminate the injected operators from the picture, we get a sound
interface that can be reused in the future revisions of C++, where the
internals will be able to use native C++26 reflection instead of the
current hacks. We would get the beautiful effect that the initial Boost
libraries provided: an interface that can work uniformly both with old and
modern versions of C++.
4. There is another reason why I do not like the stream insertion
operators. They just don't belong conceptually to this library. Stream
insertion is another problem domain:
i. How do you serialize type `char`: as a numeric value, or as a graphical
symbol
ii. How do you serialize a pointer? As an address it represents? Do you
make an exception for type `const char *`? Do you make an exception for
type `char32_t const*`?
iii. How do you separate the individual values? What character do you use
for grouping them?
iv. How do you allow users to control what happens for individual types?
v. What if the user is nonetheless disappointed with your design choices?
These issues go far beyond providing a tuple-like access to aggregates.
They should be addressed by a separate library built on top of PFR.
5. Docs say, "provides other std::tuple like methods for user defined types
without any macro or boilerplate code."This is not actually the case. In
order to get the operators for aggregates -- at least in one mode -- one
has to use a macro.
6. I would also say that this is inconvenient that the library, as
documented, allows me to inject all the operators or none. I cannot opt
into taking only relational operators. At least this is not documented. But
I cannot honestly complain about it, because I do not agree with PFR
synthesizing any of these operations at all.
FInally, I wanted to thank Antony for writing and sharing this library
here. I learned a great lot from it. I didn't realize that such reflection
was possible in C++17. I hope that my review does not sound discouraging.
Regards,
&rzej;