Wow this got really long. I was one of the coauthors for a reflection proposal (N3340) over a dozen years ago. Implementing compile-time reflection is honestly trivial - you basically transfer data from the symbol table on-demand into template specializations. It was roughly 1500 LOC to modify g++ to do it.
Looking at the examples (https://isocpp.org/files/papers/P2996R4.html#examples) what really stands out is the direct integration of type-syntax into the language. It fits in with a certain token-substitution way that connects back to templates. It also replaces some of the uglier operators (typeof?).
I hope it goes int! During the language's stagnation I left for a while, perhaps it'll be competitive again soon.
I think another good example might be extracting function signatures (parameter names & types, default values -- if any) at compile-time for various purposes.
C++ has gotten a ton of quality of life features with each update. The issue is less that new features aren't coming and more that new features bake through countless iterations of proposals for close to or often over a decade until everyone in WG21 is happy.
So it's not that we aren't getting features. They are coming quite fast and people regularly complain that new C++ has too many things for them to learn and keep up with. The issue is that those are the same features everyone has been asking for for over a decade so the people that really care found workarounds and eventually move over to the new std way of doing things when they can while everyone else continues waiting for that one feature they really care about.
Also that the features c++ is getting are bolt on additions that we already have solutions for. I think fmt is a great example - fmt is a header only library that can be dropped in. Meanwhile std format was standardised without printing to stout. That took 3 years to standardise. Meanwhile we’re working on things like ranges, and instead of implementing them in the language it’s shoe horned in as a library feature - we now pay massive compile time hits for these features that are being shoved in alongside the kitchen sink. Meanwhile the solution (modules) has been talked about longer than I’ve been writing c++, it’s still unusable, and it hasn’t shown one of the key things people have been begging for for a decade - faster compile times.
I think the committee is focused on the wrong things
I disagree completely. Libraries like ranges are dumped into algorithm, and are de-facto considered parts of the language. Reflection has gone back to have range support added, for example. Another one is that span has a performance overhead due to it being implemented as a normal type. If it was part of the language rather than a library type, the compiler could make assumptions about it, but instead it’s treated equivalent to me writing it myself. I would much rather gcc saw me passing a span around and could treat it as a special built in type.
>Another one is that span has a performance overhead due to it being implemented as a normal type. If it was part of the language rather than a library type, the compiler could make assumptions about it, but instead it’s treated equivalent to me writing it myself.
False. Nothing prevents compilers from giving their own stdlib types special treatment under the hood.
That would be an ABI break which is just not happening, and you know it. As it is we’ve decided it’s more important to be able to use std span from libc++ on clang than it is to have an optimised version for people on their tool chain.
So you're saying that turning std::span from a standard library class into a language feature wouldn't break the ABI? How so? How would such a language construct fit into the existing ABI?
(for context: parent is referring to the fact that x64 calling conventions mandate structs larger than 64 bits to be passed in memory, which means that passing a 128bit std::span is going to be less efficient than passing a separate 64bit index and 64bit length, as those can go into registers)
No, the cat’s out of the bag with span (and unique pointer) now, we can’t go back. We knew this was a problem from unique_ptr and had an opportunity to not make the same mistake again, but instead we chose back compat for a new feature over something novel and performant
What is the issue exactly? Span is trivially copyable and destructible and, at least with the Itanium ABI, it can be passed (and returned) via registers: https://gcc.godbolt.org/z/4rbcshve4 .
Other ABIs might have different constraints but there is no reason why they couldn't special case std::span. In fact if span was a built-in type there is nothing preventing a compiler form picking a suboptimal ABI and being stuck with it. In any case it is not a standardization issue, but purely a QoI.
Yes, GCC can pass it in two registers. On the other hand Microsoft's x64 ABI doesn't:
>Structs and unions of size 8, 16, 32, or 64 bits, and __m64 types, are passed as if they were integers of the same size. Structs or unions of other sizes are passed as a pointer to memory allocated by the caller.
The parent commenter handled the ABI question for me, but I want to respond to :
> In any case it is not a standardization issue, but purely a QoI.
This is an enormous problem in the C++ ecosystem - playing hot-potato with whose fault it is, instead of trying to actually fix it. Span is a decent example, the standards committee get to say it's a vendor issue, and the vendors get to say that their hands are tied by the ABI. The result is that people spend time arguing about who should fix it rather than actually fixing it.
That's all well and good, but what would you do exaclty? The standard comitee cannot impose an ABI as it would be ignored by implementors. Implementors either own the ABI (MS for example) and are responsible for their owns screwups or there are other bodies that are responsible (the informal forum for the inter-vendor Itanium ABI for another example).
In any case this has nothing to do with std::span being an technically a library type or a built in. There is really no fundamental difference between the two.
For example std::complex and std::initializer_list have special handling on many compilers, just to mention two types.
> That's all well and good, but what would you do exaclty?
Start with having the standards committee accept that they are in fact where the buck stops, and that the language includes, whether they like it or not, the toolchain. They don't have to decide upon the toolchain, but their current MO of "toolchain/ABI issue == not our problem (except for when we decide we're not willing to make any backwards incompatible ABI changes, but only sometimes)." The vendors are already jumping through hoops to support what is being standardised (modules being the perfect example here).
I can't speak for std.complex as I've never had to use it, but I think initializer list would be a great example of "how much better would this be if it was special cased into the compiler". The benefit we would get from initialisation being consistent with the compiler far outweighs the benefit of being able to use libc++'s initialiser list with clang.
> There is really no fundamental difference between the two.
Except there is. If I write an implementation of the standard library, and provide an implementation of std span as (abbreviated) - https://gcc.godbolt.org/z/c1sz4neKG it's got to respect the various conventions instead of being treated as an opaque type (like a slice in go). If it's a `_Span`, the compiler is free to go "ok you're using this thing that I know all the internals of, and can reason about. I can elide bounds checks that don't pass muster, I can specify that I will generate code for this specific type that puts extent and data as registers in the following cases". But instead, on x64 (where I work 99% of the time so it's where my effort/knowledge is, sorry), we're bound by >64 == memory.
Now, you might call that a QOI issue, but I'd call it a design flaw that could have avoided an implementation issue, that we see on many features.
> except for when we decide we're not willing to make any backwards incompatible ABI changes
That's not an exception. The committee is not willing because the implementors explicitly said it is not going to happen, no matter how much Google cries.
> Except there is. If I write an implementation of the standard library, and provide an implementation of std span
if you write it as an user you are constrained by the ABI. But implementors are not: they can bless their own span with superpowers if they want to (in practice they would use special attributes). And there is no reason the compiler can't have builtin knowledge of the semantics of std::span (the same way it has knowledge of printf, malloc and the various math functions for example).
> But instead, on x64 (where I work 99% of the time so it's where my effort/knowledge is, sorry), we're bound by >64 == memory.
[Note this is an MSVC-specific ABI issue not a general x64 one. GCC uses the Itanium ABI on x64]
But the MSVC issue is really a red herring: there is a-priori no reason to expect they would have picked a better ABI for a built-in _Span. The committee cannot force compilers to be optimal (it can't even force conformance).
(Note I'm not singling out MSVC, GCC also has multiple less than ideal ABI decisions).
> The committee is not willing because the implementors explicitly said it is not going to happen,
Yeah, and I think this is the problem at the root of my gripe. If the committee was willing to reach this point earlier I think we’d be better off!
> But implementors are not: they can bless their own span with superpowers if they want to
Except that they don’t. And we can go around in circles here - I maintain this is a design issue, and it should be fixed at the design stage, rather than passed on to the compiler vendor who are stuck behind the theoretical design that pretends an ABI doesn’t exist, and their customers who will not upgrade if they break the ABI.
Lastly, I agree that the committee cannot force conformance or optimality, nor should they. But their unwillingness to accept that unless it’s technically impossible, the vendors will move mountains for conformance. This leaves us fighting with each other over who is to blame (see this thread), and in my opinion the end result is a half baked outcome that solves the paper problem but doesn’t solve the actual users wants.
>> But implementors are not: they can bless their own span with superpowers if they want to
> Except that they don’t. And we can go around in circles here
Except they do. Look at GCC code for std::complex compared to the equivalent hand rolled class:
https://gcc.godbolt.org/z/nqcvhPWex .
edit: note that in this case GCC is just silly with the hand rolled one, but it does show that they are treated differently.
GCC does similar things with span where the class has special annotations for reference tracking to improve warning messages.
Library vs builtin is an implementation issue, not a standardization one. But yes, we are going in circle.
You can't have faster compile times. If you want fast compile times C++ needs to sacrifice something else (ABI compatibility, compatibility to compile code from 1979 for some dead big endian 12-bit word chip, etc.
In fact the truth is the people working on C++ don't actually value fast compile times over other matters (fast runtime, ABI compatibility, etc.)
They say they do. But they don't in their actions.
One egregious example: look at the compilation speeds of clang with release build configs (-O2 or higher, etc.) over the years.
It compiles much slower now than it did in 2014 for the same code. The compilation speed is worsening at a much faster rate than the runtime speeds are improving from version to version.
I agree with you. I would move heaven and earth for faster unoptimizrd builds, but I’m perfectly happy with multi hour O2 builds from clang. The state that modules was standardised in proved what you’re saying - there were many opportunities to allow for faster compile times but instead we chose maximum flexibility with no consideration for what that leaves on the table.
Ha! Modules. I wrote a basic version of that in 2006 for Boost. This is as big a useless mess as web components. It feels like c++ is in the legacy category with x86, still here, tolerable due to exponential efforts, and still dying for simpler solutions that aren't beholden to decades-old design mistakes. (variable instruction width, context-sensitive grammar, #include model, etc).
I think that what we have standardised as modules give us a path off the include model, honestly. It’s just that it’s the most pandered-to version of modules you could imagine. We entirely changed the compilation model and didn’t manage to tie module names to filesystem paths, meaning you need to parse the module file to figure out what module it is. It’s death by a thousand cuts
Personally I'd rather the comitte take longer and require more implementation experience before accepting new features. There are still too many half-baked ideas that turn out to be mistakes afterwards, resulting in either needless breaking changes or being stuck with bad solutions.
This is especially true for library features where users can always use third party libraries for containers/algorithms that are yet to be standardized (or indifinitely - not everything should be added to the stdlib). But even language features can and should exist as compiler extensions before we are stuck with them.
Failed library features at least can be relatively easily deprecated, forgotten and left to rot on the side. Language features will bloat the language almost forever.
One nice advantage of static reflection, macros and other form of program synthesis is that you can experiment with new syntax as a library before committing to integrating it to the base language.
it was actually originally published in 01991 (at which point it was just the comp.lang.c++ faq), but cline continuously updated the faq-lite until 02012†; see the update history at https://web.archive.org/web/20140225204048/http://www.parash.... so it's entirely your choice to only look at a version of it from 01994; you can easily find him merrily celebrating c++'s boneheaded design errors continuously after c++98, after c++03, and indeed after c++11, although the document was looking increasingly old-fashioned
however, even if we only consider the 01994 version, i still extensively disagree with your comment, and i will explain why in detail
in fact c++ was already well defined before ansi formed their committee in 01990, four years before the version of that faq that you chose to read. the arm (not the arm arm, the annotated reference manual) was the standard standard until c++98, and it's written as one. it says things like
> The following rule limits the context sensitivity of the rewrite rules for inline functions and for class member declarations in general. A class-name or a typedef-name or the name of the constant used in a type name may not be redefined in a class declaration after being used in the class declaration, nor may a name that is not a class-name or a typedef-name be redefined to a class-name or a typedef-name in a class declaration after being used in the class declaration. For example,
(that's from §9.9 if you want context)
but why would it be relevant whether there was a c++ standard or not? it would be relevant if the problems documented in the 01994 version of the faq-lite had been eliminated by standardization. but that is comprehensively not the case.
every single one of the misconceived features in the 01994 version of the faq-lite is still in everything resembling a c++ standard, so all the pitfalls of poorly interacting features it documents are still relevant today. that's why i think that even the 01994 version of the faq-lite is relevant
so from my point of view it is not only wholly false to say that there was nothing resembling a c++ standard in 01994, it is also irrelevant whether there was anything resembling a c++ standard when the faq-lite was written (at least for 01994 and later versions)
furthermore, your particular choice of 01994 is unjustifiable and evidently only serves to provide a spurious veneer of justification for your irrelevant objection that (interpreting you generously) the faq-lite predates the c++98 standard. if you thought that was important you should have picked a version of the faq-lite from 01999 to criticize
______
† after a hiatus, since 02019, it is again being updated at https://www.parashift.com/c++-faq-lite/ but obviously the new center of gravity for this kind of thing is stack overflow and cppreference
> Or stopped writing C++, I'd consider myself one of these for many use cases I used to use it for.
Some use cases like GUI programming sound like they are better addressed by specialized tech stacks. Nevertheless, either you're talking about greenfield projects or you are hard pressed to find a justification to rewrite a project in another framework. Claiming you stopped writing C++ doesn't fit the bulk of the experience of anyone maintaining C++ projects.
My experience maintaining old codebases is that you are just as hard-pressed to find a justification to write code to use new language features, or even to take the time to upgrade the language and compiler version. Most often you just continue writing code in the same style as the rest of the code base using an old version of the language and runtime.
> My experience maintaining old codebases is that you are just as hard-pressed to find a justification to write code to use new language features, or even to take the time to upgrade the language and compiler version.
That's perfectly fine. You should only pay for what you use.
Your average project, however, consumes dependencies and needs to keep them updated. Just because the code you write doesn't use them that doesn't mean your dependencies don't. So everyone still benefits with each release of C++ even if the are not using fancy features.
> They are coming quite fast and people regularly complain that new C++ has too many things for them to learn and keep up with.
I never got this. Can't you just decide to use subset of the language?
No-one forces people to use every single feature. It's okay to use C++ like "C with classes" and occasionally cool new thing, when it is right tool for the job.
Only people where this argument is truly valid are compiler/tools people.
> Can't you just decide to use subset of the language?
No, you can't. Not in the long run, at least. You will have to use libraries that have chosen a different subset, new people will join your team, things will get sloppy as the company grows, etc. Committing to a subset of the language is not free, it's hard and it's a lot of work. And for what?
> Can't you just decide to use subset of the language?
That is harder than it sounds. First you have to select which subset to use, it is almost impossible to get any two engineers to agree on that - if you do get agreement that is a bad sign - generally it means the people in the room don't know C++ well enough to make the decision and so you choose the wrong one. The best you can hope for is a compromise where nobody is happy - and thus everyone will look for an excuse to bring in their pet part of C++ not in the subset because there is a "good reason" for an exception.
Even if you select a subset it is almost impossible to enforce whatever subset because even if you don't allow your people to use it directly odds are you bring in a third party library that does use it (the C++ standard library being the big one!)
There are a few exceptions to the above. No exceptions/no RTTI are commonly disabled exceptions and so you will get some compiler and library support. Game companies commonly write their own standard library. Both of these are done for performance reasons and have specific domain specific reasons that can be validated in a profiler to set their rules.
Not related to reflection, but C++26 is also likely to get profiles which will disable specific old constructs, (new/delete) which are proven to be error prone and thus result in security issues. These are a good subset to use, but it is about security and so mostly doesn't get the types of subsets you are talking about.
You can still enforce a style guide and limit certain constructs via code review.
Plenty of the modern C++ people already do this by enforcing things like "no raw loops", "no raw pointers", or "no raw synchronization primitives" during code review.
The issue is that it's a lot harder to justify avoiding new features than it is to justify avoiding old features unless you have a tooling specific reason (ex lack of support) to do so.
There is the problem - code reviews are done by humans and so it is really easy to read some code and not think "wait this is new code it can't do that anymore". I read a lot of old code as part of my job and it often isn't worth the bother to update working code to new standards.
> it's a lot harder to justify avoiding new features than it is to justify avoiding old features
The problem is the opposite - people keep using old features when the new is better. I realize not everything new is better, but most of them are, yet people keep refusing to learn and use the new thing without good justification.
> The problem is the opposite - people keep using old features when the new is better. I realize not everything new is better, but most of them are, yet people keep refusing to learn and use the new thing without good justification.
I think we are saying the same thing here. Often the new is better so it's hard to really justify sticking to the old outside of project specific reasons (i.e. toolchain doesn't support new std). That people do it has less to do with justification and more to do with time commitment or laziness and the excuses given tend to fall away once pressed.
Personally I think this is true. C++ is multi paradigm and can be effectively used with many different subsets of the language and those subsets still interact well.
However that opinion is kind of a minority. There are a lot of people in the community who don't want to have to learn new features just because a dependency happens to use/expose them. I don't personally see the issue with it.
I'd rather learn std features any day over non-std features. It's just a better use of my time because they work everywhere and someday I might need them. However again not everyone shares this opinion.
or people who need to maintain someone else's code, debug their own, write a library someone else might use, or understand compiler error messages, all of which involve understanding language features you don't yourself use (at least intentionally)
If you're writing library code that someone else might use, you don't have much need to understand the features you don't use, unless you have to handle them at the interface. If you're debugging your own code, you really shouldn't have to understand features that you didn't use. (Mostly - see the next paragraph.)
You did say "intentionally". You could wind up using a feature unintentionally, but it's not very common, because most of the new features are either in a new library (which you have to explicitly call), or a new syntax. There are definitely exceptions - I could easily see you using a move constructor without meaning to.
Maintaining someone else's code... yeah. You have to understand whatever they used, whether or not it made any sense for them to use.
i accidentally used the new implicit constructors for aggregates in c++ the other day, and then my code didn't compile with the version of clang i have installed on my cellphone
LOL. What used to be a typo is now becoming valid syntax (and of course the compiler can't warn you because it's now valid). Ouch. At least an old compiler saved you...
Tbh this is why you need to set the C++ std version when you compile. Don't just assume the default for the compiler but hard lock it at a specific version so you can know which compilers you can support and compilers can warn you if you use new features.
i set it to c++20 on purpose, i just didn't know the feature existed (and didn't know that my clang didn't support that feature; i'm a little uncertain as to whether my clang is an outdated version without full c++20 support, or whether gcc is implementing a proposed extension that didn't actually make it in)
Yeah, I think gcc has a std c++20 mode as well as a with extensions mode. And adding `-pedantic` helps because it forces extensions (unless specified in the std type) and non-conformant code to be rejected.
And that clang likely just didn't have full c++20 support. Which tbf I actually don't think any clang has full c++20 support currently as even bleeding edge clang still is missing a few things in the lang and library departments.
Wish C++ fixed some of its mistakes in the standard library. std::regex is embarrassing when even Python can beat it and nobody uses std::unordered_map due to its pointer chasing. Basic Maps are something that you shouldn't need to use a third party library for.
The standard guarantees that you can get a pointer to an object in the map and this pointer will remain valid after rehashing and insertion/deletion of other elements. That basically forces implementers to use “buckets with linked lists”, also known as separate chaining. This is not cache friendly (and also suffers from excessive allocation). Other hash map implementations like abseil's don't provide this guarantee so they can put everything right there inline, which is much more efficient.
I think the bureaucratic cadence of the things also make C++ not a unified entity. C++ is patchwork language. So many of the new features don't work well together or don't fit together or they have conflicting goals.
Since the proposals target problems with differing philosophies, they each have different traps in them from bad time complexity to outright unrefined behavior. Keeping up with the updates hard because of this.
I think many C++ projects are (or will be) basically infeasible to maintain not because of the old problems but due to the exploding complexity of the interactions of all features, unless developers actively ban using large parts of the language.
> C++ is patchwork language. So many of the new features don't work well together or don't fit together or they have conflicting goals.
I don't really see this as true. In my experience most C++ features actually "just work" together and there are relatively few footguns involved in mixing features.
And it's less that C++ is a patchwork language and more that it is multi-paradigm and multi-discipline. Some features have specific applications and they get used inappropriately but in my experience that is solved with a quick reference/citation of the standard during code review or in a new ticket.
N3340 is from 2011. Prior to c++11 they had failed to deliver major changes to the language. And arguably the disfunction is still there where big ideas get destroyed in committee (reflection) or take forever and come out half-baked (modules).
While I love this paper and this proposal in general, as a C++ developer every time C++ adds a new major feature I get somewhat worried about two things:
1. how immense the language has become, and how hard it got to learn and implement
2. how "modernising" C++ gives developers less incentives to convince management to switch to safer languages
While I like C++ and how crazy powerful it is, I also must admit decades of using it that teaching it to new developers has become immensely hard in the last few years, and the "easier" inevitably ends up being the unsafe one (what else can you do when the language itself tells you to refrain from using `new`?).
I think the focus on smart pointers is a huge mistake. Code bases using shared_ptr inevitably will have cycles and memory leaks, and no one understands the graphs any more.
Tree algorithms that are simple in literature get bloated and slow with shared_ptr.
The only issue with pointers in C++, which C does not have, is that so many things are copied around by default if one is using classes. So the way to deal with tree algorithms is to have a hidden tree with pointers and a class that wraps the tree and deletes all dangerous copy methods, implicit and explicit.
shared_ptr isn't the only smart pointer and definitely shouldn't be used for everything. The default should be unique_ptr which is simple to reason about and a huge improvement over only having raw pointers that may or may not be owning what they point to.
unique_ptr is often too restricted. If you have a simple tree with unique_ptr, already the back edges need to be raw pointers to avoid cycles.
If you add an iterator, the iterator needs internal pointers to the nodes, so by definition the node pointers are not unique. Again, raw pointers are better.
I have never seen a complex data structure where unique_ptr is really used.
Yes, smart pointers don't mean you can entirely stop thinking about ownership and lifetimes but they let you express that ownership in an explicit way with some compiler-enforced protections against mistakes.
> I have never seen a complex data structure where unique_ptr is really used.
What's a "complex" data structure? Anyway, I'd expect to see unique_ptr more in user code rather than in library implementations of data structures where where relatively minor concerns might warrant an ad-hoc implementation even if you could use unique_ptr. In many cases it's probably just that the implementations precede the standardized smart pointers.
You can just not point back to parents (which is rarely needed for anything but iterators) and to not use iterators (since they're a pain in the first place!). The visitor pattern exists for a reason.
It is always weird to me when people have a big problem with C++ smart pointers but think that Rust smart pointers are the bee’s knees.
It is just a different syntax for the same thing.
I honestly think people just find the words “unique ptr” scary or syntactically overwhelming. If that is true, fortunately we also have using aliases :)
There are all sorts of issues with pointers (and not just in C++) - which are inherent to their use. They can point anywhere! They're mutable! They can be null! It's difficult/impossible to ensure they hold a valid value!
Common wisdom is to avoid excessive use of pointers when you don't _really_ need - even smart pointers.
Use references, especially as function parameters,
* Returning values in modern C++ typically does _not_ involve any copying.
* If you want to indicate the possibility that your value is uninitialized/invalid - use std::optional<T>, which can hold either an actual T value or an std::nullopt (being in an "empty" or "value-less" state).
* If your data is in some buffer (or span, or vector etc.), you can use offsets into that buffer.
* Many uses of pointers are due to the "need" to utilize polymorphism: myobj->foo() . Well, typically, you know the real type at compile-time, and can write a freestanding foo() function, which is polymorphic via overloading, or being templated over its parameter's type.
* And speaking of virtual methods and class hierarchies, you can often make do with a template parameter instead of a choice of subclass; or with an std::variant<Foo, Bar>
You actually think managing memory manually using new and delete is easier than dealing with occasional problems with leaking memory using shared_ptr? That seems pretty ridiculous to me.
naertcxx point is that you shouldn't use shared_ptr for your next ptr in your list (or other node based container) node. It is slow and error prone. Instead the list container itself should own the nodes and delete them on container destruction. I think it is a good point. unique_ptr is better, but still not always ideal.
Shared pointers are _great_. But, yeah, shared pointers don't _solve_ memory management problems and people still need to understand how pointers work and memory lifetimes. This continues to be hard for a lot of people. :/
I agree with this. Python is now more complex than C++. Python looks as if it is simple, because the syntax looks clean.
If you read the new "compilers" [1] in packages like PyTorch, which are unfortunately written in Python, you stare at a huge code base with walls of text, objects calling one another in a maze of ravioli code and generally no help at all to make sense of it all.
Compare that to the gcc code bases, where it is always possible to find some entry point to understand the whole thing.
[1] "compilers", because despite the huge code base (wasn't Python supposed to be terse?) they preprocess the function graph and then call g++ and Triton to do the actual work.
I very much have a love/hate relationship with python, but it does have a significantly milder ramp for beginners. The complexity is relatively well hidden. I can recommend python as a first language to someone that wants to learn to program. I can't do that with C++ with a straight face, unless one is especially interested in the areas were C++ still dominates.
Yeah that's because you're not supposed to be using "new" anymore since the introduction of smart pointers in
C++11. Std::shared_ptr and std::unique_ptr are preferred. Shared pointers ref count and auto-delete, and unique pointers can't be copied.
Thoughts on Zig? Just not popular enough to fit the bill or are there technical reasons?
I bring it up partially because they are not taking a "rewrite it in Zig" approach, they are specifically aiming for incremental migration in mixed C / Zig codebases.
IMO zig is neat but not that much different (for better or for worse) than C and C++. I do really like the comptime concept and wish C had it, and I like it that allocators are explicit in interfaces. There are other things I don’t like about it and I don’t think it has much chance to supplant C or C++.
Zig is basically Modula-2 for C afficionados, those of us that are confortable with C++ hardly have any benefit from it, those improvements are also on C++ type system.
Do I understand correctly that this proposal does not include annotations (i.e. attributes).
More specifically, with this I can iterate over struct members and get their names and types, but I cannot attach additional information to these members, like whether they should be serialized or under which name.
The referenced proposal P1887R1 covers this, but that's not included here, right?
Can I ask a naive question that consists of two parts and please don't flame me? lol
* What type of problems static reflection could solve, in general?
* Are there specific cases and / or situations where static reflection could resolve such case, even simplify an unnecessary complexity?
Of course not. We must involve 10 layers of templates that mere mortals cannot read and compilers cannot process in reasonable time so the academics at the committee will be happy.
Addressing real problems with simple solutions isn't allowed.
So our team switched to vcpkg recently and, while it has improved certain parts of our dependency process, it has also made other parts more complex. Notably when something suddenly goes wrong it is far more complex to figure out what actually happened (Though to be fair a lot of these issues also come from combining vcpkg with cmake). This led to most of my team revolting against vcpkg and now it looks like we might go back to just vendoring our libraries again.
I suppose I just yearn for an all-in-one build system + package manager like exists in Rust or Go. Once you've seen what can be possible when these things are integrated from the ground up it sort of ruins your C++ build experience!
What are you talking about? At runtime, an enum value is just an integer. You need to look up the enum case that corresponds to your integer in order to convert it to a string. Which is precisely what the code above is doing.
Sure, but reflection requires some support from the compiler either way. There's no reason why if you have an expression like x.str, where the compiler can see that x is an enum, that it can't rewrite it into a table lookup like __Enum_strings[x]. This would work even if x is an unknown run-time value. This is basically what any other language that supports converting enums to strings natively does. I understand that the C++ committee prefers to delegate work to the standard library, but in this case it's stupid. Reflection needs support from the compiler. Just add new syntax and put it on the compiler, ffs!
So your gripe is that you have to write `enum_to_string(x)` instead of `x.str()`? And this is of such importance, that this needs to be included in the C++ language itself, as a special case of the dot operator. Correct?
>Reflection needs support from the compiler. Just add new syntax and put it on the compiler, ffs!
Converting enums to strings is one use case for reflection. Do you suggest introducing bespoke syntax for the other use cases too?
>So your gripe is that you have to write `enum_to_string(x)` instead of `x.str()`? And this is of such importance, that this needs to be included in the C++ language itself, as a special case of the dot operator. Correct?
My gripe, if you will, is that converting an enum value to a string is a basic feature (as in, not reducible to other features) of every language that supports doing that. Not everything should be part compiler part library. And it doesn't need to be bespoke syntax. Enum values are already objects from the point of view of the compiler. Just give them a str member. This is similar to how in Rust built-in integers also have members. It's not bespoke, it's using already-existing syntax for objects and extrapolating it to other types. Another alternative that wouldn't involve bespoke syntax would be giving enum values an operator const char *() overload, either explicit or implicit.
>Converting enums to strings is one use case for reflection. Do you suggest introducing bespoke syntax for the other use cases too?
The other cases are pretty much all variations of enumerating members of a class. I have no problem with those examples, since it's basically how it's done everywhere. You get a meta object for the type that a function that enumerates the members of the type, and you go through it.
So your problem is that C++ prefers free functions instead of these pseudo-members?
Because other than the free function vs member access thing, I don't see why it would concern the user of `enum_to_string()` that it's a proper function instead of a keyword like `static_assert`...
Yeah, C++ enums are just numbers that are really good at pretending to be types. And for that reason they're not actually objects that contain things like their name. And they probably shouldn't, in the vast majority of cases it would be an utter waste of space to store the names of enum members along with them. So you have compile-time reflection for those instead. And yeah, you could implement some kind of thing pretending to be a member but actually being a reflection thing but that's both horrifying and limited so C++ takes the reasonable approach of just adding general reflection in instead.
The main canonical use case for static reflection is serialization, where serialize<T>() can easily have a default case of calling serialize() on each of the fields. In the more general case, you basically want to have some library method that does something based on the structure of a struct or class, without having to define some sort of explicit, intrusive interface that said struct or class implementation has to provide.
Does static reflection simplify such cases? ... Outlook unclear. It's definitely gnarlier to actually write the serialize() method, and in many cases, it does feel like a better option is to write a specific domain-specific language to specify what you want to specify, with a tool to operate on it as appropriate (think something like protobufs for serialization).
Any sort of reflection brings C++ one step closer to Python.
Implementing serialization for complex types often requires manual code writing or external tools. With static reflection you could automate this process
This is the thing that's driving me away from C++ very quickly. A big part of our code base is code that handles this, and it either has to be in a DSL and constantly recompiled or we have to make a bunch of boilerplate. It's a huge problem for the language not to be able to do this.
I have a scripting layer in a game that needs to set properties in a C++ Model. I used a single Boost.Describe macro per struct and a generic get/set property. It worked very well and made me get rid of a lot of boilerplate.
Serialization is largely a Solved Problem in modern C++ thanks to template metaprogramming. Wrangling a kludge DSL instead of Serialize<T> betrays poor lang knowledge...
It absolutely is. The root problem is that the language itself doesn't permit you to make statements about your constructs without learning an entirely different set of tools. Whereas other languages have these tools as a core component. Templates seem like they were added to resolve a bunch of problems with the macro system. You can see that in the earliest use of templates to eliminate a lot of boilerplate around containers that in C you might have used the preprocessor to do.
From there a lot of functionality for making statements about what something is has been bolted on to the side of what is really a very sophisticated type-aware preprocessor, and it shows. It's very painful to use it when you go from the core C++ language to the template, because the semantics are very different. Which the same can be said of C++->Preprocessor.
I think proper reflection should be a core part of the language that can be evaluated at compile-time with simple constructs. It's hard to articulate specifics but I think this proposal greatly simplifies working with type metadata to the degree that it's an approachable problem without using someone else's library. This proposal seems to do that in my opinion, and I think even some of the weaker programmers I've worked with could use this effectively.
From a quick look, generalized constexpr evaluation, but in practice it relies on parsing non-portable decorated function names from things like source location. An ugly, slow, but effective hack.
Maybe this doesn't count as static, but I used to regularly use reflection in C# to generate code for interacting with foreign DLLs.
This was a video game mod, essentially. I needed to create a text interface to modify settings for any other mod that might be installed. Other mods would simply implement a settings class with certain attributes, then I could list out all fields and their types. The list was processed into a sort of tree presented through the chat interface. From there I can generate code to modify that settings class from outside its assembly and raise value change events.
The reflection part of that was extremely simple, but just because that's how C# works. C# makes a task like this almost trivial.
At my current job, we have a similar thing. Classes decorated with attributes. We inspect them and check the generic type they implement. This way we register message handlers by their message type dynamically. You write a handler class and it simply works.
Windows Forms had a PropertyGrid control which did the same thing as my text interface, but with a grid of properties you can edit freely.
Most of this stuff is typically done at runtime. But you could have it be static if you wanted. A precious job did this to access the backing array inside of a List<> object. I offer no explanation or excuse for that one.
They exist, for instance https://github.com/beached/daw_json_link , uses mappings of JSON <-> class members/construction. It handles a bunch of types out of the box and allows custom types to be mapped. Also, it integrates with some of the reflection like libraries out there now(Boost.Describe/Boost.PFR) and will be trivial to add C++26 static reflection when available.
Because the library doesn’t need to allocate unless the underlying types being parsed to do, it has been constexpr since C++17 too.
A bunch of people have used libraries that use macros for reflection like or PFR to integrate other C++ JSON Libraries too.
Without static reflection, we need to write a lot of boilerplate code, and those boilerplate is not guaranteed to stay in sync with the struct definition. That is why static reflection is sorely needed.
Even static reflection isn’t a panacea and we have it now the libraries like PFR that give us a reference and name of each member for aggregates. But this is the same problem for anything else using those types too. For non-aggregates we need reflection or some manual library.
I am not disagreeing, just more saying what we have now.
I have been waiting for static reflection for the last 20 years. The current proposal seems quite nice, but the real question is whether any non trivial usage will kill compilation performance.
That doesn't mean much unfortunately, Clang had a fully working C99 designated initialization in C++ for many years, but the designated initialization that ended up in C++20 is only a butchered version of the full feature set despite Clang clearly demonstrating that it's possible to integrate the full C99 designated init feature set into C++.
Finally. I think there have been proposals since C++17 at least, and all I really wanted is for them to solve the common problem of basic static reflection for enums (without hacks like magic_enum uses).
magic_enum works by walking all possible enumeration values from one-by-one in a wide range at compile time, instantiating a function template for each one so it can extract the __PRETTY_FUNCTION__ name, which is very slow. The C++26 feature just directly returns the vector of the named enumerators in one go, so it should be way faster.
They have a reference implementation on godbolt under clang, so you can play around with that. I did not try it yet.
Wow. I'm trying to make some of these template instantiations explicit on a large project I'm on as magic_enum is one of the largest contributors to our build-time.
It's nice to know I can just transition to C++26 to fix this.
Do you think you could try my library [1] and let me know how it performs in comparison? I've been curious about its compile-time performance, but I've never tried to compare its performance against that of magic_enum.
If you're using C++20 or above, you could try conjure_enum (https://github.com/fix8mt/conjure_enum) It's based on magic_enum but optimized for C++20. Not sure about compile times although in our testing and with our test users it hasn't been reported as an issue.
Yes, there is magic_enum already - and we based this implementation on the core of magic_enum, but refocused for C++20, using some of the key features of this language version such constexpr algorithms, std::source_location and concepts; we also improved on and expanded the API.
This looks surprisingly fine! The opaque, extensible types remind me of Win32 with its extensibility through ever new message types. The syntax looks better than expected, too - well, it's better than templates...
Yes, that's a clever way of demonstrating viability (and with more than one compiler implementation).
I do like the examples that I see there.
This seems like the kind of language feature that I might not make much use of directly in general application code, but would wrap up in utility functions or use via lower-level libraries that the application code builds on. E.g., they showed command-line parsing, but I could also see this for benchmarking and testing frameworks to automatically find the workloads and tests in a non-hacky way.
I also wonder about how this feature interacts with translation units or modules and linkage, though. I'm reminded of the static initialization order fiasco; this seems like it might open up issues that make that look tame by comparison. (Not a complaint; I'm actually looking forward to this.)
I am a big defender that the only way to fix many of the mistakes that ended up in the standard is to adopt the same policy as other languages, papers without preview implementations shouldn't be accepted.
There are still a few gotchas being ironed out, there was a talk at ACCU about many corner cases.
But the `member_number` functions in § 3.2 look disturbing to me. It's not discernible how invalid arguments are handled. Normally I'd look at generated assembly to answer a question like that, but this probably doesn't make sense with compile-time-fu (`constexpr`)…
Having implemented reflection in languages like C(++), before, it is most certainly not bloat-free. There are sorts of 'obvious' things programmers do (like enum-reflection) that end up injecting strings all over the place. The overhead is (worst case) proportional to the source-code size, in those cases. In other cases, you end up with bloat proportional to heavily-utilized template libraries. However, unless the reflection system is very clever, i.e., exposes enough information to the linker to strip duplicate-ish symbols, you end up with a bunch of reflection copies.
I always thought it’s good practice in C/C++ to have only one translation unit in release builds e.g. SQLite amalgamations, instead of relying on LTO. It also speeds up compilation because it isn’t recompiling the same header files over and over again.
And while it helps with compile time (especially with mediocre compilers like MSVC where a large time is spent in the front-end), it doesn't help with stripping unneeded code/data compared to LTO.
It does significantly speed up clean builds in many cases, to the point that those clean builds can even be faster than many "incremental" builds in some cases.
I do not know of any compiler that can compile different parts of a translation unit in parallel. But plenty of systems (including the venerable `make -j`) can compile separate translation units in parallel because they are independent.
You don't want to clear caches for release builds. That just makes release builds unnecessarily slow and impedes the flow of Continuous Deployment. You just need to have separate caches for different build flags, including a dedicated cache for release builds.
extern templates address the issue of having multiple instances of a template being expanded inline, with only minimal mess and fuss. (A way to prevent inlining of templated code would have been nice too).
RTTI is a super vital feature for deserializing without having to generate code or write a ton of tedious boilerplate. I'd be very happy with RTTI in C++ and my life would be instantly easier if there were RTTI in typescript so I didn't have to use any of the hacky solutions out there for deserializing JSON on backends without RTTI.
I suppose C++'s template system might be able to generate JSON deserializers with static reflection as well
You don't need RTTI to deserialize data in a clean way. What you need is return-type polymorphism. Haskell has this and it makes writing serializers and deserializers symmetric and totally painless.
Return type polymorphism and inheritance doesn't mix very well.
Swift got into this mess early in it's lifecycle and it's type checking is still more expensive than the rest of the compiler combined, and unpredictable on top of that.
C++ has both return type polymorphism and inheritance. What it doesn't have is the kind of type inference that Haskell has. Its type inference is very limited.
That is still polymorphic, but as I mentioned C++ does not do the kind of type deduction that Haskell supports so you do have to explicitly instantiate the template. However, you can instantiate it based on context, for example using decltype.
You certainly can and I know there are several JSON libraries that do it. The gist of it is to write a struct and overload the conversion operator for the set of return types you want to support, for example overload the conversion operator to int and std::string. As soon as the instance of that struct is used in a context that needs an std::string, then it calls the string overload.
> I suppose C++'s template system might be able to generate JSON deserializers with static reflection as well
It definitely can, and it will be faster and more type-safe than doing it at runtime. But if you do want to do it at runtime, well, it's possible to implement runtime reflection as a library on top of compile-time reflection, so someone will probably do that.
I haven't touched C++ since undergrad. Neither have I written any Qt code. But from memory, doesn't Qt's moc implement some of this stuff because it wasn't available in C++? Could this replace moc?
Qt's moc can already be replaced and it increasingly is being relied on less and less as time goes on but dropping moc requires dropping all lower C++ standards or maintaining separate moc and modern-c++ versions.
And while it can currently be replaced with templates alone in fairly old versions of C++ (C++14 is the oldest I think), compile times are egregious unless you use very new, shiny features.
And as much as I am pro "move to new shiny C++", one of the big commercial uses of Qt is in semi-embedded applications like car entertainment centers where you are stuck with whatever (often outdated) toolchain your SOC source relies on. So pushing for shiny new Qt risks either splitting Qt in half or abandoning a lot of very well paying users.
Qt has straight-up dynamic reflection. You can get pointers to functions from strings, and such. This is just static reflections (which is still very useful!), so it's not a complete replacement. Even if it was, I would Qt would replace its build system.
I've always wondered what's the point of "replacing moc". I mean what's the problem with moc? It's required by Qt, and completely transparent by the build system. You don't even know it's used.
I mean, GCC also has some helper tools used to compile C++ code and we don't talk about "replacing them".
I'm surprised at the positive response in this thread. I find the syntax of this beyond atrocious! My goodness C++ really does not know how to do anything simply does it?
Many C++ features are useless outside of writing libraries, but your typical developer is going to be forced to understand how they work at some point. The result is just a burden.
What does this have to do with reflection. Also why do you need networking in the C++ standard library? Networking is neither something that is relevant as a vocabulary that needs to be common between libraries nor is it something that makes sense to be set in stone like basic algorithms. Just use OS interfaces or a third-party abstraction FFS.
Looking at the examples (https://isocpp.org/files/papers/P2996R4.html#examples) what really stands out is the direct integration of type-syntax into the language. It fits in with a certain token-substitution way that connects back to templates. It also replaces some of the uglier operators (typeof?).
I hope it goes int! During the language's stagnation I left for a while, perhaps it'll be competitive again soon.