[PFR][review] Andrzej's review
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
Andrzej Krzemienski wrote:
3. The library buds on a basic concept of "Base-less aggregate".
That's not really true though, is it? E.g.
#include
wt., 6 paź 2020 o 13:29 Peter Dimov via Boost
Andrzej Krzemienski wrote:
3. The library buds on a basic concept of "Base-less aggregate".
That's not really true though, is it? E.g.
#include
#include <string> #include <iostream> struct X { std::string a; };
struct Y { std::string b; };
struct Z: X, Y { std::string c; };
int main() { std::cout << boost::pfr::tuple_size_v<Z> << std::endl; }
compiles and prints "3". Bases are treated as members by the precise API.
But this is only for tuple_size. If you do a for_each, the compilation breaks: ``` Z z {{}, {}, {}}; boost::pfr::for_each_field(z, [](auto const&){}); ``` This is because brace initialization of aggregates is "incompatible" with structured binding for aggregates now in C++20. I still acknowledge that I may have not understood the requirement that PFR puts on types. This makes my request even stronger: I have to know what it expects of my types to work. I expect that this would work when I switch between C++17 and C++20. (C++20 changed the initialization of aggregates: base is treated as yet another member. But it did not change the structured binding.) Regards, &rzej;
Andrzej Krzemienski wrote:
But this is only for tuple_size. If you do a for_each, the compilation breaks:
``` Z z {{}, {}, {}}; boost::pfr::for_each_field(z, [](auto const&){}); ```
Yes, you're right. 1>C:\boost-git\develop\boost/pfr/detail/core17_generated.hpp(57): error : cannot decompose class type 'Z': both it and its base class 'X' have non-static data members That's an annoying (and unnecessary IMO) limitation of structured bindings. Someone should write a paper to lift it (instead of just posting to the reflector as I did.)
вт, 6 окт. 2020 г. в 14:42, Andrzej Krzemienski via Boost
wt., 6 paź 2020 o 13:29 Peter Dimov via Boost
napisał(a): Andrzej Krzemienski wrote:
3. The library buds on a basic concept of "Base-less aggregate".
That's not really true though, is it? E.g.
#include
#include <string> #include <iostream> struct X { std::string a; };
struct Y { std::string b; };
struct Z: X, Y { std::string c; };
int main() { std::cout << boost::pfr::tuple_size_v<Z> << std::endl; }
compiles and prints "3". Bases are treated as members by the precise API.
But this is only for tuple_size. If you do a for_each, the compilation breaks:
``` Z z {{}, {}, {}}; boost::pfr::for_each_field(z, [](auto const&){}); ```
This is because brace initialization of aggregates is "incompatible" with structured binding for aggregates now in C++20.
I think we took the wrong turn in C++17, when allowed inheritance for aggregates. Unfortunately that's not getting better with time :( -- Best regards, Antony Polukhin
Antony Polukhin wrote:
I think we took the wrong turn in C++17, when allowed inheritance for aggregates.
Nothing wrong with that IMO. Structured bindings just deliberately break this case for no reason at all. "Otherwise, all of E’s non-static data members shall be direct members of E or of the same base class of E"
вт, 6 окт. 2020 г. в 12:22, Andrzej Krzemienski via Boost
Hi Everyone, This is my partial review of PFR library.
<...> Many thanks! As the reviews go I'm getting an impression that the library needs to lose weight: * drop the flat reflection * drop the global operators * drop the macro for declaring operators for type This will make the library 2/3 smaller and simpler to understand, along with solving the problems of macro customizations and accidental flat_* usages. -- Best regards, Antony Polukhin
wt., 6 paź 2020 o 19:53 Antony Polukhin
вт, 6 окт. 2020 г. в 12:22, Andrzej Krzemienski via Boost
: Hi Everyone, This is my partial review of PFR library.
<...>
Many thanks!
As the reviews go I'm getting an impression that the library needs to lose weight: * drop the flat reflection * drop the global operators * drop the macro for declaring operators for type
This will make the library 2/3 smaller and simpler to understand, along with solving the problems of macro customizations and accidental flat_* usages.
I, on the other hand, during the review started to appreciate the "flat" part. I realized I had a good use case for it. At times, I found myself in the situation of having a number of aggregate types with the common set of members: ``` struct Person { std::string firstName; std::string lastName; int age; }; struct Employee { std::string firstName; std::string lastName; int age; double salary; }; struct Prisoner { std::string firstName; std::string lastName; int age; int cellNumber; }; ``` I would immediately rewrite it to: ``` struct Person { std::string firstName; std::string lastName; int age; }; struct Employee : Person { double salary; }; struct Prisoner : Person { int cellNumber; }; ``` This (a) avoids duplication, and (b) I have the slicing do exactly what I need: convert an Employee to a Person. Now, I am disappointed with what C++17 did to aggregate initialization. My natural expectation would be that: ``` Employee e {"Bilbo", "Baggins", 111, 1000.00}; ``` Flattens the class hierarchy and initializes each of the four members. Apparently , the C++ committee has a different vision for it. But I have noticed that if I changed derivation into aggregation (as I indicated earlier), the "flat" part of FPR would do exactly what I needed. I wonder what is your *primary* motivation for removing it: A. Because it is not implementable in many cases? B. Because there is not enough motivation for it, and people don't understand what it is for? C. Because it can be accidentally confused with the "precise" version? If it is (A), then I guess you are right. If any other then maybe leaving it in would be a better alternative. Regards, &rzej;
On 06.10.20 21:50, Andrzej Krzemienski via Boost wrote:
struct Person { std::string firstName; std::string lastName; int age; };
struct Employee : Person { double salary; };
struct Prisoner : Person { int cellNumber; }; ``` This (a) avoids duplication, and (b) I have the slicing do exactly what I need: convert an Employee to a Person. Now, I am disappointed with what C++17 did to aggregate initialization. My natural expectation would be that:
``` Employee e {"Bilbo", "Baggins", 111, 1000.00}; ``` Flattens the class hierarchy and initializes each of the four members. Apparently , the C++ committee has a different vision for it. But I have noticed that if I changed derivation into aggregation (as I indicated earlier), the "flat" part of FPR would do exactly what I needed.
Does that example really work? My expectation would be that flat reflection would try to break apart the std::strings, and fail with a compile-time error because std::string is not an aggregate. If it does work, then flat reflection is a lot more useful than I had initially thought! -- Rainer Deyke (rainerd@eldwood.com)
śr., 7 paź 2020 o 09:22 Rainer Deyke via Boost
struct Person { std::string firstName; std::string lastName; int age; };
struct Employee : Person { double salary; };
struct Prisoner : Person { int cellNumber; }; ``` This (a) avoids duplication, and (b) I have the slicing do exactly what I need: convert an Employee to a Person. Now, I am disappointed with what C++17 did to aggregate initialization. My natural expectation would be
On 06.10.20 21:50, Andrzej Krzemienski via Boost wrote: that:
``` Employee e {"Bilbo", "Baggins", 111, 1000.00}; ``` Flattens the class hierarchy and initializes each of the four members. Apparently , the C++ committee has a different vision for it. But I have noticed that if I changed derivation into aggregation (as I indicated earlier), the "flat" part of FPR would do exactly what I needed.
Does that example really work? My expectation would be that flat reflection would try to break apart the std::strings, and fail with a compile-time error because std::string is not an aggregate.
If it does work, then flat reflection is a lot more useful than I had initially thought!
The following compiles and outputs "1 2 3 " on my GCC compiler: ``` struct A { int x, y; }; struct B { A a; int z; }; int main() { B b = {{1, 2}, 3}; boost::pfr::flat_for_each_field(b, [](auto const& field){ std::cout << field << " "; }); } ``` However, when I substitute strings for ints it no longer compiles. I am not sure if this is just a bug in the implementation or an inherent limitation of this library. Regards, &rzej;
śr., 7 paź 2020 o 09:58 Andrzej Krzemienski
śr., 7 paź 2020 o 09:22 Rainer Deyke via Boost
napisał(a): struct Person { std::string firstName; std::string lastName; int age; };
struct Employee : Person { double salary; };
struct Prisoner : Person { int cellNumber; }; ``` This (a) avoids duplication, and (b) I have the slicing do exactly what I need: convert an Employee to a Person. Now, I am disappointed with what C++17 did to aggregate initialization. My natural expectation would be
On 06.10.20 21:50, Andrzej Krzemienski via Boost wrote: that:
``` Employee e {"Bilbo", "Baggins", 111, 1000.00}; ``` Flattens the class hierarchy and initializes each of the four members. Apparently , the C++ committee has a different vision for it. But I have noticed that if I changed derivation into aggregation (as I indicated earlier), the "flat" part of FPR would do exactly what I needed.
Does that example really work? My expectation would be that flat reflection would try to break apart the std::strings, and fail with a compile-time error because std::string is not an aggregate.
If it does work, then flat reflection is a lot more useful than I had initially thought!
The following compiles and outputs "1 2 3 " on my GCC compiler:
``` struct A { int x, y; };
struct B { A a; int z; };
int main() { B b = {{1, 2}, 3}; boost::pfr::flat_for_each_field(b, [](auto const& field){ std::cout << field << " "; }); } ```
However, when I substitute strings for ints it no longer compiles. I am not sure if this is just a bug in the implementation or an inherent limitation of this library.
Oh, I know why it is doing this. In general, the library cannot know to what level the user wants her structure to be flattened: ``` struct W { // you do not know what is inside }; ostream& operator<<(ostream&, const W&); struct A { W x, y; }; struct B { A a; W z; }; int main() { B b = {{w1, w2}, w3}; boost::pfr::flat_for_each_field(b, [](auto const& field){ std::cout << field << " "; }); } ``` Here, it is not clear whether you want to just print the W, or flatten it further down if it happens to be an aggregate. So maybe the "flat" part in PFR cannot solve my problem. Regards, &rzej;
ср, 7 окт. 2020 г. в 10:59, Andrzej Krzemienski via Boost
śr., 7 paź 2020 o 09:22 Rainer Deyke via Boost
napisał(a): struct Person { std::string firstName; std::string lastName; int age; };
struct Employee : Person { double salary; };
struct Prisoner : Person { int cellNumber; }; ``` This (a) avoids duplication, and (b) I have the slicing do exactly what I need: convert an Employee to a Person. Now, I am disappointed with what C++17 did to aggregate initialization. My natural expectation would be
On 06.10.20 21:50, Andrzej Krzemienski via Boost wrote: that:
``` Employee e {"Bilbo", "Baggins", 111, 1000.00}; ``` Flattens the class hierarchy and initializes each of the four members. Apparently , the C++ committee has a different vision for it. But I have noticed that if I changed derivation into aggregation (as I indicated earlier), the "flat" part of FPR would do exactly what I needed.
Does that example really work? My expectation would be that flat reflection would try to break apart the std::strings, and fail with a compile-time error because std::string is not an aggregate.
If it does work, then flat reflection is a lot more useful than I had initially thought!
The following compiles and outputs "1 2 3 " on my GCC compiler:
``` struct A { int x, y; };
struct B { A a; int z; };
int main() { B b = {{1, 2}, 3}; boost::pfr::flat_for_each_field(b, [](auto const& field){ std::cout << field << " "; }); } ```
However, when I substitute strings for ints it no longer compiles. I am not sure if this is just a bug in the implementation or an inherent limitation of this library.
That's an inherent limitation of flat reflection. Flat reflection is kind of a legacy - it's the first implemented "reflection", and it was working only for PODs. Since early versions of PFR new precise ways of reflection were discovered. Moreover, precise reflection turned out to be much portable and works on more compilers.
I wonder what is your *primary* motivation for removing it: A. Because it is not implementable in many cases? B. Because there is not enough motivation for it, and people don't understand what it is for? C. Because it can be accidentally confused with the "precise" version?
It's rather 'D.' - unlike flat reflection, precise representation work in much more cases and on more platforms (MSVC fails to do flat reflection) -- Best regards, Antony Polukhin
śr., 7 paź 2020 o 12:30 Antony Polukhin
ср, 7 окт. 2020 г. в 10:59, Andrzej Krzemienski via Boost
: śr., 7 paź 2020 o 09:22 Rainer Deyke via Boost
napisał(a): On 06.10.20 21:50, Andrzej Krzemienski via Boost wrote:
struct Person { std::string firstName; std::string lastName; int age; };
struct Employee : Person { double salary; };
struct Prisoner : Person { int cellNumber; }; ``` This (a) avoids duplication, and (b) I have the slicing do exactly
need: convert an Employee to a Person. Now, I am disappointed with what C++17 did to aggregate initialization. My natural expectation would be that:
``` Employee e {"Bilbo", "Baggins", 111, 1000.00}; ``` Flattens the class hierarchy and initializes each of the four members. Apparently , the C++ committee has a different vision for it. But I have noticed that if I changed derivation into aggregation (as I indicated earlier), the "flat" part of FPR would do exactly what I needed.
Does that example really work? My expectation would be that flat reflection would try to break apart the std::strings, and fail with a compile-time error because std::string is not an aggregate.
If it does work, then flat reflection is a lot more useful than I had initially thought!
The following compiles and outputs "1 2 3 " on my GCC compiler:
``` struct A { int x, y; };
struct B { A a; int z; };
int main() { B b = {{1, 2}, 3}; boost::pfr::flat_for_each_field(b, [](auto const& field){ std::cout << field << " "; }); } ```
However, when I substitute strings for ints it no longer compiles. I am not sure if this is just a bug in the implementation or an inherent
what I limitation
of this library.
That's an inherent limitation of flat reflection. Flat reflection is kind of a legacy - it's the first implemented "reflection", and it was working only for PODs. Since early versions of PFR new precise ways of reflection were discovered. Moreover, precise reflection turned out to be much portable and works on more compilers.
I wonder what is your *primary* motivation for removing it: A. Because it is not implementable in many cases? B. Because there is not enough motivation for it, and people don't understand what it is for? C. Because it can be accidentally confused with the "precise" version?
It's rather 'D.' - unlike flat reflection, precise representation work in much more cases and on more platforms (MSVC fails to do flat reflection)
Interesting. I do not understand enough about the implementation to be
making definite statements, but based on my superficial knowledge I expect
that it should be possible to reimplement a "flat" reflection on top of
"precise" one.
1. First you call structure_to_tuple() and you get std::tuple
ср, 7 окт. 2020 г. в 15:48, Andrzej Krzemienski
Interesting. I do not understand enough about the implementation to be making definite statements, but based on my superficial knowledge I expect that it should be possible to reimplement a "flat" reflection on top of "precise" one. 1. First you call structure_to_tuple() and you get std::tuple
2. For every Tn that is an aggregate type you apply structure_to_tuple() recursively. 3. Then you potentially get a tuple of nested tuples and you need to flatten it.
Yes, that's doable. But is that really a popular feature? Flat representation was useful, because there was no precise representation in the early versions of the library. That was the only way to do things. Back to your example: struct Person { std::string firstName; std::string lastName; int age; }; struct Employee : Person { double salary; }; struct Prisoner : Person { int cellNumber; }; Probably composition would work better than inheritance for your needs? struct Person { std::string firstName; std::string lastName; int age; }; struct Employee { Person person; double salary; }; struct Prisoner { Person person; int cellNumber; }; With composition you get more flexibility: std::ostream& operator<<(std::ostream& os, const Person& p) { os << "person: "; pfr::write(os, p); return os; } std::ostream& operator<<(std::ostream& os, const Prisoner& p) { using pfr::ops; return os << p; } // Outputs: {person: {"Harley ", "Quinn", 21}, 100500} std::cout << Prinsoner{{"Harley ", "Quinn", 21}, 100500}; -- Best regards, Antony Polukhin
śr., 7 paź 2020 o 17:11 Antony Polukhin
Interesting. I do not understand enough about the implementation to be making definite statements, but based on my superficial knowledge I expect
ср, 7 окт. 2020 г. в 15:48, Andrzej Krzemienski
: that it should be possible to reimplement a "flat" reflection on top of "precise" one. 1. First you call structure_to_tuple() and you get std::tuple
2. For every Tn that is an aggregate type you apply structure_to_tuple() recursively. 3. Then you potentially get a tuple of nested tuples and you need to flatten it. Yes, that's doable. But is that really a popular feature?
Flat representation was useful, because there was no precise representation in the early versions of the library. That was the only way to do things.
Back to your example:
struct Person { std::string firstName; std::string lastName; int age; };
struct Employee : Person { double salary; };
struct Prisoner : Person { int cellNumber; };
Probably composition would work better than inheritance for your needs?
struct Person { std::string firstName; std::string lastName; int age; };
struct Employee { Person person; double salary; };
struct Prisoner { Person person; int cellNumber; };
With composition you get more flexibility:
std::ostream& operator<<(std::ostream& os, const Person& p) { os << "person: "; pfr::write(os, p); return os; }
std::ostream& operator<<(std::ostream& os, const Prisoner& p) { using pfr::ops; return os << p; }
// Outputs: {person: {"Harley ", "Quinn", 21}, 100500} std::cout << Prinsoner{{"Harley ", "Quinn", 21}, 100500};
I admit that the motivation for the flat reflection is much smaller than for the precise one, so in the end it may not be worth the effort. Regards, &rzej;
Andrzej Krzemienski wrote:
I admit that the motivation for the flat reflection is much smaller than for the precise one, so in the end it may not be worth the effort.
Flat reflection struck me as a legacy feature, one that exists only because it came first, and Antony has admitted that that's indeed what it is.
participants (4)
-
Andrzej Krzemienski
-
Antony Polukhin
-
Peter Dimov
-
Rainer Deyke