Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I just don't understand why some people are so fascinated by this. Can you all admit that this is not at all practical? I swear C++ folks like it for the sake of it. No other engineer do this. Only antiques people or whatever.

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?



> Why can't you just do this at the language level like any sane person?

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.


>Like how struct designated initialisers have to be in order of member declaration due to object lifetime rules or something like that.

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.


You still don't need to syntactically require same order initialization, it's an easy job for a compiler to reorder things so that all dependencies work out - every language with order independent declarations have to do that for example.


You don't need to require same-order initialization, but allowing people to do different orders will be confusing when actions are reordered behind the scenes. Especially imagine if there are dependencies between the objects you're passing in.

  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.


Scala does it by pulling out the keyword arguments to variables so that your example would become

{ 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.


In a general case you can't do that with separate compilation. [0]

  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/xMb64ssYK


Your code snippet does not use the designated initializer feature that this comment thread is talking about.

Furthermore your code possibly contains undefined behavior depending on the behavior of the constructor of A.


As I stated above, it's the same kind of situation. See my another comment in this thread for an example with designated initializers.

>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?


Actually, 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.

[0] https://eel.is/c++draft/class.cdtor#2


You can't easily take the pointer to an other member in a designated initializer. It's still a problem for members that are implicitly initialized by their default member initializer, but that can be sorted out.


I think it is easy enough to be a potential footgun [0]:

  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


That's a stretch to call it "easy enough", you are explicitly pointing the gun at your foot. That `b.a1` might not be explicitly UB, but that's quite suspect when b's lifetime didn't start yet. Accessing members through `this` in constructors have some special allowance to not make that UB.


Good point, I should've used direct initialization in this example.

  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.

https://godbolt.org/z/MPoqEhTvf


My gripe was not the form of initialization of the elements, but forming `b.a1` before `b`'s lifetime has started. It hasn't started before all of the elements are initialized.


But do we need the lifetime of b to be started? Isn't it enough that a1's lifetime is started? Taking of address of a1 happens after that. [0]

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]

[0] https://eel.is/c++draft/dcl.init.aggr#7

[1] https://eel.is/c++draft/class.cdtor#1


Personally, I write the simplest subset of C++ I possibly can at all times. Loops, variables, classes and the STL library when useful.

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.


I have almost entirely removed loops from my definition of the simple subset of C++. Turns out that in nearly every case anytime I think loop I'm able to find a STL algorithm that does the same thing in a more expressive way.

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.


>Also the more people get used to seeing it.

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.


Remember the master, no raw loops!. :)


No raw loops, no new/delete (malloc/free). The two keys to making C++ a better language for you. You can still run into a lot of foot guns, but the above rules two eliminate a large portion of them.


I do the same thing. C++’s feature is incredibly broad, and sprinkling in just a bit too much templating can result in code that is cleaner, but harder to reason about when reading it back after a month.

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.


>hides what the code actually does.

As does any function call.


God forbids! I want functions to hide HOW is it done, not WHAT. One thing is abstraction, the other is obfuscation.


When you add "actually" to "what" it becomes "how")

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.


>> I write the simplest subset of C++ I possibly can at all times. Loops, variables, classes and the STL library when useful.

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"


Those 1000 lines of what can be 10 in python are a good thing when the total code base is 10million lines and you need all those obscure corners. However many people hear that and decide that everything should be the 1000 line template/inheritance mess instead of 10 lines because their code base is multi-million lines line, when in reality most of your code will never need all the advantages the complex 1000 line solution gives.

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.


Reflection is spectacularly useful and sorely missed in C++

But because this is C++ the committee designs the most insane and terrible version of reflection possible


Unfortunely so it is with anything committee design, including SQL, C and Khronos APIs as well, it is not only C++.


Because they know the cost of everything but the value of nothing.


This is a nifty little gadget, not a production level feature. C++ lacks a lot of things, but it's objectively cool that you can hack some of them yourself (like kwargs). No one should use this in important code. This is just fun.


indeed


Apparently you have not been around many Lisp, Python, Ruby, Haskel, Scala,... folks.

This kind of geekyness is surely loved by many, not only C++.


Exploring like this is a way to get familiar with the feature, explore its limits and inform you on how to use it in the future.

It's curiosity, play and learning. It's great.


Honestly, all I ever want is to be enumerate a struct data member or an enum at compile time, and be able to get the name, type and value of each iterated member. That's it. That's all I want.


Would be happy with a enum that's a named key value.

    key_enum one_two_three 
    {
       FIRST = { .name = "first"},
       SECOND = { .name = "second"},
       THIRD = { .name = "third"},
       LAST = { .value = 255, .name = "last"},
    };


What is wrong with the following? Do you want type more type safety, e.g. linking the enumeration constants to the array?

  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 },
  };


You still need generic code to link the enum type to its format string, for example for use in std::format or std::ostream.

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.


I have yet to see a project where there have never been bugs due to someone forgetting to update the metadata attached with the enum when there's a change


magic_enum addresses your issue but will absolutely kill compile time on a large project due to recursive template instantiation.


I think there's a humour to C++ being like Python in that, left alone, the language's community will almost immediately try and stuff as much syntax as possible into the language.

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.


