Hello, and welcome to C++ Weekly. I'm your host, Jason Turner, available for code reviews and
on-site training. In this episode I want to go back to the random number generator that I used
in my episode on stably generating random, or pseudo-random, game assets (episode 285). We got a
lot of feedback about this episode; and there's a couple of things that I would like to comment on.
It largely comes down to API design, so I think that this will be an interesting one.
All right, so this is the C++ implementation that I have of the
xoroshiro128. Now I took this from the implementation here which was released in
the public domain, and I had someone point out a point about this version that I linked to,
but also several things that came up about mine. Now the first that I want to address
is someone suggested that instead of providing a fork function that I should have considered making
a copy constructor that did the same thing. So this is the canonical form of a copy constructor,
and if I was going to do this in the copy constructor,
then I would need to do something kind of like, well, what the fork function is doing.
So let's just go ahead and bring that up here,
and it would look something kind of like this. Now I will say that I did actually consider
this before the episode and I strongly rejected it; and you might be staring at the code right
now and wondering why it fails to compile, or you might immediately see the error. Now the problem
is that I would actually need a non-const reference to the thing that is being forked,
so I would need it to do something like this. Now this becomes... well, also I don't want to
return from a constructor, that's just weird. So instead I will use delegating constructor syntax,
which I don't recall if we have ever discussed on this episode, but I will say
that this is a very handy feature of C++11 that is largely overlooked
by people because we can actually call constructors from other constructors.
So something to be aware of when you're using delegating constructors is that the
constructor still needs its own body. So what you do when you're calling a
delegating constructor is you have your own constructor signature and then you have a colon,
and just like you're initializing any other member function you call a different constructor,
and then you can do whatever other steps you want to after that other constructor has been called.
This is weird, I no longer have a valid copy constructor. If I actually wanted to copy this
thing I now need to add, like, a copy member function, or if I wanted to very much confuse
the users of my library then I could have const and non-const copy constructor overloads.
Don't do this. This is weird. So if I were to copy something that is
non-const I would mutate it; if I were to copy something that is const then I don't mutate it and
I get completely different behavior in these two scenarios. Don't do this. So I flat out rejected
this possibility which is why I went with the fork member function. So now let's look at the fork
member function. I did have someone point out that I don't need to call
'next' twice. Now I am trying to initialize two 64-bit values, and my 'next' is returning
a 64-bit int to me. They pointed out that I could do something like
this maybe, where I give it one value (or the default value for one of the values) and just call
next for the second value. That is totally legitimate because is this going to be any less
random in any way that we can detect? Not that I know of. We could use even the same value twice
potentially, and that would provide some extra, perhaps, level of randomness - I don't even know.
So I could do something like this in my fork function, and I'm totally cool with that as
well. That definitely reduces the amount of computation that needs to happen when I fork.
Someone else pointed out that I should have supported the 'jump' function. Now this is not a
very uncommon thing to see in pseudo-random number generators. You've got some sort of function that
knows how to skip so many different iterations of the random number generator. I didn't implement
this because there's a jump that skips 2⁶⁴ calls. Now the idea is that I wouldn't have forked,
I would have jumped instead. So I would have said, "From your current location, do a copy, and
then jump 2⁶⁴ iterations ahead" so that we know we're getting a very large gap between these two
different instances of a random number generator. I did consider doing this. I didn't because I
didn't understand how the jump function worked, and I've got 'jump', and I've got 'long-jump' - 1
is 2⁶⁴, the other one is 2⁹⁶. I have no idea how these things work - would it have made
sense for me to implement this without knowing exactly what I was doing? I didn't think so,
so I didn't; although I could have implemented my own 'jump'. This is effectively what it was doing.
(I really should be using max int here, the integer type traits, so
don't do this. This is bad code. I'll put a avatar up here or something to tell you to not do this.)
So this is basically what the other thing was doing, but of course it was doing it in a much
more efficient way. Actually doing this 2⁶⁴ times is is kind of crazy. I probably shouldn't do this.
That's actually doing it to the "64-1" times because I have it less than "max int" because
otherwise I would get an infinite loop here, which the compiler would detect.
It would be interesting to see if it actually... no, we don't see the generated code for this. I'm
guessing it's going to actually generate a loop, it's not going to know how to optimize this. It
would be interesting to try. Just out of curiosity I'm going to put it here. Yeah, there we go,
"processing time exceeded." That's not surprising. So I probably should have implemented some sort of
a jump. I'm totally going to agree with that. I'm also not going to implement one that is, you know,
would take the heat death of the universe to execute or anything like that, particularly
if the compiler cannot optimize it away in a reasonable way. I think the main thing to bring up
here is I called this function 'fork' because that seemed like a reasonable thing to call it to me,
but it turns out that there's already a name for this, because of course people have been
studying random number generators for far longer than I have. So this comment here from
mCoding describes a mistake that I made. What I am calling forking RNGs (random number generators)
is known as splitting in the literature, so for anyone else looking this up it's
called splitting. So "splittable pseudorandom number generators using cryptographic hashing,"
that's what it's called here. Now I should probably learn even more about what exactly
"splitting" refers to instead of just calling my function "forking," but when you're implementing
something and it exists in a certain space that already has its own vernacular and its own
vocabulary you really should go and look up what those things are and name your thing that you are
doing correctly. So it would, in this case, should not have been "fork," it should have been perhaps
something like "split," and I should have done more research into what is actually the correct
name for this so I don't surprise the users of my library. So this has all been about the principle
of least surprise basically. I don't want to implement something that isn't well described.
I don't want to surprise my user by mutating the objects that they passed in on a copy
construction, and I don't want to surprise the user of my library by using words that don't
make sense to someone who's already an expert in that domain. So I just thought this would be
an interesting episode to dive into a little bit in API design, mistakes that I made, and possible
future options for code like this. There's a whole lot of other comments about what I did here on
that episode of C++ Weekly, so make sure you check out that episode and learn from the comments that
other people have left. Thank you for watching this episode of C++ Weekly. You know what to do.