Erm... that's not just false. The point of templates is generic programming, reusable components. If you don't put them in a header, you're not reusing them much. And if you have to "selectively pick TUs where they're instantiated", you're basically admitting that you have to invest effort to reduce compile times. You are refuting the very point you're making.
C++ templates _are_ slow to compile. They require running something like a dynamically typed VM in the compiler.
**** Template sets that took longest to instantiate:
833 ms: sf::base::Optional<$> (911 times, avg 0 ms)
Each individual instantiation of this class is sub 1ms.
Including the header itself takes 3ms.
I'm sure I can optimize it even further if I wanted to.
---
Now to refute your other incorrect claims:
> The point of templates is generic programming, reusable components.
That's ONE use case. A more general use case is just reducing code repetition in a type-safe manner, which is extremely useful even within the same translation unit. Another use case is metaprogramming. And I'm sure I can come up with more. Templates are a versatile tool.
> And if you have to "selectively pick TUs where they're instantiated", you're basically admitting that you have to invest effort to reduce compile times.
...well, yeah? Of course you have to put in effort to reduce compile times. That doesn't undermine my point at all.
Not slow to compile? 0,833 seconds extra compile time for a trivial utility class that doesn't do anything interesting other than make something perceived "safer"? Does that mean that each of the 911 instantiations took several million CPU ticks? You could convince me that it's not slow if it was 2-4 orders of magnitude less.
As I wrote elsewhere, 1 second is a timespan where we could aim to compile 1 MLOC of code on a single core.
> A more general use case is just reducing code repetition in a type-safe manner
As I said -- code reuse. And interestingly your Optional.hpp is a header...
That's a strange dismissal. `Optional<T>` isn't "perceived" safety -- it eliminates a whole category of bugs (null dereferences, uninitialized reads) at the type-system level, with zero runtime overhead versus a raw pointer or sentinel value.
If you think that's uninteresting, that's an aesthetic preference, not a technical argument.
But let's set that aside, because it's also irrelevant to the compile-time claim.
The point of the example wasn't "look at this fascinating class," it was "here is a real template, used 911 times across the codebase, in a public header -- exactly the scenario you said would be slow -- and it costs under 1ms per instantiation."
You can swap `Optional` for any non-trivial template of similar complexity and the numbers will look similar.
On your 1 MLOC/sec benchmark: that's a fair reference point for C-like code, but it's not the right yardstick for template instantiation, which is doing semantic work (overload resolution, SFINAE, constraint checking) that a C compiler simply isn't.
Comparing them is comparing different jobs.
The honest question is whether template compilation is slow relative to what it's actually doing, and in well-structured code, it isn't.
And yes, `Optional.hpp` is a header -- that's the whole point of the demonstration. I'm not claiming you should hide every template in a .cpp file. I'm claiming that even templates in headers, instantiated hundreds of times, are cheap when written with compile times in mind.
The "put templates in .cpp where it makes sense" advice is for the specific cases, not a blanket rule.
Even days of compilation may be an acceptable price for good optimization, as long as debug builds or builds with minimal optimizations are fast enough.
To be honest there are ways to make that much nicer. I believe that if you use recursive macros using the VA_OPT feature, you should be able to provide enumerators directly to define enum as a list.
The underlying machinery implementation is going to be much uglier and complex, though.
It is kind of weird at first but the reason is that `std::vector` requires heap allocation and transient allocations are not allowed in `constexpr` contexts. The purpose of `std::define_static_array` is to promote the storage of the vector to static storage to eliminate the transient allocation issue, and so that the `template for` can work properly with it.
Is there a reason why `std::meta::enumerators_of`, a reflection feature that's surely almost exclusively going to be used in constexpr contexts, returns a value which doesn't work in constexpr contexts?
Just another example where C++ language features are incompatible with each other, to be fixed "in a later version" which may or may not happen. There are so many of those in C++. I desperately wish they'd just do it properly initially.
This cost is not significant nowadays, it's the frontend/parsing time.
You can also use `#pragma once` which works everywhere, is nicer, and technically needs less work by the compiler, but compilers have optimized for include guards since a long time ago.
Yes, I've heard that before, but comments like this one in your linked issue still make me wonder:
> at least for gcc and Visual Studio using #pragma once has a significant impact. The fact is, the compiler does not need to continue parsing the whole file when reaching a #pragma once. otherwise the compiler always needs to do it even if the include guard afterwards will avoid double processing of the content afterwards.
As written the explanation for these optimizationst suggest that both "pragma once" and include guard optimization still requires opening and closing the file each time an include is encountered, even if you bail after parsing the first line. Is that overhead zero? Or are the optimizations explained poorly and is repeatedly opening/closing the file also avoided?
Either way, do you know what causes the slowdown as a result of including <meta>?
The compiler doesn't need to open the same file multiple times. It can remember if a a file is guarded or not every time it sees its name.
My understanding is that this is an optimization that has been available for a very long time now.
The only issue is if a file is referred through multiple names (because of hard links, symlinks, mounts). That might cause the file to be opened again, and can actually break pragma once.
The overhead isn't zero, but with SSDs (and filesystem caches in the gigabytes these days) it's damn near insignificant in pure terms of opening files and such.
In short, it probes enum values in a pre-defined range (e.g. [-256; 256]), and parses the `__PRETTY_FUNCTION__` macro at compile-time to extract the name of the enumerator.
Once you have that in place, you can easily detect duplicates, etc...
It is "cryptic" and "ugly" to you just because you're not familiar with it. You'd pick the macro-based implementation because you are familiar with it.
Seeing this argumentation is so tiresome, because it feels like there is a lack of self-awareness regarding what is "familiar" and what isn't, which is subconsciously translated to "ugly" and "bad".
Have you ever used other (modern) programming languages ?
In a lot of languages, you achieve the same with 1 line of code. It's not about familiarity, it's about the fact that it's a long and convoluted incantation to get the name of an enum.
Why do I have to be familiar with all those weird symbols just to do a trivial thing ?
And what if you want to implement something like Rust's "derive"? That is what the article shows.
As far as I understand you would have to mess with individual parser tokens in Rust instead of high-level structures like "enum" (C++ reflection). It would be much, much uglier to implement anything like "to_enum_string" in Rust as you would have to re-implement parts of the compiler to get the "enum" concept out of a list of tokens.
What? No. Parent comment is comparing C++ to modern programming languages, showcasing how they provide commonly used utilities out-of-the-box instead of making every programmer re-implement them again and again and again and again and again.
So it is NOT built-in and the code example shown above is dishonest - @SuperV1234 compares how "lean" two languages are but conveniently hides half of the code in their preferred language to make it seem simpler that it actually is, as otherwise it would look bad in the comparison!
The comparison was about `to_enum_string` example, so I'm asking for exactly that! You can't just make up different rules, that's not how comparisons work!
First of all, the only correct way to use package managers is with validated internal repos, don't vibe install, that goes for node, and goes for C++ as well.
Second this thread was all about how code lands in one's computer.
So finally, it's NOT built-in, and the parent comment was showing that in other languages - it IS built-in. So your code example is NOT correct and comparison is NOT correct, because you just hid the most important part of it, which is the implementation, that the user has to either: a) write themselves, b) find somewhere on the Internet.
So? The original argument was about the "ugly" syntax that the user didn't want to interact with nor read. I proved that there's no need to do so to consume reflection utils.
No, it is objectively cryptic and ugly. I honestly don’t understand how can anyone keep up with this garbage, but the ship has sailed long time ago. It is just a soup of symbols at this point.
Java reflection is another beast altogether as it is runtime reflection. C++26 reflection is purely compile-time, which not only means it adds zero runtime cost, but also prevents those kind-of-insane use cases you see in Java and C#.
I think C++ devs have to eventually update their knowledge how Java and .NET work when talking about reflection.
Yes, originally they only supported runtime reflection.
Nowadays they have compile time tooling as well, via plugins, annotation processors, and code generators.
Which is exactly how you can have a Spring like frameworks that do all the AOP magic at compile time, for native code with GraalVM or OpenJ9, like Quarkus or Micronaut.
reply