Reflection is such an insanely useful tool that pretty much all modern languages have it. As usual C++ is 20 years behind language design and brings terrible syntax.

But it is tremendously useful.

https://en.wikipedia.org/wiki/List_of_reflective_programming...


Isn't the reason why so many other languages have reflection due to the fact that they essentially have the information there from the get-go?

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 debt. When used correctly, reflection can be quite elegant, but I think those cases are seldom advantageous in comparison to a different and likely better design approach.


>Isn't the reason why so many other languages have reflection due to the fact that they essentially have the information there from the get-go?

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.


When I started out programming in C#, I used reflection sometimes to circumvent the language’s design and restrictions. This resulted in brittle and hard to reason about code. Reflection should never be used to do this.

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.


>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).


Takes a bunch of manual work per POD, that's the gist of it.

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.


This is absolutely possible, even currently - albeit in a very much non-portable way. For example boost::pfr and my own (wip) repr library have the required machinery for this.


Interesting! There seem to be a lot of limitations though:

> 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++ compilers also had this information since forever(basically reflection is giving to the end user some level of access to the AST nodes information) - the objections against reflection were always more political or about end-user design of the feature.


The article is about compile-time reflection, which is not nearly as useful as what the Wikipedia article you linked is about. It's only upside is that it is "zero cost" by some definitions of cost.

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 in the opposite camp, runtime reflection is useless, and almost always points to a design flaw while compile time reflection is actually useful (for obvious things like automatically building a serialization layer or an UI which represents the type, or building types from other types).

I'm sure that C++26 has implemented it in a way which is highly unpleasant to use though ;)


The element of runtime reflection that is the most useful is something along the lines of "here's function, here's a list of arguments, go call this function with those arguments".


Having to do this by reflection is a problem in principle solvable by types. Here is a function of type (A, B, C) => R, and here is a tuple of arguments of type (A, B, C), go call this function with those arguments.

Most programming languages make it hard to easily express those constraints so reflection is used instead.


Compile-time reflection is by far the most useful

It replaces the plethora of code generators that currently surround C++ like flies


You can build runtime reflection on top of compile time reflection, it's impossible the other way around. Compile time reflection is a superset of what runtime reflection can do.


We have add decent AOT compilation in Java almost since 2000, with Excelsior JET being one of the first vendors to offer it, but few are willing to pay for such tools.

Also I consider ART, GraalVM and OpenJ9 decent enough as free beer AOT.


The fascination i sort of get - sometimes rabbit holes are fun to jump down if you have time.

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 you will want to 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.


> C++ exposes all this complexity because it's not willing to break anything, ever.

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.


Python2-3 is a worst case example where the benefits were small or negative compared to cost.

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.


99% of "modern" C++ is just masturbation and trying to keep up with what other languages had 10 years ago. The introduction of new standards isn't improving any code, in fact it's just causing old code to rot faster as the gap widens between the latest standard and the standards companies actually use.

It's obvious to any developer with even a little experience that less is more, complexity is the enemy. C++ continually embraces complexity, it even appears to encourage it as an opportunity to show everybody else how smart you are.


i think it's neat that you can do that

i'm going to stick to C, to prevent it from compiling, though


It feels to me that C++ has long become a language for "academic" or compiler-minded people to try yet another thing just for the sake of it or because its fun. Yes reflection eases some issues we have on C++ but the syntax is horrible and clutters the language


At this point, you should just switch to JavaScript. The desperation to have the simple ease of JavaScript without having to say you're going near "that terrible language" is twisting C++ into ridiculous loops. I'm serious. It's like a bizarre Victorian relic now.


https://javascriptwtf.com/

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/


I'm not gonna claim C++ is a great language, just that JavaScript starts its weirdness "earlier"/"lower" than C++. Especially since a bunch of the issues listed on your linked reference are technically UB but compilers (a) can warn about it, and (b) have reigned back somewhat on exploiting UB for optimizations.

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.


The same tired edge cases. Especially the null pointers. Yes, but what did you want the language to do? Stop you? You can do that yourself.


Honestly? I want the language to add integers. I don't understand how anyone can take seriously a language that only has floats for numbers. It's straight up ridiculous.


Is JS simpler than Python? I don’t think so but willing to change my mind.


The syntax? Probably a little. It’s a typeless C. As for what you can use it for, it’s limited to browsers, practically, so not really comparable.


> it’s limited to browsers, practically

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.


We don't even write utilities in JVM languages, because spawning whole JVM is generally considered too expensive.

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.


> We don't even write utilities in JVM languages, because spawning whole JVM is generally considered too expensive.

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.


> Electron, like the JVM is nice and cross-platform with no BS.

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.


Oh it has its problems for sure. Especially the bloat.

I was just bringing up the similarity in ease of packaging.


Which uses JS made for a browser with some modifications.


Ah yes, the language that has no built in support to test if two non-primtive types are equal (& not just objects but arrays of primitives too), that has no proper integer type, whose standard library is thread-bare, and is an exercise in masochism if you don’t transpile it from TypeScript. TypeScript is at least somewhat livable but can we get beyond arguing which terrible language is better?




Consider applying for YC's Fall 2026 batch! Applications are open till July 27.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: