Three new utility functions in C++23
mariusbancila.ro
mariusbancila.ro
1.) Coming from embedded programming, I can see the utility of `std::unreachable`. But shouldn't this be a compiler directive? Or a standardized #pragma? Can someone more knowledgeable in C++ say whether using functions as markers is a common mechanism in std:: ?
2.)Maybe the example is bad here, as it doesn't even save typing. (18 chars for `std::to_underlying` vs. 16 for `static_cast<int>`. The 'old' variant seems more expressive to boot. If anything, how about `static_cast<auto>`? or <underlying> ...
3.) The usefulness of `std::byteswap` to convert data to network byte order seems trivial vs. the venerable old `htonl` family of functions. `std::byteswap` seems more like intrinsics meant to expose possibly present target machine instructions to the user. Like `std::unreachable` this is probably of most use to embedded / low-level programming. This may be a deficiency in the article...
If it's not obvious, I'm not a big C++ fan and read the article with C-tinted glasses :)
From a compiler perspective, using a function makes the most sense, since that fits into the existing control-flow analysis that the compiler will do. Pragmas are processed by the pre-processor, so they aren't appropriate for expressing control flow hints.
2. `std::to_underlying(t)` is a wrapper around `static_cast<std::underlying_type_t<std::remove_cv_t<std::remove_reference_t<decltype(t)>>>>(t)`. So a lot fewer characters. It's also useful since `std::underlying_type_t<T>` behaves weirdly if `T` is not an enum type.
I think you are maybe missing the context that C++ allows the representation of an enum to be defined, e.g. `enum class X : unsigned char {};` vs `enum class Y : unsigned long long {};`. So you can't always cast to `int`. Technically, this isn't the case in C either: the type defaults to `int`, but the compiler will pick a larger type if necessary, e.g. `enum Z { a = ((long long)INT_MAX) + 1 };`
3. `htonl` are not standardized, so they were not part of the C++ standard library. Also, on Windows, I believe you'd need to include `winsock.h` to get access to them, which has its own idiosyncratic issues. You are also missing the context of C++ defining operator overloading, so you can call `std::byteswap(0ull)` and get an `unsigned long long` and you can call `std::byteswap(std::uint16_t{0})` and get a 16 bit unsigned integer.
[0]: https://stackoverflow.com/questions/60802864/emulating-gccs-...
I was questioning the motivation of this to facilitate network-byte-order issues as stated in the blog post. The proposal[1] (that the post linked to) also confirmed that the motivation was to expose more machine code intrinsics rather than deal with network byte order like I expected.
Concerning 1.) yeah, I guess I'd prefer a #pragma aesthetically, but didn't think about the fact that it wouldn't be exposed to the compiler.
Thanks for the well thought out reply!
[1] : https://isocpp.org/files/papers/P1272R4.html#motivation
if you read this and alarm bells didn't ring in your head I really invite you to immediately go check any enum you may have defined in your code because this is absolutely false with MSVC in C++ (https://gcc.godbolt.org/z/6bqW9rE81) (and C is often compiled as C++ on windows)
struct A;
enum B; // as you pointed out, not allowed by the C++ standard
// fine to declare, define, and invoke
void fooA(struct A*);
void fooB(enum B*);
// ok to forward declare, but you can't call them
void barA(struct A);
void barB(enum B); enum class A; // defaults to int
enum B :int;
enum C;
fooA(A);
FooB(B);
FooC(C);
fooA((A)0);
fooB((B)0);
fooC((C)0); // error
but indeed GCC refuses plain enum. Forward declaring enums in C is an old GCC extension that still deosn't allow to pass them when incomplete;Enums in GCC do always default to int or unsigned int, unless -fshot-enums is used which is ABI breaking.
Probably I was misremembering this combination of the int ABI and the forward declaring extension.
This is wrong a lot of pragmas, I would even say most pragmas are not handled by preprocessor. Some examples: pragma pack, warning control, pragma GCC unroll, per function optimization setting changes, all the pragmas which 1:1 map to c++11 style attributes. None of that is handled by preprocessor, pragma once seems like rare one which is. Yes all of them are compiler specific, but handling compiler specific behavior is one of the main purpose of pragmas.
void foo();
#pragma unreachable
class bar
{
#pragma unreachable
};
namespace foobar {
#pragma unreachable
}
But pragmas can generally be used anywhere, barring tokenization issues. The end result is that `pragma unreachable` would likely end up turning into a magic function call inside the compiler, since it really only makes sense in a spot where you can invoke a function.Also, I think you are conflating pragmas with `__attribute__`, which is how you set per function optimization settings. If you do that with pragmas, then it isn't limited to a single function.
This seems par for the course for all C++ stuff: it's designed from a compiler perspective, not from a programmer perspective.
The correct way, IMHO, is to design features that supports the user's workflow, not to design the same feature in a way to make the compiler's job easier.
> `htonl` are not standardized, so they were not part of the C++ standard library.
They're POSIX standardised. The decision should have been to adopt something that exists in an existing and widespread standard rather than the worrisome not-invented-here syndrome that I see here.
They are: https://pubs.opengroup.org/onlinepubs/9699919799/functions/h...
I don't thing this is remotely true. C++ pragmas were designed with the express purpose of providing additional information to compilers.
I'm not so sure attributes are better. They have political traction, but that does not mean better. All major compilers use pragmas effectively to implement custom compiler flags. See for instance how Visual C++ uses pragmas extensively to toggle specific compiler warnings, not to mention the infamous #pragma once
I can believe this is useful in explicitly-typed form, i.e. using std::byteswap<T> with T specified. But letting T be inferred seems quite dangerous: C++ loves changing integer types around all by itself (via type promotion, for example), and byteswap<int> and byteswap<long> are (on UNIXy systems) simply not the same operation. For that matter, byteswap should really only be used on uintN_t.
byteswap(a+b) is just asking for trouble.
They are still far from being the only game in town.
1. Why "should" it be something different? It is semantically part of code flow; making it a pragma breaks that model. This replaces the nonstandard __builtin_unreachable().
2. This doesn't exist to save typing. static_cast<auto> doesn't make sense to me (that reads like a no-op). static_cast<underlying> introduces a new reserved word which is a big no-no.
3. This is not the same as htonl, which only does anything on little-endian machines. (htonl is also a POSIX function, not a C++ function.)
In practice what is actually available and whether it works satisfactorily varies considerably more than for the hosted environments. Almost everything is up for grabs and so you probably can't rely on the standard much. Your compiler vendor probably couldn't care less what the standard says anyway.
Probably "committee pragmatism", a stdlib change might be easier to get approved than a language change, and compilers already have builtins for this, they're just not compatible (but the differences can be wrapped in a macro, and since C++ doesn't like to expose macros, it's probably still a macro, but hidden inside a stdlib template).
FWIW it looks like C23 will also just get a macro:
https://thephd.dev/ever-closer-c23-improvements#unreachable-...
Another way of thinking of this: With NDEBUG undefined, the sequence `assert(condition); if(!condition) action();` is perfectly reasonable and equivalent to the naked assert. But, because assert(false) is defined, defining NDEBUG changes the semantics of the code, with the two examples having identical behavior when NDEBUG is unset, and different behavior when NDEBUG is set. This is contrary to my understanding of the intent of NDEBUG, to suppress debugging information without changing semantics.
A third view: With NDEBUG undefined (which is normal for test and development), /I have no way of testing what happens past an assert(false)/. If I then define NDEBUG for a release build, I'm by definition exposing my user to untested code. The compiler might (with a defined assert(false)) define the behavior of this new code, /but I cannot, since I can't test it./ It just makes more sense to acknowledge that anything past an event horizon is undefinable, and call it undefined.
https://github.com/dspinellis/unix-history-repo/blob/Researc...
Yes.
2.) static_cast<int> is shorter, if you know that the underlying type is int. But even if you know, you might not want to spell out int, to be more robust to code changes, which might involve a change of the underlying type of the corresponding enum.
In generic context you might not even know the underlying type.
3.) I absolutely agree. I think it was a mistake to include. The included `std::byteswap` can be expressed in terms of `std::ranges::reverse(std::as_writable_bytes(obj))`. It could be quality of implementation detail to get a bswap instruction from the latter.
I would be happy with equivalents of the `htonl` functions in the standard library, but I have strong opinions of the appropriate function signatures of it for C++.
The issue is exactly which code pattern is detected by which compiler varies a lot, so to avoid having to rely on that there was a strong push to add all these explicit intrinsics. Also std::bytesewap is more readable than the longer reverse+as_writeable_bytes.
byteswap semantically works on the object representation of the integer, while all other operations semantically work on the integer value, expressed in powers of two.
The C++ language has no strong requirements on the object representation of the underlying integer. The C++20 guarantee of two's complement also only just expressed in terms of integer value, not bit representation.
Therefor `byteswap` somewhat sticks out. It's possibly useful for `std::endian::little == std::endian::native` or `std::endian::big == std::endian::native`, and for `std::has_unique_object_representations_v<T> == true` (aka. no padding bits) for the corresponding integral type T. CHAR_BITS also change the semantics.
So, in this sense it is quite low level, and you have to check all of these to make use of it, or you have to implicitly rely on implementation defined values of these for the targets you care about.
I'm not saying that byteswap is not useful though. But it has the wrong interface. It reverses sequence of bytes, it does not work on integers.
edit:
Oh, in addition of having no padding bits, you also want no trap representations. I don't know if you can check for that.
edit2:
Apparently, you don't necessarily need to check for padding bits, byteswap does it for you, and fails to compile if there are padding bits:
https://eel.is/c++draft/bit#byteswap-2
I don't see trap representations being handled at all. This might be a defect.
> In generic context you might not even know the underlying type.
Thanks for pointing this out! I'm aware that the underlying type of an enum isn't always `int`. This is what I meant by saying the `static_cast` feels more expressive to me, i.e. reading the code, at least I'll now on sight what type is being used here. I'd probably have to 'let go' more to effectively use CPP, but this category of mechanism to cope with a generic/type system so complicated that I can't figure out the type of anything anymore. In the same vein, 'making code more robust to changes' feels like kicking the can down the road (to me). At some point you'll need to deal with the actual type ...
Lisp perspective: anything that can be a function almost certainly should be a function (and not a special operator or macro).
std::unreachable() does not have any arguments; therefore it doesn't need any special argument evaluation semantics that would require a compiler built-in.
The way C and C++ work, compiler directives are keywords and not identifiers. Keywords are not namespaced. Introducing a keyword called "unreachable" is problematic; far more so than a new element in the std namespace.
My only problem with std::unreachable is that I would never use it over std::abort.
Nobody needs a function whose only job is to invoke undefined behavior (from which it is then assumed that it is not reached).
It's a good cold day, so I can almost hear the Rust people laughing in the distance.
The only difference is that it’s marked unsafe.
https://doc.rust-lang.org/std/hint/fn.unreachable_unchecked....
There is a safe version that is just a wrapper around panic! with a standardized message.
Again, I'm not up on the latest papers, but that was the situation last I checked.
auto x = (y == 1) ? foo :
(y == 2) ? bar :
...
std::unreadchable();Nor is it unprecedented to have a standard library function triggering UB when called - it's just that this one has a precondition that's always false, so it's always UB.
std::unreachable() goes a step further, and tells the compiler that it doesn't even have to emit the call instructions or any instructions leading up to it.
But I don't see a reason why this is not be possible with an attribute? (I realize this is a purely aesthetic call from me .. a function that doesn't actually get called seems out of place). My guess would be that using a function allows niche compilers that don't support attributes to support `unreachable` and is easy to implement for compilers already using __builtin
That's not what I or the person I was replying to was saying, and wasn't the relevant point.
We were saying how else could you give the compiler an intrinsic, without a call of some kind? Like what other syntactic mechanism is there you can treat as an intrinsic in the compiler.
The alternative would be a new keyword, but then the go-to complaint would just be about c++ keyword bloat and source compat regressions
i.e., the functions documented in https://man7.org/linux/man-pages/man3/endian.3.html (why oh why are they not also documented in the GNU C Library Manual...)
Maybe having something like convert_be() and convert_le(), one of which is a no-op and the other does the byteswap (depending on your arch) would be better. It removes the duplication of e.g. htobe() and betoh() which are exactly the same function, while allowing the caller to not worry about which architecture their code is compiled on.
https://bloomberg.github.io/bde-resources/doxygen/bde_api_pr...
auto to_network_endian(auto std::integral value) {
if constexpr (std::endian::native != std::endian::big) {
return std::byteswap(value);
} else {
return value;
}
}
ntoh is left as an exercise :).edit: in practice I think std::endian and std::byteswap is a compromise between those that wanted a simple {to,from}_network_endian and those that wanted strongly typed wrappers to prevent mixing object with distinct endianess (a-la boost::endian). As the commitee couldn't reach consensus, this is the compromise and you can build your own thing with these portable bits.
For instance... /usr/include/x86_64-linux-gnu/bits/endian.h has:
#define __LITTLE_ENDIAN 1234 #define __BIG_ENDIAN 4321 #define __PDP_ENDIAN 3412
Apparently this is called 'middle-endian'... yikes!
On the other hand I sort of like having one function that's clearly used for importing values to the host's byte order, and another for exporting values from the host's byte order. But maybe that's just because I'm used to having them...
The benefits are obvious: Code is declarative, and there's no risk of a bug where you missed to call std::byteswap() or did it twice.
Also, the compiler could automatically extract and insert values from/to little-endian and big-endian bitfields (which can be a handful...), and it could optimise to reduce the number of byteswaps in the code.
Is that generically useful enough to be part of the C++ standard? Seems like the type of thing that more belongs in a helper utility (aka, surely something like Boost already has this)
According to Stepanov generalisation is something you are supposed to discover as you develop a program, not something you do from first principles.
I suspect 99.9% of uses of std::unreachable would be better replaced by abort. (There will be those times when the code is correct and the optimisation gains are worth it -- but they will be rare).
I felt physically ill when I read:
It’s intended to be used when you know you have an execution path in your code that cannot be reached but the compiler cannot figure that out.
It felt like saying to the compiler, "please, find a way to make my program break even more easily". Exactly not what I need.
But for my purposes I much prefer to stick something which flags me (exception or logging or whatever) of "this should never happen" instead of crashing. (Undefined.)
I'd rather have solid logging and/or abort handling than some extra yak hairs shaved off the speed on my release builds. I'm weird that way.
An abort would make them obvious, surely unreachable makes them less obvious?
So this is a “hide bugs but maybe improve performance” function.
1. Null pointers are never dereferenced.
2. Ptr *p is dereferenced.
3. p therefore cannot be null.
4. Ergo, we can omit checking whether p is null.
If the initial premise isn’t actually true (i.e., you slip up and dereference a null pointer), the chain of logic breaks down and boom! Applying many such rules can certainly lead to weird emergent behavior—and maybe you should act as if anything can happen—but it’s not a total free-for-all.
Compare these two blocks similar to the article
switch (ch) {
case 'a': do_a(); return;
case 'd': do_d(); return;
// ch is guaranteed to be 'a' or 'd' by previous code.
default: assert(0);
}
switch (ch) {
case 'a': do_a(); return;
case 'd': do_d(); return;
default: std::unreachable();
}
If the programmer is wrong about `ch` in the first one, the program terminates.
For the second one, the compiler could change it to be equivalent to if (ch == 'a') { do_a(); }
else { do_d(); }
return;
If the programmer is wrong here, the program might `do_d()` with unintended consequences. I'd say "going down unintended codepaths" is typically considered worse than crashing.N.b. fixed last code example- thanks afiori.
So what does std::unreachable() do here? In this particular case, and with NDEBUG defined and any level of optimization selected, I suspect that, at a minimum, the switch would be replaced as you have shown in all versions - it would take a more complex example to show how std::unreachable() makes a difference. The point is, now we have a choice - and it is one that is being offered without creating any backwards-compatibility issues.
Furthermore, the original function, without assertions, is not guaranteed to crash, with or without std::unreachable(). You need some explicit checks to get a desirable response in the case where a mistake has been made, and that option is just as available whether or not you use std::unreachable().
Therefore, while I agree you have shown that not all broken variants of a given program are equivalent, this does not show that std::unreachable() is harmful.
> For example, without the __builtin_unreachable in the example below, the compiler assumes that the inline asm can fall through and prints a “function declared ‘noreturn’ should not return” warning.
void myabort(void) __attribute__((noreturn));
void myabort(void) {
asm("int3");
__builtin_unreachable();
}It actually can (Hint: what happens if the operating system `iret`s from its int 3 handler?), although it's probably not a issue in practice. Regardless, you don't need __builtin_unreachable to write:
void myabort(void) __attribute__((noreturn));
void myabort(void) {
asm("int3");
myabort(); // might need `return myabort();` to force TCO,
// but gcc doesn't like that and it should work anyway
}
# Assuming tail-call optimization etcetera, this produces:
myabort:
int3
jmp myabort
which is a correct implementation.However, if you're implementing built-in/standard functions like abort, you presumably know what compiler you're using and don't need a std interface in the first place. There's zero legitimate reason to use a undefined-behaviour-based `unreachable` in application code.
Claiming std::unreachable is useful for implementing abort is like proposing a std::manual_copy function because your compiler optimized a implementation of memcpy to a call to itself - at some point you do in fact have to resort to implementaion-specific details to define the abstractions that abstract away said details, and "in literally the same function as the (also-nonstandard, IIRC) inline assembly that hopefully doesn't return" seems at if not noticeably past that point.
One example was interesting, though. I could see someone believing these might produce the same optimized assembly (-O2):
void a(int& x, int& y) {
if (&x == &y) __builtin_unreachable();
x ^= y; y ^= x; x ^= y;
}
void b(int& __restrict x, int& __restrict y) {
x ^= y; y ^= x; x ^= y;
}
However, it produces: a(int&, int&): # @a(int&, int&)
mov eax, dword ptr [rdi]
xor eax, dword ptr [rsi]
mov dword ptr [rdi], eax
xor eax, dword ptr [rsi]
mov dword ptr [rsi], eax
xor dword ptr [rdi], eax
ret
b(int&, int&): # @b(int&, int&)
mov eax, dword ptr [rsi]
mov ecx, dword ptr [rdi]
mov dword ptr [rsi], ecx
mov dword ptr [rdi], eax
ret
Is this something compiler contributors would optimize once they know about it? Similar question probably exists with using `__builtin_unreachable` if values aren't aligned versus `__builtin_assume_aligned`.I'd love to see these things illustrated with more real-world examples.
The typical usecase would to wrap this in a #define, so that it aborts on a debug build but you get faster code in a production build.
In debug mode it makes a lot of sense to replace the __builtin_unreachable with an abort() though.
The rational is better explained there :)
Most programmers will write an error message and halt execution flow (return EXIT_FAILURE, abort(), exit() or even an exception). The hint about assembler give cares about special (maybe optimized or machine specific) code or embedded stuff. And the second example refers explicitly to functions which do exit() and never return actually. The committee tidies up stuff. Something which compilers provide individually is becoming standard.
Optimization is a very large part of the reason for having undefined behavior at all. Viewed from that perspective, I don’t see how the existence of std::unreachable is at all odd. Also, it’s not like anyone is required to use it.
Only in the parts specified by the protocol (headers etc). I encourage everyone sending data over network in a novel way to just use little-endian.
Actually, for wire encodings, there is, although I've (I-think-)literally never seen any proponent of big endian bring it up (versus the bullshit "it's human-readable" nonsense[0][1]): big endian encodings of unsigned numbers have lexicographic order that matches their numeric order.
The most obvious concrete example of why this is useful is a keys-sorted encoding of a hash table: if you encode keys in size-type-value format, you can check sortedness by lexicographic order of type-value strings (which means you can add new types without old software needing to know how to compare them), and you'll get integer keys in inspection-friendly numeric order rather than semi-random order. (Encoding negative numbers with a type id of T_UINT-1 lets you extend this to them as well.)
At a more abstract level, where (zero-padded) little-endian numbers have the same value at different granularities, this means that big-endian numbers have invariant lexicographic order at different granularities: two strings viewed as bits, bytes, or uint32s are consistently in the same order.
You can kind of use reverse-lexicographic order for some of this, but there are obvious problems with sending data in value-type order rather than type-value, so forward-lexicographic tends to be strongly enforced.
0: "You mean for arabic numerals, except not actual arabic numerals, because Arabic is written right-to-left, so the numbers are little-endian there, but ended up big endian because they stayed least-signifiant-digit-right rather than least-signifiant-digit-first when imported into Latin."
1: "Also, so (supposedly) is decimal and sign-magnitude, but we've (agonizingly slowly) learned that those aren't good ideas."
That seems like a glaring footgun to me, to the point where I think I must be missing something.
What I want when dealing with endianess are "from_little_endian/to_little_endian", "from_big_endian/to_big_endian" function pairs that expand to either nop or a byte swap depending on the host architecture.
Exposing the byte swapping directly without this layer on top is asking for trouble because every user will have to make sure that they correctly detect the local endianess before attempting a swap. That's the potentially tricky part, not swapping the bytes.
It was just updated for C++20 (with some coverage of C++23).
There's always a bunch of CppCon talks for this too. Just pop on over to their YouTube channel.
The Arm architecture does call this operation "reverse bytes", though, so it's not universal to call it "swap".
"Reverses the bytes in the given integer value n."
I would expect the description for "byteswap" to be something like "swaps the given bytes of an integer value n." and be called like byteswap(x, a, b), where a and b are the indicies of the bytes to be swapped.
You were saying that the final array doesn't have many indices i,j such that a[i] = sorted_a[j] and a[j] = sorted_a[i].
My initial comment was not referring to the final order of the array, but the operations made to reach that order (one or multiple swaps). Another example could have been saying that we could have named the "sort" method "compare" because it uses comparisons in its algorithm (which was a parallel to your initial comment that it's called "swap" because the reverse operation uses swaps to achieve this).
I didn't mean the std::swap() function itself, but the swapping of two elements a[i] and a[j]. Is there any sorting algorithm that doesn't rely on swapping two elements (or two parts of the array)? I guess, only if the sort is not being done in-place and the result is stored in a different variable/memory.
> does not necessary look like the elements were swapped
The only way elements don't look like they are swapped is elements are changed, added or removed, which doesn't happen during sort.
If you look at the result and at the original, generally you will not find many pairs in which the elements exchanged their positions. It takes special initial arrangements for this to be true for all elements.
Code after an abort() call is unreachable. (Or after any function attributed noreturn).
GCC and Clang know this, and do things accordingly.
For instance, I've seen GCC emit code which assumes that ptr is not null after ASSERT(ptr != NULL), because the custom ASSERT macro called an __attribute__((noreturn)) function in the null case.
abort() has defined behavior; it terminates the program abnormally, as if by raising the SIGABRT signal.
Your own function attributed __noreturn__ can have whatever behavior you want it to have. The one in the ASSERT macro I alluded to above calculates and prints a backtrace and other useful information.
Is a default on a switch required? Is lack thereof a compiler warning or something? It’s been a very long time; I only ever seem to recall that kind of “all paths must be handled” from functional languages.
Having an unreachable block communicates to other humans that you didn't forget the else case, it shouldn't exist. Code is about communicating to the next maintainer of the code.
Unreachable communicates to static analyzers, which can throw an error if it detects a code path that would reach this code, even though otherwise that code path is fine. Also static analysis will stop analysis at this point, and since static analysis often is running into the halting problem having a forced halt means some other heuristic elsewhere will get more time to run and so it can find bugs in a different code path that it wouldn't have analyzed before.
> Is a default on a switch required?
Many style guide do not allow a default case on a switch. If you don't have a default case and you add a new item static analysis will flag an error (compiler warning), thus ensuring you look at that section of code that you may not have known about. So unreachable is a way to mark a lot of not possible cases as ones you have thought about, without either skipping them or adding a default.
> I only ever seem to recall that kind of “all paths must be handled” from functional languages.
C++ doesn't require all paths be handled, but realistically as a programmer you want to handle all paths. Marking a path as not reachable is a useful way to handle impossible code paths.
int process(my_enum x, state_t state) {
if (x == my_enum::COMPLICATED) return do_stuff(state);
switch (x) {
case my_enum::SIMPLE0: return 0;
case my_enum::SIMPLE1: return 1;
case my_enum::SIMPLE2: return 2;
}
}
and your compiler complains that my_enum::COMPLICATED isn't handled, even though it clearly was. You generally like these warnings, because they do keep you safe, but in this case, you're smarter than the compiler, but it forces you to put something there. With std::unreachable, it will squelch the warning and not emit extra code. For example, you might be tempted to throw an error -- but if nothing else in the function throws, you end up emitting a bunch of unreachable error-handling code that the compiler can't remove.But I can imagine other use cases that are like "this is not supposed to happen" that can cause major issues like buffer overflows. Better to be more defensive and write code that basically says you are aware of code that shouldn't be reachable.
Wouldn't it be safer just to trigger a panic or something?
Tradeoffs. The panic can result in a lot of code generation which in turn makes the reachable paths slower. Sure we are talking nanoseconds, but this is C++, if performance isn't important you shouldn't be using C++.
It sounds like the functionality of unreachable is to inform the compiler of something, which is what a core language keyword, like "if" or "for", does. Of course then there could be name collisions with the new keyword, so being in std:: might be a solution for that. But that is inconsistent, sometimes they solve this with prepending/appending __ or _t instead, or using obscure enough names like "constexpr" or "nullptr" that probably don't clash
constexpr can't be a function because it is not used a such. Nullptr could have been std::nullptr, but it is used often enough that it made sense to put it in the global namespace (but note that it had to be nullptr instead of null to avoid collisions, so only two chars saved). The type is still std::nullptr_t.
The C++ standard doesn't really use __ as a prefix, it is a namespace reserved for the implementor. The _t suffix in the global namespace is from POSIX originally then adopted by C; C++ still puts _t names under std.
std::unreachable is a very obscure functionality, polluting the global namespace for it wouldn't have been a good idea.
By not doing that you still have to do `std::is_enum_v<T>` somewhere.
For types that aren't enums it should do nothing (maybe `std::identity`)
assert(0);
which is used frequently in D. This is a bonus from assert() being a builtin to D rather than a macro. #define unreachable die("unreachable code reached")More and more programming languages are moving to a more lightweight or scheduled release schema though, e.g. Java that had been stuck in limbo for nearly a decade due to design-by-committee and backwards compatibility concerns by major players.
You can't just have some dude write a span.hpp and go "yep, that's going into my compiler v2" - not how standards work.
Span is nice especially as a vocabulary type, but it wasn't hard to implement something functionally similar and in practice most of codebases already did.
C++'s Biggest Problem: unbelievable, extraordinary, just mind-blowing levels of complexity.
Solution: add more stuff!
The complexity explosion in C++ is due to extra language features etc. not 3 extra, probably well documented, quality of life functions in the standard library.
std::unreachable is the most atypical “function” in this post, but from a code readability perspective, it is pretty clear what it does and it doesn’t really add any new concepts to the language (UB has always been a footgun).
Also, if you want to preserve backward compatibility, all you CAN do is add more abstractions and hope they are simpler to understand and cover the majority of the use cases that the old ones did.
In a lot of metrics C++ has never been more popular. And given the direction of heterogeneous programming, C++ is here to stay.
> Solution: add more stuff!
In a lot of ways, yes, that is the solution.
This is a bad practice. Parse, don't validate - https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...