
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<Person, boost::pfr::flat_less<>>; std::set<Employee , boost::pfr::flat_less<>>; std::set<Prisoner , boost::pfr::flat_less<>>; ``` 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<MyAggregate, boost::fpr::less<>> 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;