But because this is C++ the committee designs the most insane and terrible version of reflection possible
But it is tremendously useful.
https://en.wikipedia.org/wiki/List_of_reflective_programming...
For example, C# and Java have their intermediate language that has a bunch of meta information attached to it already or the info can be added easily if need be. Is C++ not more raw in its compilation process from code to object files? I mean sure, I guess meta-data could be added to the object files themselves for runtime inspection, but if it were really that simple, why wouldn't they have just added it if reflection is indeed that much of a sore spot for the language's purpose?
Now, I'd like to add, my understanding of compiler internals is quite limited, so if I'm way off base here, please correct me.
Additionally, while I too find reflection useful, I've seen a lot of cases where it ends up being a bandaid for poor design and gives people an all too enticing shovel to dig themselves deeper into a hole of technical date. When used correctly, reflection can be quite elegant, but I think those cases are seldom advantageous in comparison to a different likely better design approach.
So I do think you’re right that reflection can (and will) be abused by beginners if present as a language feature.
With that said, I don’t know much about the internals of the C++ compiler, but having built a simple reflection system for C++, I think the important thing is just being able to serialize and deserialize POD structs to and from some representation (e.g. json). For more advanced data, such as images or specific binary (file) formats, it’s easier to write custom writing / reading, encoding / decoding logic.
Why not just use a visitor? (Genuine question, not c++ snark).
Fundamentally, if I have struct { int x = 3; std::string y = "bla"; }
I want the json to look like { x: 3, y: "bla" } without writing any code specific to the struct, and have it work bidirectionally. This is currently not possible because of a lot of reasons, but most trivially because the names "x" and "y" do not even exist anymore in your compiled code.
> Boost.PFR library works with types that satisfy the requirements of SimpleAggregate: aggregate types without base classes, const fields, references, or C arrays:
And in general seems to be dependent on C++20 for getting field names.
Do you know how this works? Initializer lists seem somehow involved.
C++26 reflection is compile-time reflection, not runtime reflectiom, it doesn't require any extra information in the binary. It just uses information that the compiler already has at compilation time.
Runtime reflection on the other hand is useful but it also puts up obstacles not easy to overcome. For example it is one major reason we still have no decent ahead-of-time compilation in Java.
I'm sure that C++26 has implemented it in a way which is highly unpleasant to use though ;)
Most programming languages make it hard to easily express those constraints so reflection is used instead.
It replaces the plethora of code generators that currently surround C++ like flies
Also I consider ART, GraalVM and OpenJ9 decent enough as free beer AOT.
The more advanced features you use, the easier it is to make subtle mistakes, confuse someone who hasn't seen that feature before, or just make everyone working on the software wary of ever touching it.
For example, using C++ 20 ranges over simple loops is an example of where brevity and “cleanliness” can reduce clarity because the heavy templating and operator overloading hides what the code actually does.
On the other hand, sometimes one can’t avoid the complexity when writing a library that aims to work on many platforms, and with special tricks such as SIMD, as is present in the Eigen library.
As does any function call.
What the code using C++ ranges is doing is quite clear, but understanding how it is doing it requires some knowledge of how ranges work.
That stating that is not stating the obvious, shows how screwed we are. Indeed, why use things that are not necessary? "Just because you can"?
And still I find everywhere programs less than 1000 lines of code, that can be written maybe in 10 lines of python, but in C++ they use templates, inheritance, and the most obscure corners of the STL. Mind you, I do not mean some random github repo. where somebody was just practicing and learning. I mean production software, made "professionally"
There are places I have that 1000 line template mess that nobody can understand in less than a month of staring at it (including me and I wrote it!) because it is the right answer. Most of the time I don't write that because it is hard and there is no advantage, but once in a while it is the right answer to a complex problem. Those 1000 lines can turn many many 100 line problems into 5 line problems and so overall be a net reduction when done right.
The longer I write code the less use I have for inheritance. Templates are still useful.
I do use lambdas all the time, they are very powerful and so worth learning the complex syntax to make them work. I strongly recommended you add them to your list of things you use all the time (replacing loops)
I use templates, but only when I'm writing generic code. Templates in the right place can save a lot of code and errors. However they are rarely needed and so you only need a few people on a large project to write them, and everyone else can say "not in my subset of C++, talk to [someone else]"
> more advanced features you use, the easier it is to make subtle mistakes, confuse someone who hasn't seen that feature before,
Also the more people get used to seeing it. Soon everyone knows those things, and so they won't make the subtle mistakes of be confused. Of course you need to pick the right things to add. Ranges look really useful for people who work on a different type of problem from the type I normally work on - thus ranges are not in my subset of C++, but my impression is they should be very common in other areas. Modules are not currently on my list, but everything I know suggests in 5 years the tools will (finally!) work and I will be converting my code to modules. I'm not clear where reflection sits, I suspect like templates a few experts will be needed on a large project and everyone else just uses them - but only time will tell.
This is a great idea in theory, but doesn't always hold up well. With a sufficiently large and old project, and when people move on and off either parts of the code or the entire project regularly...it doesn't hold up as well.
No one may see or touch that code again for years. By the time someone realizes there's a missed edge case or it needs expansion, it may simply be too late. The person that worked on it may be gone completely or haven't worked on that part of the code in those years.
key_enum one_two_three
{
FIRST = { .name = "first"},
SECOND = { .name = "second"},
THIRD = { .name = "third"},
LAST = { .value = 255, .name = "last"},
}; enum { FIRST, SECOND, THIRD };
struct { char *name; int value; } table[] = {
[FIRST] = { .name = "FIRST", .value = 0 },
[SECOND] = { .name = "SECOND", .value = 1 },
[THIRD] = { .name = "THIRD", .value = 3 },
};Nothing really difficult, normally all of this is done by hand, which is tedious and error prone, or incorporated in a macro. It would just be nice if there was a standard provided turn-key solution to the problem.
Arguably this is worse than C++ because for basic things/beginner to intermediate users, the footguns in C++ are more benign (e.g. performance related, rather than correctness) than the ones in JavaScript.
> the footguns in C++ are more benign
I don't think I can agree. https://pvs-studio.com/en/blog/posts/cpp/1215/Now that I think about it, the fact that you necessarily have to use a compiler with C++ is also part of the equation here. It's basically a mandatory linter. With JavaScript as with any interpreted language there's a lot of mines you can trip over waaaaaay down the line, especially if you aren't taking steps to use linters.
That massively ignores node.js and all of the many popular backend frameworks built on it today. JS might not be everyone's cup of tea, but it is certainly not limited to browsers, practically.
Yet there are people who will happily spawn whole web browser and even go as far as recommend doing so. All because they can't even write a left-pad function.
This is definitely not why, at least today. People are even willing to spawn an entirely new userspace (docker) for their utilities.
> Yet there are people who will happily spawn whole web browser and even go as far as recommend doing so.
Electron, like the JVM is nice and cross-platform with no BS. The only problem with the JVM is that it doesn't have a lot of important features/historical advantage that web tech does, not to mention the difference in quality between web UI and Swing, JavaFX, etc,. The combination of all of this is pretty much why electron is preferred. And I guess I should say chrome-tech instead of web-tech.
> All because they can't even write a left-pad function
Well, they certainly want the functions they write to work on every possible combination of OS/userspace.
If we were talking in person I'd look at you like you had 2 heads now. In my bubble it is extremely en vogue to hate on Electron being a piece of bloated shit…
…but then again, I will agree that this is a piece of social signaling and would need some actual evaluation. There's probably some real problems with Electron that have caused people to start hating on it, but who knows how relevant those are. Trivial junk can easily snowball into social signals like this.
I was just bringing up the similarity in ease of packaging.
The reality is C++ is a ridiculously complex and legacy-ridden language, with a difficult goal to preserve backwards compatibility. I haven't read the history on the keyword args proposals but I'm guessing they were declined due to a deluge of silly edge case interactions with C++ semantics that became too hard to work around. Like how struct designated initialisers have to be in order of member declaration due to object lifetime rules or something like that.
I would recommend trying to not be outraged at the state of C++ these days. It's time to stop hoping that C++ gets the nice features we need in any sort of reasonable manner. The reality of the language is not compatible with much niceness.
It matters in which order sub-objects are initialized - if you have a class with the members A and B, and B takes pointer or reference to A in its constructor and does something with A, A better be already initialized. Sub-objects are initialized in the order of their declaration and having different order of designed initializers would be confusing. In fact, exactly that problem we have in C++ with the list of initializers of base classes and data members in constructors.
struct A { A(A*);};
struct B {
A a1;
A a2;
};
void f()
{
B b{.a1 = A(nullptr), .a2 = A(&b.a1) };
}
[0] https://godbolt.org/z/cGaxzh17T struct A {
int x;
A(A *a) { if (a) a->x = 42;}
};
struct B {
A a1;
A a2;
};
void f()
{
B b{.a1{nullptr}, .a2{&b.a1} };
}
This code is valid.Now if I change the struct definition to
struct B {
A a2;
A a1;
};
it will become UB. Luckily it won't compile because of the difference between the order of declaration and the order of designated initializers.The alternative way is to always initialize the sub-objects in the order of designated initializers (what do we do if not all initializers are provided?), but this would mean that the order of constructor calls wouldn't match the (reversed) order of destructor calls. Or we would need to select the destructor dynamically based on the way the object was initialized.
Upd:
There is an interesting sentence in [class.cdtor] but I don't think it applies here because B has no constructors:
"For an object with a non-trivial constructor, referring to any non-static member or base class of the object before the constructor begins execution results in undefined behavior."[1]
struct A { A(A*); };
A* f(struct B *b);
struct B {
A a1;
A a2;
B(): a1(f(this)), a2(f(this)) {}
};
//in a different translation unit
A* f(B *b)
{
return &b->a1; //or a2, we don't know
}
[0] https://godbolt.org/z/xMb64ssYKFurthermore your code possibly contains undefined behavior depending on the behavior of the constructor of A.
>your code possibly contains undefined behavior
Only if f() returns a pointer to a2 (which is my point). Or did you imply that in the case when f() returns a pointer to a1 and it gets passed to the constructor of a1, provenance matters?
"During the construction of an object, if the value of the object or any of its subobjects is accessed through a glvalue that is not obtained, directly or indirectly, from the constructor's this pointer, the value of the object or subobject thus obtained is unspecified." [0]
Reading an unspecified value isn't UB, that's good, but I don't understand why the standard says 'unspecified' because it clearly can be indeterminate if a sub-object hasn't been initialized yet.
struct A {
B one;
C two;
};
struct B {
B() {
cout << "init B" << endl;
}
}
struct C {
C() {
cout << "init C" << endl;
}
}
Mixing up the order is confusing: A{.two=B(),.one=A()}
since `two` is initialized after `one` despite coming before (the comma operator <expr a>, <expr b> usually means `expr a` happens before `expr b`.This case is a little contrived, but run the evolution forward: you can have members that depend on each other, or have complex initialization logic of their own. There, debugging the specific order of events is important.
{ val x$1 = B() val x$2 = A() A(.one = x$2, .two = x$1) }
This maintains left-to-right evaluation order while allowing you to pass arguments in any order.
There is probably some dark and forbidden reason why C++ can't do that.
ETA: That's basically what the post does.
This kind of geekyness is surely loved by many, not only C++.
It's curiosity, play and learning. It's great.
I'm working on a satisfactory node modeling tool for fun, and built one of the stores (I have a few different stores) using CRDT's and added collaborative support over webrtc. Is it practical? The result, yes. The process - no, it's a total waste of time and energy compared to much simpler alternatives. No question.
Is it fun? Yes. It has been a lot of fun.
Was it fascinating - yes, i learned a lot.
So the fascination i get - sometimes people do this stuff to learn more about it, or because they are jumping down a rabbit hole they like, or whatever.
At the same time, i totally agree with you that this article presents something wildly impractical as-is. But i'm not sure it was meant to be practical. I hope not, since it totally isn't :)
For C++ itself, this is why Google, and others, gave up (on the evolution of the language) after decades of trying. Or at least one reason. C++ exposes all this complexity because it's not willing to break anything, ever.
If you go look at the reflection paper, they use one single opaque reflection type, and the whole justification is that baking it into the language is dangerous because we might have to change it and that would break things. They give an example of something standardized in 2003 that would have broken if they gave more specific types. Note this is now proposed for 2026. To me, 23 years before breaking something would be a pretty reasonable thing. Especially if migration can be automated. Worse, this logic unfortunately can be applied generally - if you are never going to break anything, it severely restricts your ability to generate more useful interfaces, because you can't depend on anything that might ever change.
Which is of course, a losing war anyway - you won't accurately predict all the things that will change, so either you get it wrong anyway, or you now actually block the language by what you did choose to depend on. So the interfaces will only get worse over time, as you are either restricted by the existing stuff not changing, or because you guessed wrong and became one of the reasons those existing things can't change. This has a very predictable end.
For every other language, they would just eventually break things, and figure out how to do effective language migration. When they do, you hope it's for something better enough to be worth it, but that's a product problem, and that the language migration takes care of ~all of it for you, which is an engineering one.
That is also why people use C++ for large complex problems. Not doing that is why python3 took forever to replace python2. When you have a lot of code that works it isn't worth the money to rewrite it if you can help it.
There are also plenty of very successful languages used for large complex problems and have broken compatibility in various ways over the years.
You are right nobody wants to pay to rewrite it but if the tooling takes care of it for you nobody cares about it.
Python 2-3 had tooling that, for larger customers and projects, often could not do even 50% of the conversion, and when it came out the other side, nobody felt it was really better.
i'm going to stick to C, to prevent it from compiling, though
The difference is how much power the community has to turn these syntactic fever dreams a reality. With C++, you get first-class stuff like the STL and even the C preprocessor, and now reflection. With Python, it usually comes down to magic method abuse (see pathlib's use of `__div__`), maybe messing around with metaclasses, and the PEP process (though I think they're pumping the brakes at this point -- walrus was fun, but I really hope PEP 750 doesn't make it in, and it seems to me that it'll stall).
You technically have `ast`, but that'll only go so far since unless you really go overboard with your hack, you still have to write valid, parseable Python before `ast` will ingest it.
Can you imagine an engineer that is adamant on using his mystifying bespoke tool instead of just using a ruler. "But what if I have to measure it in the 4th dimension!?".
I was expecting something simple but good Lord, its kwargs. Not some magical asynchronous runtime.
inb4 there are still corner cases so you can't just say "users don't have to know the implementation details so only one person has to suffer". I bet money this abstraction is leaky.
Why can't you just do this at the language level like any sane person?