Post by Nicol BolasIf you had a need to use a linked-list type in your `eastl::vector`, it
would not
Post by Nicol Bolasautomatically be able to use this optimization. You would likely need to
write
Post by Nicol Bolasanother specialization. With sufficient cleverness, you could write a
traits type
Post by Nicol Bolasthat could be used to detect such classes and employ those optimizations
globally. But even that would only affect you and your own little world,
not the
Post by Nicol Bolasrest of the C++ world.
Which is exactly what we did. When type traits can be a library feature
instead of a language feature, they can be evolved more quickly than the
language allows directly. eastl type traits are override-able for
particular classes, so while the default for "has_trivial_relocate" is
"has_trivial_copy && has_trivial_destruct", programmers can specialize
those traits for particular object types when they know the semantics of
the underlying objects.
My general rule is "don't assume the language standard knows more about
your program than the programmer". The implementor of FooClass should be
able to write "template <> has_trivial_copy<FooClass> : public true_type
{};" in FooClass.h, even if they implemented a non-trivial copy
constructor, because they hopefully knew what they were doing when they
wrote that line of code, and this shouldn't suddenly cause vector<FooClass>
resizes to become UB. Maybe their copy constructor just logs when copies
happen because they were curious how often FooClass was passed-by-value,
and they don't care about users like vector<> who make trivial copies.
There's tons of reasons for this kind of code.
So it's not "vector knows about AutoRefCount", that would be a horrible
layering problem. It's "vector knows about has_trivial_relocate, a new
type trait", and "AutoRefCount knows that it is trivially relocatable."
And vector is trivially relocatable too, so a vector of vectors can be
resized with memcpy as well! In fact, types that aren't trivially
relocatable are the exception rather than the rule, even if they need
non-trivial move constructors to handle the fact that they need to leave a
live object behind to be destructed. I am sure this is the reason for the
plethora of relocation proposals, but this isn't a complicated concept.
It's just that the way the language defines the object/memory/lifetime
model makes it complicated to iron out the details.
Type traits would get much simpler with a good compile-time reflection
system so that there is a standard way to query "are all X's fields
relocatable and X doesn't declare a special move constructor or destructor
etc.?" allowing us to not have to manually specify has_trivial_relocate for
every class, just the ones for which it's true, in the same way that the
compiler automatically derives has_trivial_copy from the fields of the
object via some un-exposed-to-the-user magic.
Making this sort of behavior defined allows iterating on these sorts of
language features without the friction of a multi-year design and
standardization process, and allows groups to come to the committee with
(possibly portable!) implemented code that shows the feature for
standardization -- moving it from the language standardization group which
is intentionally high-friction, to the libraries standardization group
which is a lower barrier for change, or allowing groups like boost to
create portable extensions to the language before they become standard.
Post by Nicol BolasC++ makes these first-class features. That way, everyone implements them
the same way, and everyone's class hierarchies are inter-operable. If you
write
Post by Nicol Bolasa class, I can derive from it and override those virtual functions using
standard
Post by Nicol Bolasmechanisms. Oh sure, we still have function pointers. But nobody uses them
to write vtables manually; that'd be stupid.
You'd be surprised. I've seen some crazy code :)
Post by Nicol BolasAll that code worked just fine, but it relied on UB in probably 5
different ways.
Post by Nicol BolasAnd it was good, useful code that shipped on at least 3 different
projects.
Post by Nicol BolasBut I would never want it to be *standard*.
AHA, so I think this is where we have been talking past each other.
So the perspective I'm coming from is that when the standard writes
"undefined behavior", what I read is "this will probably cause your program
to crash or corrupt memory, and we can't guarantee that said memory
corruption won't cause your program to erase your hard disk or do something
else terrible. Because of this, optimizers are free to pretend this case
never happens and use that to help generate invariants about the code"
e.g.
int* p = f();
int& x = *p; // UB if p == nullptr
if(p == nullptr) { /* optimizer can eliminate this as dead code */ }
When there's something that is useful but not possible to make portable due
to representation or other issues, that is usually instead labeled as
"implementation-defined behavior" rather than "undefined behavior". Two
Post by Nicol Bolas[expr.shift] (3)
The value of E1 >> E2 is E1 right-shifted E2 bit positions. If E1 has
an unsigned type or if E1 has a signed type and a non-negative value,
the value of the result is the integral part of the quotient of E1/2E2. If
E1 has a signed type and a negative value, the resulting value is
implementation-defined.
Most implementations are twos-complement and treat signed-right-shift as
"arithmetic right shift" which extends the sign bit to the new high bits.
But the standard supports other integer representations such as 1s
complement, so it's not possible to define the actual value of the result.
But it's not UB--right shifting a negative number gives some
implementation-defined result and it's not standards compliant for doing so
to crash your program or erase your hard disk.
Post by Nicol BolasHow useful is `reinterpret_cast` really, at the end of the day? You can't
use it to violate
Post by Nicol Bolasstrict aliasing, even if the type you're casting it to is
layout-compatible with the source.
Post by Nicol BolasThe only thing I use it for with anything approaching regularity is
converting integer values
Post by Nicol Bolasto pointers. And that's only because `glVertexAttribPointer` has a
terrible API that
Post by Nicol Bolaspretends a pointer is really an integer offset.
I used to use it commonly like this:
static_assert(sizeof(float) == sizeof(int32_t));
float x = ...;
int32_t x_representation = renterpret_cast<int32_t&>(x);
// do IEEE floating-point magic here e.g.
https://en.wikipedia.org/wiki/Fast_inverse_square_root
// or use it for serialization.
The current standard has no way to do this sort of 'representation-cast'.
Implementations now generally outlaw using reinterpret_cast to violate
strict aliasing, but allow representation casting to be done via unions.
But even that is an extension, and according to a strict reading of the
standard it is UB. I think that this is exactly the kind of thing that
should have "implementation-defined" behavior rather than be UB. The
difference is that usually implementation-defined behavior has bounds to
how wild implementations can go, for example: "the resulting value is
implementation-defined" vs. "is UB". It puts boundaries on how
non-portable this code is.
Post by Nicol BolasAll that code worked just fine, but it relied on UB in probably 5
different ways. And it was good, useful code that shipped on at least 3
different projects.
Post by Nicol BolasBut I would never want it to be *standard*.
So when you say you don't want this behavior to be standard, I see where
you are coming from, but I also don't think it should be UB. UB means the
next version of the compiler could decide to make your program erase your
hard disk, and start world war 3. The implementation of vtables is
certainly implementation dependent but that doesn't mean the memory model
shouldn't still allow you to treat an object as a bunch of
implementation-defined bytes. In the cases where it makes sense to do so,
those bytes should even have defined values (e.g. standard layout object).
Tongue-in-cheek idea: 'frankenstein_new(p)' begins the lifetime of the
object pointed to by p without calling any constructor. ("IT'S ALIVE!")
Easy to audit for! Implemented as a no-op but has semantic value to
analysis tools. UB if p is not standard-layout and the
implementation-defined bytes aren't correctly initialized (with "correctly
initialized" being implementation-defined).
(random segue here to more reinterpret_cast thoughts...)
Another use I've been doing more of lately is using reinterpret_cast to
create strong opaque typedefs. This is fortunately well-defined according
to the standard:
foo.h
// define a uniquely-typed version of void*
// that isn't castable to other void*s
struct OpaqueFoo_t; // never defined
typedef OpaqueFoo_t* FooHandle;
class IFoo {
(some functions that create and operate on FooHandles here)
};
foo_impl.cpp
struct ActualObject {
...
};
FooHandle ToFooHandle(ActualObject* p) { return
reinterpret_cast<FooHandle>(p); }
ActualObject* FromFooHandle(FooHandle p) { return reinterpret_cast<
ActualObject *>(p); }
class FooImpl : public IFoo { ... };
This allows multiple coexisting implementations of IFoo to exist within the
same program as long as users only pass handles created by one IFoo to the
same IFoo. There are ways to make this more typesafe but the ones I've
found all add a huge burden to users that so far hasn't been worth the
small additional safety.
Post by Nicol BolasPost by Ryan IngramI'm not sure I can continue having a discussion with you if you continue
to be intellectually dishonest.
For example, you ask
if you're just going to ignore the rules and do what your compiler
lets you get
away with anyway... why does it matter to you what the standard says?
which, guess what, I already answered -- in fact, you immediately quote
That may be what they do now, but it is of vital importance what the
standard
declares as UB as compilers continue to take advantage of UB detection
to
treat code as unreachable. As the theorem provers and whole-program
optimization used by compilers get better, more and more UB will be
found
and (ab-)used, and suddenly my "working" code (because it relies on UB,
as
you say) causes demons to fly out of my nose.
and you follow with this claim
That's what happens when you rely on undefined behavior.
You can't have it both ways; I care about what is in the standard because
I care about my programs continuing to work,
Also, if truly every C++ programmer does this sort of thing, then surely
no compiler writer would implement an optimization that would break every
C++ program. If the kind of thing you're talking about is as widespread as
you claim, you won't have anything to worry about.
So what is the reason for your fear that your UB-dependent code will
break? Or are you not as sure as you claim that your "common coding
practice" is indeed "common"?
but I also care about being able to write programs that make the hardware
Post by Ryan Ingramdo what I want. The argument I am putting forward is that this behavior
shouldn't be undefined; that the standard and common coding practice should
be in agreement. There are ways to have a language that semantically makes
sense without hardcoding everything into explicit syntax about object
construction, but you dismiss this idea out of hand.
The purpose of a constructor/destructor pair is to be able to establish
and maintain invariants with respect to an object. The ability to
create/destroy an object without calling one of these represents a
violation of that, making such invariants a *suggestion* rather than a
*requirement*.
You want the validity of this code to be dependent on the *kind* of
invariant. That you can get away without doing what you normally ought to
do based on the particular nature of the invariant that the class is
designed to protect. I see nothing to be gained by that and a lot to be
lost by it.
A static analyzer can detect when you attempt to `memcpy` a
non-trivially-copyable class. A static analyzer cannot detect when you
attempt to `memcpy` a non-trivially-copyable class and then drop the
previous one on the floor, such that the invariant just so happens to be
maintained.
The world you want to occupy makes that static analyzer impossible to
write in a way that didn't get false positives or false negatives. The
world I want makes that static analyzer *correct*, according to the
standard, requiring you to write your code correctly in accord with the
specification.
Post by Ryan IngramWhy should we *want* that? As far as I'm concerned, [basic.life](4) is
a mistake and should be removed. If you give an object a non-trivial
destructor, and it doesn't get called, then your program should be
considered broken.
More dishonesty here. You call me out when I propose that things are
problems in the standard, but then you go ahead and do the same *in the
same message*.
I'm not saying that the standard is perfect. I'm saying that the things
you want to be legal are bad. Even if some of them already are legal.
Post by Ryan IngramThat's not how a standard works.
A standard specifies behavior. It specifies what will happen if you do a
particular thing. If the standard does not explicitly say what the
results
of something are, then those results are undefined *by default*.
I agree with this statement. I was simply pointing out that
[basic.types] is not sufficient to call this behavior UB; I don't know the
entire standard word-by-word (and neither do you, as evidenced by your
misquote of [basic.life]), and I'm not 100% convinced that there is nothing
elsewhere in the standard that defines what should happen in this case,
although given that it's not defined at that point, I am willing to believe
that it's more likely than not UB. When something explicitly is declared
UB it's easier to quote the relevant section!
Generally speaking, something only needs to be called out explicitly as UB
when it might otherwise have worked. Like the rules about integer overflow
(though those are implementation-defined, not UB). Normally adding integers
has well-defined results, but there are certain corner cases that have to
be called out.
Nothing in the standard says that memcpy will generally work on objects,
but there is a specific allowance made for trivially copyable types. Thus
outside of that allowance, doing it is UB.
So, lets go back to trying to find some common ground. (Apologies to any
Post by Ryan Ingramof our readers who were hoping for a good old-fashioned flame war!)
OK, I know nothing about Haskell (or functional programming of any kind),
so I'll try to translate my understanding of what I think you're saying.
{-# RULES
Post by Ryan Ingram"map/map" forall f g xs. map f (map g xs) = map (f.g) xs
#-}
Here the programmer of "map" knows that semantically the code on both
sides of the equals is the same, but that the right-hand one will generally
be more efficient to evaluate. Rewrite RULES inform the compiler of this
knowledge and ask it to make a code transformation for us, adding
additional optimization opportunities that can show up after inlining or
other optimizations.
Now, you could trivially write a false rule
Post by Ryan Ingram{-# RULES
"timesIsPlus" forall x y. x*y = x+y
#-}
The fact that the programmer could write a buggy program doesn't mean
that the language makes no sense.
From this, I gather that Haskell has some syntax for performing arbitrary
transformations of its own code via patterns. And that you can write
transformations that are actually legitimate as well as transformations
that lead to broken code.
I have no idea why you brought up Haskell here. You may as well have used
a macro or DSELs with C++ metaprogramming and operator overloading (say,
Boost.Spirit). Any feature which could be abused would make your point.
This feature is designed to be used by programmers who have proved that
Post by Ryan Ingramthe code transformation being applied is valid. In C++-standards-ese I
would say "a program with a rewrite rule where the right hand side is
observably different from the left hand side has undefined behavior"; the
compiler is free to apply, or not apply the rewrite rule, and it's up to
the author of the rule to guarantee that the difference between the LHS and
RHS of the rule is not observable.
I am not suggesting a wild-west world where everyone just memcpy's
objects everywhere. I think you're right that this isn't a useful place to
be.
I am suggesting a world where it is up to the programmer to define places
where that makes sense and is legal; one where the memory model works in
tandem with the object model to allow low-level optimizations to be done
where needed. [basic.life](4) is an example of this sort of world, it
specifies that I can re-use the storage for an object without calling the
destructor if I can prove that my program doesn't rely on the side-effects
of that destructor. It doesn't say I'm allowed to just not call
destructors willy-nilly--it puts an obligation on me as a programmer to be
more careful if I am writing crazy code.
The fact that you *could* make use of something by itself does not
justify permitting it.
The fact that something could be abused by itself does not justify
forbidding it. It's a delicate balance. However, I feel that trivial
copyability strikes a good balance between "blocks of bits" and "genuine
objects". You get your low-level constructs and such, but it's cordoned off
into cases that the language can verify actually works.
I consider an object model where important elements like constructors and
destructors are made *optional* based on non-static implementation
details to not be worth the risks. Your object either is an object or it is
a block of bits.
It's this same sentiment that puts reinterpret_cast and const_cast in the
Post by Ryan Ingramlanguage; tools of great power but that also carry great responsibility.
How useful is `reinterpret_cast` really, at the end of the day? You can't
use it to violate strict aliasing, even if the type you're casting it to is
layout-compatible with the source. The only thing I use it for with
anything approaching regularity is converting integer values to pointers.
And that's only because `glVertexAttribPointer` has a terrible API that
pretends a pointer is really an integer offset.
And how useful is `const_cast`? The most useful thing its for is making it
easier to write `const` and non-`const` overloads of the same function.
Nice to have, yes. But hardly a "tool of great power".
Post by Ryan IngramSimilarly, objects on real hardware are made out of bytes, and sometimes
it's useful to think of them as objects, sometimes as bytes, and sometimes
as both simultaneously. What are the best ways to enable this? Are there
ways that make sense or do you think it's fundamentally incompatible with
the design of the language?
OK, let's look at your exact example. You have some internal vector analog
and you have your internal intrusive pointer. Now, let's assume that the
standard actually permits you to `memcpy` non-trivially copyable types, and
it allows you to end the lifetime of types with non-trivial destructors
without actually calling those destructors.
So let's look at what you've done with that. In order to implement your
optimization, you had to presumably add a specialization of `eastl::vector`
specifically for the `AutoRefCount` type. That's a lot of work for just one
type. You had to re-implement a lot of stuff. I'm sure that there were ways
to reuse a lot of the standard code, but that's still a lot of work.
Let's compare this to the ability to optimize std::vector's reallocation
routines for trivially copyable types. That works on *every type that is
trivially copyable*. Why? Because these are the types that the *language*
can prove will work. Suddenly, once those optimizations are made,
everyone's code gets faster. `std::copy` gets faster when using trivially
copyable types too. As do many other algorithms.
I don't have to specialize `vector` for each trivially copyable type I
write. I don't have to specialize `std::copy` for each type. It all just
works. I can take a type written by someone who can't even *spell*
trivially copyable and it will work.
If you had a need to use a linked-list type in your `eastl::vector`, it
would not automatically be able to use this optimization. You would likely
need to write another specialization. With sufficient cleverness, you could
write a traits type that could be used to detect such classes and employ
those optimizations globally. But even that would only affect you and your
own little world, not the rest of the C++ world.
Furthermore, I can now write my own `vector` analog that uses these
optimizations. It can use template metaprogramming to detect when they
would be allowed and employ them in those cases. I'm not writing some
special one-time code for a specific thing. I'm making all kinds of stuff
faster.
*That* is the power of having a firm definition in the language for
behavior. *That* is the power of knowing a prior which objects are blocks
of bits and objects which are not. *That* is the power of having real
language mechanisms rather than inventing them on the fly based on the idea
that any object should be able to be considered a block of bits whenever
you feel you can get away with it.
C is about giving you a bunch of low-level tools and expecting you to
implement whatever you like. C gives you function pointers and tells you
that if you want virtual functions and inheritance, you'll have to
implement it yourself.
C++ makes these first-class features. That way, everyone implements them
the same way, and everyone's class hierarchies are inter-operable. If you
write a class, I can derive from it and override those virtual functions
using standard mechanisms. Oh sure, we still have function pointers. But
nobody uses them to write vtables manually; that'd be stupid.
That's why adding destructive-move, relocation, or whatever else is *far
superior* to your "let's pretend a type with invariants is a block of
bits" approach. Because once it's in the language, *everyone* can use it.
Everyone gets to use it, likely without asking for it. Every user of
`unique_ptr`, `shared_ptr`, etc gets to have this performance boost.
If you improve the object model such that it actually supports the
operations you want, you won't have to treat objects as blocks of bits to
get things done. Just like with virtual functions, it's best to identify
those repeated patterns and put them into the language.
Look, I've written code that treats genuine objects as blocks of bits. I
wrote a serialization system that would write binary blobs to disk, which
would later be loaded onto different platforms directly in memory. It
supported virtual types through vtable pointer fixup. Oh, and the platform
that wrote the data? It was completely different from the platform(s) that
read it, so endian fixup was often needed (at writing time).
All that code worked just fine, but it relied on UB in probably 5
different ways. And it was good, useful code that shipped on at least 3
different projects.
But I would never want it to be *standard*.
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussion+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
Visit this group at https://groups.google.com/a/isocpp.org/group/std-discussion/.