- Welcome to the 9 a.m. session.
Do I have my mic on?
Good.
My name is Scott Schurr.
I work for Ripple.
This stuff I'm about to talk about
has nothing to do with
what I do at my day job.
I very seldom do any type punning.
I used to.
I spent quite a number of years
doing embedded development
and type punning is
just one of those things
that's unavoidable when
you're doing embedded stuff.
So it's something I care about.
And when somebody said,
"You can't type pun that way.
"That's undefined behavior,"
my initial reaction was,
okay, so what do I do?
And they said, here do this.
Okay, but why?
And so this, after a number
of interactions with folks
of that nature, I decided it was time
to try to get a handle on those stuff.
So this is the result of my research.
I had help from a number,
a small number of experts.
But the mistakes are my
own, the correct stuff
is large due to the experts that help me.
So we're about to look at type punning.
I don't have a monitor here.
We're gonna talk about two things,
we're gonna talk about type punning,
and so the question is
what is type punning,
and I like the Wikipedia.
This isn't quite the definition
but this is how they introduce it.
And type punning is when
you subvert the type system
is the way I read this.
And I think that's a good definition.
So we're talking about things that
people would ordinarily
encourage you not to do.
And then, sorry.
There we go.
Then the other thing we're
talking about is C++17.
When I first thought about this topic
I wanted to title it Type
Punning Through the Ages.
But once I started finding out
how difficult it was to figure out
what you can and can't do
I decided I really need to focus on
just one standard rather
than a collection of them.
So we're only gonna talk about
what are the rules for C++17.
Because we're talking
about undefined behavior
you can't rely on what
your compiler tells you.
So the only place to go for information
about undefined behavior,
unless you're running
UBSan is to the standard.
So we're gonna be looking
at a number of quotes
from the standard.
And those aren't really from the standard.
They're from that paper
which is pretty close
to what's been finalized as C++17.
It's not exactly there
but it's pretty close.
We're also gonna be talking
a little bit about C11
which is the most recent C standard.
And I don't actually have
a copy of the C standard
but I did pull this paper off the web,
so that's what I'm using.
And I have compiled this
stuff with Clang and GCC,
not all of it with GCC, mostly with Clang.
Why do you type pun?
What draws anybody to do this stuff?
So the motivation for doing this
is if you're doing Endian conversions,
turns out that there are a number of folks
who serialize their stuff
to files by pretending
that it's a bunch of unsigned char
shooting out to a file,
sucking it back in and
expecting it all to work.
That's type punning.
If you operate on any opaque type,
and opaque types happen
to be things like pointers
and floating point numbers,
the standards says nothing about
what the internal of a
pointer or a floating
point number is supposed to be like.
So once you start digging
around inside of a pointer
or a floating point number
you're doing type punning
whether you know it or not.
So what are the risks?
There are a number of risks.
None of it's portable because
when you're type punning you're relying on
a local representation of
some piece of information.
And that piece of information,
the way that information
is stored can change
not only from processor to processor
but from compiler to compiler.
And when you change your
compiler flags it can change.
So this stuff is not portable
in the most serious possible way.
It's fragile for the same reasons
we were talking about before.
And it's hard to maintain
because generally
you're doing something
that's a little bit off
the beaten path.
So when somebody comes
in to do the maintenance
on this stuff they're gonna say,
"What in the bejeesus were you up to?"
and if you're there to answer the question
you can tell them,
but if you're no longer in that job
they're gonna be guessing
unless you've left voluminous comments
and they bother to read them.
This stuff is bug prone
because we're gonna see
just how much undefined behavior
lurks around the corners of this stuff.
So we're gonna focus on
trivially copyable types,
and we'll get to what those are.
The reason being that many
of the techniques we're using
they're defined only for things
that are trivially copyable.
So if you're not using a
trivially copyable type
when you're type punning,
you're in even more danger.
Definitions.
And I should pause for a second.
If you have questions about
stuff that I'm saying,
I am hoping this will be
an interactive session.
So hold your hand up to interrupt.
I'll try to repeat the question.
We're gonna be talking about
things like undefined behavior,
and I think it's a good idea to start with
what do these things mean.
So we're gonna say, I've said,
use trivially copyable types.
So what does the standard have to say
about what a trivially copyable type is?
And in order to figure that out
we have to go back a few
levels in the definition
and we're gonna start with what an object
representation is.
An object representation
is an array of bytes.
And then in side of that
array of bytes, let's see,
there we go,
the value representation inside of that
is the value representation
is the set of bits
inside of that array of bytes, okay?
Everybody with me?
So you got a bunch of bits
and some number of bytes.
And then for, yes, Ben?
- [Ben] It's the implication that
there might be some unused
bits in your object.
- Yes, so Ben's bringing up
that there may very well be
unused bits in the object
and that is 100% correct.
So for trivially copyable types,
the value representation
is the set of bits.
So if that's not true for your type,
it's not trivially copyable.
The standard library provides a type trait
that will allow you to
see whether your type
is trivially copyable or not.
So if you're type punning,
you're smart to use that.
Yes, Vinni?
- [Vinni] How does it know?
- Next slide, perfect set up.
The question was, how does it know?
The standard, because it's
hard for it to know this stuff,
the standard infers what a
trivially copyable type is
by looking at this set of
characteristics of the type.
So it looks at the definitions of the move
and copy constructors
and assignment operators.
And if any of those are user-defined
then it knows it's not trivially copyable.
If it has a non-trivial destructor
it knows that it's not trivially copyable.
There are a couple of other things,
and it's not just the outermost object.
It's all of the objects all the way down.
Yes?
- [Ben] Jason and I found that,
a trivially destructable,
what you said about user-defined
(indistinct question),
you just have to mention
them that it's not trivial.
- Yeah.
- [Ben] They could be empty
but they're still not trivial.
- Right, so Ben's point is very useful.
In order for any of these
special members to be non-trivial
all you have to do is mention them.
Yeah?
- [Participant] If you get them apart.
If you use equal default.
- Yeah, yeah.
So if you give them an
empty body or whatever
then they're non-trivial.
And virtuals are bad,
not in general,
but if you're trying to make
a trivially copyable type
then as soon as a virtual
shows up it says, no, no, no.
This is not where we're at.
So a bunch of things
are trivially copyable,
chars, arrays of things
that are trivially copyable,
structs generally.
You can have protected
and private sections
and things that are trivially copyable.
They're still just fine.
You could have unions.
You can have arrays of structs
or arrays of unions, all of those things
can be trivially copyable if the things
that are inside of them are
also trivially copyable.
The place where you can't go
is you can't have a reference.
References are not.
Here we have a user-supplied
copy constructor,
that means this is not trivially copyable.
And here's a virtual.
So this one also is
not trivially copyable.
So we're done.
We've beaten trivially
copyable into the ground, yes.
- [Participant] So I would expect that
another property would
like is standard layout.
What's the relationship between (mumbles)?
- Yeah, so the question is
the relationship between
standard layout and trivially copyable.
I'm not an expert.
We will be looking at standard layout,
the properties of standard layout
a little bit later on in this session.
My belief, which is not to say
that I'm an expert on this,
my belief is that if
it's a standard layout
thing, object, that it is
also trivially copyable.
But there are things which
are trivially copyable
which are not standard layout.
One of the constraints I think
for a standard layout
is that you may not have
sections that are private.
And with trivially copyable
it is allowed to have a
private section for example.
So the other thing we're
gonna be getting into
undefined behavior.
So let's see what the standard
has to say about this stuff
because that's really,
that's the only thing
we can be talking about,
the standard is the thing that says
whether something's
defined behavior or not.
So there are actually a
number of kinds of behavior
that the standard talks
about that are interesting.
People talk mostly about
undefined behavior.
But there are other things
to consider as well.
So the first one I wanted to mention is
implementation-defined behavior.
This is stuff that depends
on the implementation
and your implementation is obliged,
according to the standard,
to document what it will do.
So if you were to go and
look in the standard,
and I encourage you all to
take the time to do that,
it's hard to read, but
it's not impossible.
And just because you
interpreted it one way
doesn't mean that's what it really means
but you're often right.
So I encourage you to go
ahead and make the effort.
But it will call out specifically
if something's implementation defined,
and then you're supposed to
go to your implementation
and say, "How do you behave?"
There are a couple of examples here of
things that are implementation defined.
The number of bits in a byte,
and that surprises some people.
I worked with the DSP for
quite a number of years
where there were 32 bits in a byte.
That's just fine.
The standard doesn't mind that.
Let's see if I can get there.
All right.
The next one is what atomics
are lock free if any.
That's up to your implementation.
That gives you a reasonable
intuition for what
implementation defined
means and why it's used.
Beyond that there is unspecified behavior.
Unspecified behavior I
think is really interesting
because when behavior's
unspecified it is always
explicitly called out in the standard
is this behaviors unspecified
which is kind of backwards to me.
But if it doesn't mention
it in the standard
there's a different rule.
The standard can say this
is unspecified behavior,
and generally will say here's the range
of acceptable behaviors.
Your implementation is not obliged
to document what it will do.
But the standard will
say here's a range of
acceptable behaviors
and your implementation
might do any one of these.
So here are some examples
of unspecified behavior.
I'm turning around 'cause I don't have
a monitor up here and I'm working on
the same information
you guys are working on.
So the order of evaluation of arguments
and using function call syntax.
That's up to your implementation and
it doesn't have to say what it's gonna do.
And where the memory for an
exception object comes from,
that's all up to your implementation
and your implementation
doesn't need to tell you
how it does that.
Now we're on to undefined behavior.
So there are two different ways
in the standard to get undefined behavior.
There are a number of places
where the standard explicitly calls out
that there is undefined behavior.
But the primary way that
undefined behavior happens
is if the standard doesn't say anything.
If the standard is silent on the topic
then that's undefined behavior.
That is literally there is no definition.
And the other choice is when the program
uses an erroneous construct
or erroneous data,
that will be explicitly called out
as undefined behavior.
So you can get to undefined
behavior in one of two ways,
one is if the standard doesn't tell you
that you can do it.
So implicitly you have no permission to do
things that it doesn't
tell you you can do.
Or it may say this is undefined behavior.
You can get there either way.
So here's one.
I'll mention that the things
that are in the square brackets
you can use those to reference
a section of the standard.
That's how there are
chapter numbers and stuff
in the standard.
But those move around over time and
the square bracket things are
supposed to stay constant.
So if you do searches
for the square brackets
that'll get you the right section.
And then there's a paragraph so that
square bracket expert,
that's a reference to a section in
the paper that I was referring to.
And then that's paragraph four.
So the consequences of
overflow or underflow
of a signed integer,
that's undefined behavior
and that's called out explicitly.
Any kind of a race
condition, doesn't matter.
And the standard also defines
what a race condition is.
I don't wanna go into that,
but it does say explicitly
if there is a race condition
that's undefined behavior
and with good reason.
One of the things that was
interesting to me is that
an access to the non-active
member of a union,
and we'll talk a little bit about
how a union member gets active,
but an access to a non-active
member of a union is
usually undefined behavior simply because
the standard doesn't say you can do it.
It's silent on that,
and therefore you infer that
it's undefined behavior.
We should do smart
jumping quite like that.
Okay.
Like I mentioned earlier
undefined behavior can change over time,
not arbitrarily but if
you change compilers
or change compiler flags,
in particular compiler flags have
a huge impact on what happens.
And that's because if you
change your optimization level,
your optimizer may take advantage of
something being undefined
and saying that can't happen.
So the standard does say
list a bunch of expected,
sort of expected behaviors
for undefined behavior.
The one that it doesn't mention,
I'll say this one first before
we go through the other ones
that are in the standard,
the most likely thing to happen
when you run across undefined behavior
is your compiler will do exactly
what you hoped it would do.
So you are unlikely to see,
when your code has
undefined behavior in it,
you're unlikely to see
any negative consequences.
I'm not saying you won't,
I'm just saying it's unlikely.
So your implementation
is allowed to document,
if this is undefined behavior
this is how I'm gonna behave.
And it is allowed to terminate
either the compilation process,
I suppose it could terminate
the linking process
if it knows how to do that,
or to terminate its execution.
That's all fine.
The other ting it can do is ignore
the situation completely
with unpredictable results.
This is what your optimizer
takes advantage of.
It can generate smaller or faster code.
When undefined behavior gets in your way
it's because your optimizer or compiler
is trying to do you a favor.
Your compiler will generally not
intentionally reformat your hard drive
or make demons fly out of your nose.
So the question is,
and people have made the point,
the reason I'm mentioning
those is those are all things
that people have brought up
as the potential consequences
of undefined behavior.
But your compiler is not gonna do that.
And the reason it won't is that there is
no economic incentive
for your compiler vendors
to do anything like that.
They want you to use their tools.
The reason that undefined behavior is
when it does behave funny for you,
the reason that it's behaving funny
is because they're
trying to do you a favor.
They're trying to make
your program run faster
or be smaller or both.
So my take on undefined behavior
is that it's everywhere.
If you think about the
definition of undefined behavior
that that's a huge space,
it's all of those things that
the standard doesn't say.
So the likelihood of your program
containing undefined
behavior is very, very high.
Just as an example,
how many multi-threaded programs have
guaranteed no raise conditions?
And there may very well be some
but I don't think there
are very many of them.
And so my inclination is that pretty much
any multi-threaded program
has undefined behavior.
I hope some of you are old enough
to catch the Dr. Strange love reference.
My take is no that we don't just go,
no, no, no, I'm a believer,
everything's good.
I think it's worth understanding
what's undefined behavior
and staying out of the way.
Now actually to the meat of it.
Yes, Ben?
- [Ben] Sorry, just one comment.
(indistinct question)
- Yes, Ben's pointing out that
if you're evaluating in context
per context at compile time,
so it's not obliged to do this
if you have a context or function
that is executing at runtime.
But if you have a context or function
that is being evaluated at compile time
it will not contain,
by the time it's done,
it will not contain
any undefined behavior.
The compiler will only do
things that it knows how to do.
So, thank you, Ben.
That's an excellent point.
The easy place to start is
what happens when we cast away const?
So the standard is
extraordinarily clear about this.
You can cast away const all you want.
That's just fine.
Don't mutate a const anything
unless it's marked as mutable.
So if it's a const object
with a mutable member,
that's fine, change it all you want.
If it's just a const object,
the standard says that's
undefined behavior.
Now that's not to say you
can't get away with it
'cause we've talked about, you know,
the compiler isn't enforcing any of this
and it may very well do what you want
but it is undefined behavior
to change that value.
This is straight out of the standard.
So the standard is being about as clear
as it knows how to be.
Don't change those consts.
I think it jumped.
Let's see if I can get back to that.
Yup, okay.
For me I wanted to look at a motivation
for why would the standard care.
And when you modify a const,
well let's start with what are
some of the optimizations that
an optimizer might wanna do?
Well if an optimizer sees
that something's const
it knows that it's not supposed to change.
So if you change a const value
the optimizer or compiler may have cached
that value previously and
so may not see that change.
So there are very good
reasons for this rule.
The rule of don't change the const
makes your compiler,
gives your compiler an optimization point.
So this is one where if
you don't follow the rule
you are likely to get slapped because
your stuff won't behave
the way you want it to.
I'm not saying you will be slapped,
I'm just saying that it's
a greater likelihood.
But there's sort of a way around this.
This is new in C++17.
It's called standard launder.
And it's not something that
people talk about a lot
because it's pretty hard
to use in my estimation,
but you can use it.
So what we've got here
is a const with two,
or a const, a struct
with two const members,
and renewing it.
And then we're doing
on the next line below
the yellow line with the new,
we're doing a placement new over the top
of what we just did.
Yes, Arthur, I'm gonna be corrected.
Good.
- [Arthur] You're not gonna be corrected.
I have a question about
this that I don't have
the answer to.
You might think (mumbles)
but not only are you putting
a new object in there
you're doing it without
destructing the old object.
And the old object as
a trivial destructor.
But even in that case is it okay to skip
the trivial destructor
and just not call it?
- Okay, so the question
is is it okay to skip
the trivial destructor when
you do the placement new?
And I am not an expert.
I'm not seeing any experts
hold up their hands
to say I know, oh okay.
(indistinct answer)
Okay, we've been assured by an expert
that you can skip the trivial destructor.
So thank you.
Thank you.
I'm relying on you guys.
What we're doing is we're
doing a placement new
over the top of a preexisting object.
We have undefined behavior
when we're accessing the
members of that object
because we've modified const values.
See if this will work.
So what we can do is we can,
at the place where we're doing the access,
there's nothing you can do at the place
where you're doing that
second placement new,
there's nothing you can do
according to the standard
to improve that situation.
What you have to know is
that I as a programmer
know somebody screwed around
with that const value.
I you know that may have happened
then you can use standard launder
at the site where you're accessing
the thing that may have been messed with.
So this is very hard to use correctly.
But it's here.
I guess one other thing
worth pointing out is that
you'll notice that
those other two accesses
that are not through the standard launder,
those two accesses remain
undefined behavior.
So launder doesn't claim the whole thing.
It can provide something that's clean.
Let's see if I can advance through here.
(indistinct comment)
Well yeah.
You can save the pointer
that's returned by standard launder.
And if you always access
through the pointer
that came from standard
launder, then you're golden.
So you can either go
straight from the response
the launder gives you,
which is that pointer,
or you can save that
pointed and go through there
and the compiler and
optimizer will say, okay,
we know that const is
not necessarily there.
Otherwise you're on your own.
So I think standard launder
is hard to use correctly
if you're relying on that
to get you out of trouble.
It's a hard road.
So my suggestion is if
you can possibly avoid it
don't mutate const things.
I'm going too slowly.
Thank you.
So let's talk about pointers.
This is a damp pointer,
one of those new kinds of pointers.
So I wanna start out
with using, let's see,
an invalid pointer value
is purely undefined.
So anything you do with a pointer
you need to be careful with it.
Thanks.
It's bouncing.
Where is it going?
I think I'm gonna have to
give up on this and get stuck.
- Undefined behavior.
- Yeah.
- All right, I know where that is.
We know where that is.
Manipulate your corners carefully.
The type punning that is very clearly
talked about in the standard,
and is well defined,
is converting between
pointers and integers.
What's that?
I think I'm good.
So pointer to integer conversions
are well defined by the standard.
There are some constraints
but the mapping in both
directions is well defined
and it is the exact nature of the mapping
is implementation defined.
So the form of the
integer that you get out
of that pointer is up
to your implementation,
but is guaranteed to make the round trip.
So whatever that integer is,
when you turn that back in
to a pointer of some kind,
that pointer has,
if you didn't mess with
the integer in between
you get the same pointer back.
So it's roundtrip capable.
Here's an example where uintptr_t,
that's one of the two
types that the standard
provides for you to do this conversion.
That's basically just an integer
that's big enough to hold
whatever the representation
of a pointer is.
So returning a pointer to
doubles into an integer,
IP is an integer.
And we're using that integer over here.
We're doing addition.
Integer addition with the plus I,
and then we're casting that
back into a double pointer,
and I think this is, oh,
Eric is telling me this
is undefined behavior.
- [Eric] Pretty sure because
the representation of it
as uintptr is not guaranteed
to be equally as sequential
as when it's a pointer.
So you need to do the
addition while it's a pointer.
- Okay, so Eric is saying he thinks
this is undefined behavior.
I am not gonna disagree with him.
- [Eric] Totally seven here who are better
in standard than I am, but.
- Yeah, okay.
I'm not gonna disagree
with him, not an expert.
But this was something that I thought
might very well work.
- [Participant] That
was a good description.
It might very well work.
- [Participant] It doesn't quite (mumbles)
but it might very well work.
- So I think I've just heard a second
to agree with Eric that
this is undefined behavior.
- [Participant] (mumbles)
defined the exact
behavior of conversion
between pointers and integers.
- [Participant] Yeah, I would
say implementation defined,
it'll get you back a pointer that compares
equal to the original pointer,
but then you go and do stuff with it.
- So my reading of the standard,
which is I'm not an expert,
was that as long as the
integer to pointer conversion
gives you back a pointer
which would be valid.
- Right, so the problem here.
- I see.
- [Participant] Is
you're doing the addition
and then you're de-referencing it.
But because you do the
addition on it as an integer
rather than as a pointer,
there's no guarantee that that's
going to be a valid pointer
that you're referencing.
- Okay, yeah, right.
I will absolutely agree with
you that there is no guarantee
that after the addition that
the pointer that you get back
is a valid pointer.
I completely agree with that.
- [Participant] Can I ask you something?
- Yes?
- [Participant] Okay now I'm
gonna ask some questions.
I might be wrong so don't laugh at me,
but uintptr_t lets you convert
any integer on the platform to an integer
that's large enough to hold it?
- The question is,
let me see if I can clarify this.
- [Participant] Okay.
- A uintptr_t is an integer type.
It's an unsigned integer
type that is big enough
to hold an integer
representation of a pointer,
of any pointer on the platform.
- [Participant] Okay, next question.
What is the size of a
float on a 32-bit platform?
- We're not talking about floats,
we're talking about pointers.
- [Participant] Okay, what's
the size of the double
on a 32-bit platform?
- A double?
It is implementation-defined,
but in either case
we're not talking about
floating point numbers
we're talking about pointer to
integer conversions and back.
- [Participant] But it seems
like the integer is 32 bits
while the double is 64 bits in size.
- But there's no double here.
- [Participant] It's right up there.
- There's a pointer to a double.
So what we're doing,
the uintptr_t is operating on p
which is a pointer to a double.
So we're converting a pointer to a double
into an integer.
We're not converting the
double into an integer,
we're converting the pointer
to the double into the integer
and I can see I'm not
gonna get anywhere close
to the end of my slides.
- [Participant] Could be a void star too.
- Yeah.
My hope was, and the
experts that I work with
didn't call me out on this.
That's my responsibility.
I think this is as close to
well defined behavior as it can get
assuming that after the addition
the returned pointer is a valid pointer,
one that you've recalculated.
That's not to say that, okay,
I'm getting a thumbs up from over there.
So we're type punning,
we're skating on the edge this whole way.
What the standard guarantees to you
is that if you do that roundtrip
don't mess with the integer.
You just turn it into an integer,
turn it right back into a pointer.
The standard guarantees that's okay.
In my particular implementation,
when I did that computation,
again what I'm doing is
I'm messing with a pointer,
I'm taking a pointer to a double.
And I'm turning that
pointer into an integer
and I'm adding an integer to that pointer
which happens to be the size of a double.
So I'm indexing.
And then I'm converting that
integer back into a double
and people are making faces.
Can't say I blame them.
Those are optional types.
The compiler says or the standard says
you may or may not have those
in your implementation.
If there is no integer type big enough
to hold that conversion then it will
certainly not be provided.
And the integer
representation of the pointer
might not be the same bits of sequence
as the pointer itself.
The standard encourages
people to make that
something that would
be sensible to someone
who understood the underlying pointer.
But it's just encouragement,
it's not a requirement.
And in terms of choosing between
intptr_t and uintptr_t,
I would get very uncomfortable using
an intptr_t because if it,
an assigned pointer just
doesn't make any sense to me.
So moving on from integers,
if you have an initialized
value representation
there are a list of a
bunch of different ways
you can access that value.
And all of them in the standard,
this is basic.lval paragraph eight,
they all contain the word, the dynamic,
or something rather about the
object you're referring to
and there are a bunch of those.
And at the very end of the list
you get three to work with that have
no relationship to the original object.
You get to look at it
as though it's a char,
an unsigned char or a standard byte.
Yes, Peter?
- [Peter] The idea is to
(mumbles) some future life.
- Okay.
- [Peter] Or some future
timeline that spawned
20 years ago.
- Yeah.
So Peter's point is that
the goal for the standard
is to move away from
unsigned char and char
and use only standard byte as the way
to access this stuff.
But for tight now this is
what the standard says.
So that's how you can access
something that has a determinant value.
So if you have something that
is an indeterminate value,
how do you access that?
So to start out with we need to talk about
what an indeterminate value is,
and it's something that has been
brought up into existence either through
because you have an
auto thingy on the stack
or because you brought
it from the news store.
And so you don't know the things inside.
So we've got examples of things
that are indeterminate values.
Note that we're not at file scope.
If it's at file scope it gets initialized
to a well-known value.
But if it's just being generated locally
it has these things
because they don't have
constructors that initialize
them to particular values.
They have indeterminate values.
So that class for example has a member
that's not getting initialized,
that union, all these things,
they have uninitialized values
or indeterminate values.
So there's a secondary
standard that talks about
how you can access the contents
of indeterminate values,
and it says specifically
that you can't get there
unless you're using an unsigned char star
or a standard byte.
So those are your choices.
What I think is interesting about that
is that if you're accessing
any kind of a value,
whether it's been initialized or not
you can get to it through either
an unsigned char char
star or a standard byte.
I'm not seeing any
disagreements, that's reassuring.
One of my experts did warn me
you have to be careful of
how you've got your pointer.
I won't tell you how to be careful
how you got your pointer because I think
that's too confusing for me,
not for you, but for me.
That's hard.
So we've got a piece of code
here where we're gonna be
accessing the least
significant bits of a pointer.
I've got a pointer,
and I have the unsigned
char const star const.
That's a pointer to my pointer.
And we're gonna roll that pointer,
that pointer's gonna walk across
and we're gonna look at just the two
least significant bits of that pointer
as the pointer changes.
And at least on my implementation,
this is not portable.
On my implementation
this is what I got out.
So that worked and I'm
not seeing anybody saying
"That's undefined behavior,"
so I think I'm safe.
Here's what happens,
we're moving on to a
general reinterpret cast.
What happens when you convert
a pointer to something
to a pointer to something very different?
You can convert any pointer
to any other pointer type.
And accessing through that pointer,
so the conversion itself,
the compiler is not gonna yell at you.
You can go ahead and make that change.
But accessing through the pointer is
very well likely to be undefined behavior
based on a number of issues.
So is the representation valid?
It goes as fast as it goes.
Yeah, okay.
What we're about to talk
about is this last line,
strict aliasing rules.
The strict aliasing
rules have been with us
for a long time.
They were introduced in C
back in the mid-80s I think.
But before we can talk
about strict aliasing
let's stop about aliasing.
What we've got here,
the sum_twice function.
It takes pointers to two integers
and it adds them twice.
And I want you to think about
what you'd expect this to do
and I'm calling that
function down here below.
And what I'm getting out is a six.
This all makes sense?
Now we're gonna get into aliasing.
The second two lines in my example.
We're passing the same pointer in.
The top function has no idea,
I mean it sees pointer A and pointer B,
those may point to different
things, but it doesn't have to.
So as soon as we call
pointer the function with
pointer A and pointer B as the same value
we're aliasing.
And the compiler is obliged
to do the right thing.
So that's aliasing.
And you will see that we do
get two different answers
based on whether we're aliasing or not.
So is aliasing a problem?
What it does is it slows
your compiler down because
code runs fastest in registers.
Pointers and references,
you can alias through references.
Pointers and references
both operate on memory.
They work on memory addresses.
So if a memory location might be alias
then the compiler is obliged to
either write to memory or read from memory
if that alias might have
been used in the interim.
That's aliasing and that's
why it's not your friend
most of the time.
Sometimes you really wanna do it.
But when it can happen your
compiler will slow down.
And so here are the strict aliasing rules.
There are a set of object types
through which you are the compiler
is obliged to do the right thing.
The strict aliasing rules list those.
They're all here.
I'm not gonna read them
out, they're in the slides,
so maybe I can go a little
bit faster this way.
You'll notice that in
the end you're always
allowed to access through a char,
and inside a char or a standard byte
just like we saw before.
So the reason for the
strict aliasing rules is
so your compiler can run faster
so that there are fewer
pointers that may be aliasing,
that are forcing it to go back to memory.
That's the point.
So here's an example
of typical type punning
that is a strict aliasing violation.
So what we're gonna do is we're gonna swap
the halves of a 32-bit
integer through that pointer.
So we're taking in object
which is a 32-bit thingy,
we're making a 16-bit
pointer so we can access that
and we're swapping those things around.
The compiler is this is
strictly by the standard.
This is undefined behavior,
and by the way when I did this in client
it worked just fine.
So no problems, with an 03.
So the optimizer saw what I was up to
and did me the favor of doing
what I hoped it would do.
You can disable district
aliasing rule on most compilers.
But if you do this your code
will probably run slower
because it has to go to memory more often.
I will mention that
since we're talking about
making your code run more slowly,
in C you can tell your compiler that know
you can make a promise to your compiler,
there are no aliases to this pointer.
And that terminology is
restrict in the C standard.
And since many C++
compilers also support C,
many of them have
extensions where you can do
something that has the word restrict in it
so that you can say this section
that I'm really worried about,
I can guarantee to you
that there are no aliases,
please optimize this the best you can.
But then the onus is on you.
Don't lie to your compiler.
Regarding pointers.
Signed to unsigned, that's well defined.
uintptr_t between pointers and integers,
that's well defined.
Eric tells me what you do
with that integer is risky,
not gonna argue with that.
And then accessing through
a standard byte char
or byte star or an unsigned char star.
That's okay.
Let's talk about unions.
Oh boy.
All right.
Let me talk quickly about unions.
So there is a way that you
can type pun with unions
but it's not probably what you think.
So there is in a standard an escape clause
regarding unions that if
you have inside of the union
standard layout structs,
and this was the point you were getting to
or made, be related to the point
you were getting to earlier.
So the standard layout
structs inside of a union
and it's also a standard layout union,
then you're allowed to access the
common initial sequence
if there happen to be
things that are in common
between those structs
and they're at the front,
then the standards specifically
allows you to do that.
What's a standard layout struct?
The definition, you can find
it there in the standard.
It's a long definition.
But the point of it is
to constrain the data
so that it's shareable.
That's the reason that
they care about that
because they're trying to be
create a type which is
shareable between languages.
And you can identify it
with a static_assert.
So the compiler will look
at this type and tell you
whether it's what you want or not.
So you can avoid actually trying to
drive through the standard divs.
This is an example of a common
initial sequence and a union.
So I've got a nil, a bus and a car,
and what you'll notice is
that they have a common
type t at the front.
So in my example I'm setting
my vehicle as a bus
and setting that value.
But when I access for that assert
I'm not accessing through the bus.
I'm accessing through
t which is the nil type
so I can find out that
there is a bus in there,
and the standard says that's okay.
What it doesn't say is that you can do
anything else funny with a union.
So with a union you've got
an active member of the union,
and it becomes active
when you assign to it
or do a placement new into it.
That's how it becomes active.
And that's the active member of the union.
So you can access the
active member of a union
if you have standard layout structs
and there's a common sequence.
You can access that.
Any access to a non-active member union
other than what we've just talked about,
that's undefined.
Mean time over in the C standard, C11,
there is this exact paragraph.
This is footnote number 95.
This is not normative behavior,
but the paragraph itself
is extremely clear.
So I think that if someone,
if a compiler vendor chose not to follow
the advise of this non-normative note,
they'd be in a lot of
trouble from the users.
So I think there's a lot of power
in this non-normative note.
This says that in C11 if you do
type punning through a union,
you're on good ground.
That's the way I read it.
In C11 again in non-normative text,
so it's not binding by the
standard but it's very clear.
Access is well defined.
And my belief is, I'm not talking about
what our standard says,
my belief is that if you have a compiler
that supports both C11 and C++17
your compiler is likely to give you
the C11 behavior simply because
it's too much work not to.
They've written that code,
they know how to make that work.
I'm not saying that the letter of the law
is that accessing through the,
so no guarantees.
Let's see if we can race through
unrelated object types.
I've got five minutes.
I think these are unrelated object types.
I think there's very little to do
with a submarine and three polar bears.
So we're gonna do an exercise.
What we're gonna do is we're gonna,
for good reasons, we're
gonna take an integer
which will be a NaN payload
and we're gonna turn that,
we're gonna wrap a float
around that integer.
So this is the NaN with a payload,
that's something that the floating point
IEEE standard, whatever that number is,
this is something that's well described.
So this is the function we wanna write.
We wanna put the contents in there.
We have to use type type punning.
So we're gonna start with a
strict aliasing violation.
This is the easiest one to do.
We're gonna take cargo,
which is our uint32
that carries our payload.
We're gonna
pour in to our cargo the stuff
that has to go at the front
that turns that cargo into a NaN.
And now what we're gonna do
is we're gonna return that
as a float using that reinterpret cast.
The reinterpret cast is
not undefined behavior.
That's just fine.
What is undefined behavior is that return
that was pointed out.
Next option is a union.
We've already talked about unions.
What we're about to do is we're gonna
drop cargo into our union of a uint,
the combined uint float.
So we're dropping,
we start out by initializing
that with either
the positive header or
the negative header.
And then on the ret.i or equals cargo,
we're gonna or in the cargo,
and that's all fine because
we're operating on integers.
And now we're gonna access
the float in the union
and that's undefined behavior.
Now we're gonna use mem copy.
What we're gonna do is we've got
our separate float per return.
We're gonna do the bit
manipulations over here on cargo
'cause it's easy to do that on an int.
Now we're gonna mem copy
that into the return value
and then we're gonna return that.
Now the question is is
this undefined behavior.
This is absolutely, if
you look on the web,
this is the answer you'll get.
The web tells you do this.
So if you look at what the standard
has to say about this,
again I'm just a programmer,
when I'm dealing with a
standard I am struggling.
But the standard does explicitly say
that if you're copying
the underlying bytes
for the same trivially copyable type
that this is okay, this
is a swell thing to do
if it's the same type.
And then there's silence.
So when I read that my
inclination is to say
that this is undefined behavior.
But I suspect that if I talk with somebody
who is actually in core
they will say that it's not undefined
because mem copy uses unsigned
char as its access method.
So I think this is really
skating on the edge.
Nevertheless this is widely
promoted as the way to do it.
And I wish that if this what
the standard wants to promote
that they would come out and say it.
- Write a proposal.
- Yeah.
- And use its byte.
- Yeah, okay.
I think that proposal may very
well be coming, thank you.
Hopefully I'm not too far overtime yet.
So the last choice,
this is the one that I'm pretty confident
doesn't have any undefined behavior
that if you just use unsigned
char star or stood byte,
the reason I didn't
stood byte in my examples
is my client doesn't support it yet.
That's the only reason.
I would have been using
stood byte everywhere
if it'd worked.
But using an unsigned char star,
pretty confident this is
not undefined behavior.
Those are my four examples.
And I should have
silenced my phone, sorry.
That was not a potential.
C++ folks care a lot about
how fast will something be.
So I went ahead and disassembled
all four of those examples.
When I counted instructions
I did leave out
there was a common header
to all four of them
where they all did an assert,
they did the same assert through that out.
That's not relevant.
So in terms of the changes
in the type punning methods,
and I'm compiling with
clang on Mac OS to dash O3.
So this is maximum optimization.
And I'm not saying don't follow this trick
to aliasing rules.
Strict aliasing rules
are absolutely in play.
Nevertheless what Clang
produced for me was
nine instructions for the
one with the unsigned char,
eight instructions for all
three of the other examples.
And for all three of the other examples
despite how different
they looked in the code,
the exact assembly
instructions were the same
for all three examples.
I think I need to jump to the end
which is probably good
because I was about to
embark on some more undefined behavior.
So let's jump to the summary.
- [Participant] (mumbles)
terminating your app.
We did go overtime.
- Yeah.
The summary, I was very proud of this pun.
Avoid modifying non-mutable const values.
Consider the effects of
possible pointer aliasing.
Be aware of that whether
you're type punning or not.
Be aware of the strict aliasing rules.
When you're punning with pointers,
prefer standard byte star
or unsigned char star.
For punning pointer math,
there was stuff that we skipped
that you can look at the slides later on,
pointer math is hard to get right
even if you leave it as a pointer.
So if you're doing pun pointer math
there may be good reasons
for you to do that
as a uintptr_t.
I would encourage you
to consider doing your
punning pointer math as a uintptr_t
and then argue with the committee
if you don't get the answers you want.
And I think the real cue of this is that
avoiding undefined behavior is hard.
Thank you very much for attending.
- [Participant] Thank you.
(crowd applauding)
