Where to start with this one. I opened a can of worms trying to write a lambda expression. Or more specifically, a function that takes a lambda expression as an argument. This seems easy enough, right? Lots of examples everywhere. But what would we learn by taking the easy way out?
My target for this lambda-accepting function is my crude collision detector, which is going to allow every shape in my OpenGL world to call “Collide” on every other shape, exactly once, with the obvious exception that a shape cannot collide with itself. Without lambdas this looks something like this:
std::vector<std::unique_ptr<Shape>> shapes;
// ... initialize shapes, more on this later ...
for (int i = 0; i < shapes.size() - 1; i++)
{
for (int j = i + 1; j < shapes.size(); j++)
{
shapes[i]->Collide(shapes[j].get());
shapes[j]->Collide(shapes[i].get());
}
}
The way I have written collision detection, it has to be done both ways – i.e. for two Shape
s to collide, we must call a->Collide(b)
and b->Collide(a)
. Each call represents the effect one Shape
has on the other.
What I’d really like to do is have a spiffy pairwise
method, one that we could invoke with a lambda like this:
shapes.pairwise([&](Shape* a, Shape* b) { a->Collide(b); });
This is much nicer than my nested for
loops. It’s much clearer, too, I think; for each possible pair, collide one with the other. What’s not to like.
Well here is where the fun starts. First of all, where do we put this pairwise method? My first thought is to subclass std::vector
, but OH NO YOU DIDN’T according to the internet… we are not supposed to do that. As best I can figure it’s because std::vector
has a private destructor which won’t get called when the subclass gets destroyed. Ok, that sounds reasonable enough. I’m not using that many methods on the shape vector anyway, so why not just make a new class with a vector as a member and re-implement the methods I need? This is known as “composition over inheritance”. Yay, it has a name.
So we have our new class, which I have called ShapeVector
. How do we initialize this thing? Just like we’d initialize a vector, right? Something like this:
ShapeVector shapes = {
std::make_unique<BoundingBox>(glm::vec3(-3, -3, -3), glm::vec3(3, 3, 3)),
std::make_unique<Cube>(glm::vec3(1.5, 0, 0)),
std::make_unique<Cube>(glm::vec3(-1.5, 0, 0))
};
Nice and tidy. Remember we are using unique_ptr
s to preserve polymorphism and be all modern like. What does this look like in our ShapeVector
constructor? Maybe one of those good old ellipsis things like in C? No, there is a new way! It’s called an initializer_list
, and it gets magically created when a construct like the above is used. Here’s how to copy stuff out of the initializer_list
into our internal array:
ShapeVector(std::initializer_list<std::unique_ptr<Shape>> shapes)
{
for (const std::unique_ptr<Shape>& shape : shapes)
{
this->shapes.push_back(std::move(shape));
}
}
Real simple right? We’re being all careful about const
and &
and move
. There’s only one problem, though… it doesn’t compile. You’ll get an ‘attempting to reference a deleted function’ error.
Here’s where you can go down a rabbit hole finding all kinds of hacks on the internet that don’t work. My advice – forget about this approach. Whoever created this initializer_list thing clearly did not want it to be used this way, so let’s just let them have that.
It turns out there’s this new ‘variadic function template’ thing you can do that is like the new version of the C ellipsis, here’s what that looks like:
template<typename... Args>
ShapeVector(Args... args) {
addshapes(args...);
};
template<typename T>
void addshapes(T& shape) {
shapes.push_back(std::move(shape));
}
template<typename T, typename... Args>
void addshapes(T& shape, Args&... args) {
shapes.push_back(std::move(shape));
addshapes(args...);
}
Now I’m not going to lie to you here, I don’t totally understand all this template/typename stuff. I copied it off the internet. What I think this code is saying is that you can pass in a bunch of anything you want, and I’m going to try to stuff it into the shapes
vector whether it’s the right type or not. But since I’m me, and I know what I’m going to pass in, we’re going to sidestep that issue for the moment.
One notable thing about this new construction is the recursive nature of addshapes
. Apparently there is no way to just iterate through this args
thing! They forgot to make that part. I guess I am glad they didn’t put it in an initializer_list
.
Also of note is that we had to make the addshapes
method because we can’t do this recursive business with constructors. And, all this template junk has to be in the header file, because reasons. I am beginning to think this is a language for people who like a lot of intricate and mysterious rules. Because who doesn’t, really?
Anyways, our initialization now looks like this:
// create vector of shapes
ShapeVector shapes(
std::make_unique<BoundingBox>(glm::vec3(-3, -3, -3), glm::vec3(3, 3, 3)),
std::make_unique<Cube>(glm::vec3(1.5, 0, 0)),
std::make_unique<Cube>(glm::vec3(-1.5, 0, 0))
);
And that isn’t so bad, really. Parentheses instead of curly braces, and no equals sign. Heck I think it looks better, if you want my honest opinion.
Weren’t we doing some kind of lambda thing? Ah yes, here is what that looks like – also a template, in the header file.
template<typename Func>
void pairwise(Func pairfunc)
{
for (int i = 0; i < shapes.size() - 1; i++) {
for (int j = i + 1; j < shapes.size(); j++) {
pairfunc(shapes[i].get(), shapes[j].get());
pairfunc(shapes[j].get(), shapes[i].get());
}
}
}
Again, I think this template stuff is saying that I can pass in any old function I want, and we’re just going to go tits up if it’s not the right signature. That seems unfortunate. Perhaps we will find a way to fix that later. But for now, this allows me to do the pairwise
lambda call I described above, so I’m happy.
I also made a foreach
call that looks like this:
template<typename Func>
void foreach(Func eachfunc)
{
for (auto& shape : shapes) eachfunc(shape.get());
}
That one wasn’t as satisfying, because I’m basically turning a one-liner into another one-liner. Let’s do something interesting – let’s define an operator! How often do you get to do that in C++. Let’s define the []
operator, since that makes sense for a vector-esque class.
Shape* operator[](int i)
{
return shapes[i].get();
}
This is really basic stuff, but it’s exciting to me, okay? If that is not interesting enough for you, did you know that if you have an array of int
s you can reverse the operands for []
and it does the same thing? Like, if your array is called ints
, you can reference ints[5]
or 5[ints]
and you’ll get the same result. There, now you know.