6 ms·
This is a pretty dismissive response to something that's a real problem. Sure, the "Why are you deleting my checks for if *this is null" is a little silly - bu
by vgatherps 5y ago
This is a pretty dismissive response to something that's a real problem.
Sure, the "Why are you deleting my checks for if *this is null" is a little silly - but there are definitely sharp edges where UB conflicts with actually useful things you might want to do. Did you know seqlocks are undefined (benign race conditions)? Ever ran into padding concerns playing poorly with trying to do atomic CAS?
It's not unreasonable for the standard to say 'padding is undefined', 'data-races are undefined' - but having no way to say "hey, trust me, please un-poison this thing you don't like" is pretty unfortunate.
- jcranmer 5y ago> Did you know seqlocks are undefined (benign race conditions)? It wouldn't be undefined behavior if you used atomic variables--data races involving atomics aren't undefined behavior.
- vgatherps 5y agoThat is true in theory, but in practice it's not feasible. Even assuming you restrict to trivially copyable types, you either have to have a purely-atomic duplicate of anything that you want to store, or play games with trying to get a fast atomic memcpy. Unless things have changed a lot in the past two years neither LLVM or GCC do that much optimisation around atomics so this comes with disastrous performance implications as well as overhead battling the standard.
- gpderetta 5y agoYes, a fast and safe seqlock is still elusive. We will hopefully get the misleadingly named std::atomic_memccpy at some point in the future.
- klodolph 5y agoI know this is not relevant to your point... but padding in C is not "undefined". The amount of padding is implementation-defined, but it can only occur in specific places. The value of padding bits is unspecified. "Unspecified" in the standard means that it can be various different things, but it has to actually be one of those things. As opposed to undefined behavior, which is more of a "poison value" that trims paths from your CFG.
- vgatherps 5y agoAh yes, that's correct - I believe that reading and comparing the padding bytes results in undefined behavior - the bytes might not actually be initialized depending on the object class. I also don't know if you can actually use the result of comparing an unspecified/implementation defined/indeterminate value - that's where poison is generated. In practice (UB aside), this is basically fine in the context of a read, compute, CAS loop. Those bytes do have some value in the machine and if that memory isn't written they won't mysteriously change. It's playing games with the optimisers and UB however. You might be able to get around this by first initialising the bytes to zero, then in-place copy constructing whatever you want? I wouldn't bet anything serious on that being defined though.
- klodolph 5y agoThe poison is only generated with indeterminate values in objects with automatic storage duration (local variables on the stack). Padding bytes in other circumstances will have specific, valid values and can be compared. (An "indeterminate value" is an unspecified value or a trap representation, unsigned char does not permit trap representations, and unspecified values are "valid values".) Use of other unspecified values results in "unspecified behavior" which does not result in trimming the CFG. I think the spec is written this way to codify how compilers are allowed to do liveness analysis... a local variable is marked as live when it is initialized, and if a variable is not live, its storage (registers or stack) may be used for other variables or temporary values. You then get UB because, for example, two comparisons may result in contradictory outcomes, because the value has been overwritten with garbage between the two comparisons. Or in a more modern compiler, you read from an uninitialized local variable, and the compiler trims the CFG. Here's an example of what I'm thinking of: int x, y; // point A if (condition) { x = 5; } else { y = 10; } // point B A compiler can realize that x and y can't both be live at the same time, and assign them to the same register or stack location. However, this means that if you read the value of x at point A and point B, but x is uninitialized, it will naturally have a different value each time you use it... because the value of Y is overwriting it!