So, there was the world of C++ before the lambda. And now there’s the world of C++ since the lambda – and I’ve got to say, if you aren’t using lamdas, then your’e probably still in the C++ stone age. This post is going to be all about lamdas and how rad they are. Do Java or .Net have anything as cool? Nope. C++ is where it’s at, and you need to be in the Know.
First off, C++ always had some sort of lamda ability, with functors, and indeed modern lamdas are just functor sugar. But, it’s really nice sugar and the style has gone way past mere ancient functors.
So this is an example functor:
class Functor {
int a{ 0 };
public:
int operator()() {
return ++a;
}
};
int main()
{
Functor f;
f();
f();
f();
std::cout << "value of f: " << f() << std::endl;
//writes 4
}
An object that has the call operator – can be perceived as a function. What may not be apparent though, is that it can have member variables and its own instance. The instance could be ephemeral or it could persist for a long time.
Functors ended up being needed to write a lot of algorithms in the STL, if not most of them, and they created a lot of slop.
int main()
{
std::vector<int> v{ 9,0,1,4,1,9,6,4,1 };
class MyCustomSorter{
public:
bool operator()(const int& a, const int& b) {
return a < b;
}
};
std::sort(v.begin(), v.end(), MyCustomSorter());
}
So now with lamdas we can do it like so:
std::sort(v.begin(), v.end(), [](const int& a, const int& b)->bool {
return a < b;
});
What’s really cool about this line, is that we have actually declared a class-like object inline a function call, but with a lot less code. And what’s cool is that there are three different areas to pass arguments, the first area the example uses is the lambda parameters area, or the parameters to the call operator.
operator()(const int& a, const int& b) == (const int& a, const int& b)
The second area it uses is the return type
bool == -> bool
The third area is to pass in members of the lambda. Stuff that persists until the lamda goes out of scope. We could do the following:
std::vector<int> v{ 9,0,1,4,1,9,6,4,1 };
std::sort(v.begin(), v.end(), [&](const int& a, const int& b)->bool {
return a < b;
});
Amperstand, &, in this case, causes anything from the superscope to be passed by reference. So given that, we could have a lamda body as follows:
std::vector<int> v{ 9,0,1,4,1,9,6,4,1 };
ObjectA obja;
std::sort(v.begin(), v.end(), [&](const int& a, const int& b)->bool {
if (obja.method())
return false;
return a < b;
});
What happens in this case, is that the lamda is compiled with an ObjectA& member, which is set at the lamda creation and remains until it goes out of scope. The scope of these variables can sometimes be tricky as it is possible for a lamda to retain a reference to an out of scope variable (crash).
What we have been given the ability to do here is pretty incredible, we can define objects inline, anywhere, as lamdas. Here’s a threading example, with a lamda inside a lamda:
int result=-1;
std::thread threadA([&result]() {
bool working=true;
int i = 0;
auto doWork = [&]()->int {
while (working) {
if (++i > 10) working = false;
}
return i;
};
result = doWork();
});
threadA.join();
So we can define functions inside of functions, which are basically private function methods, and that’s actually really useful. Really, this is an entirely new C++ paradigm. Consider the following as well:
x++;
y++;
z++;
f1(x, y, z);
x++;
y++;
z++;
f1(x, y, z);
f2(x, y, z);
x++;
y++;
z++;
f1(x, y, z);
Could be improved like so:
auto f[&]() {
x++;
y++;
z++;
f1(x, y, z);
}
f();
f();
f2(x, y, z);
f();
This is cool because on the fly, I can create a function, that is specific to my current function, will never be used outside of it – absolutely no reason to pollute the external namespace. Yet I can clean up code and make it clearer. Comment what a block of code is doing? How about put that specific block of code in a lamda, and suddenly everything is clearer – the block of code has a name that isn’t a mere comment ( and the external namespace is pristine)! And you get the cool benefit of being able to call it multiple times or comment its call out ( no reason to have huge swaths of commented-out code).
In the vein of private function methods, if-statements can sometimes gain a lot from them; consider the following:
for (auto& user : mUsers) {
auto isUserReady = [](auto& user)->bool {
//...//
return true;
};
if (isUserReady(user)) {
//...//
}
}
Again, we’re saving the external namespace so much pollution! Generally when working like this, I like to declare the lamda about right before its going to be used, or near to it.
The only downside to this concept is potential code duplication – has this method already been written somewhere else? Could be hard to know all the time without getting a namespace exposure.
What’s become apparent is that we can pass lamdas as parameters to functions, this is hugely powerful as we can write our own functions that accept lamdas.
Want the user to be able to essentially modify your function but without overriding it? Lamda to the rescue.
template<typename Action>
void clean(const Action& deleteAction) {
for (auto& item : mItems) {
deleteAction(item);
}
mItems.clear();
}
The entire STL tends to work like this.
In C++ 20, lamdas are going to be further improved. From my current project, here is an example of a templated-lamda from C++ 20 preview.
auto createHlms = [&]<typename T>(T* &hlms) {
//...//
T::getDefaultPaths(mainPath, libraryPaths);
//...///
hlms = OGRE_NEW T(archive, &archives);
//...///
};
Ogre::HlmsUnlit* hlmsUnlit = nullptr;
createHlms(hlmsUnlit);
Ogre::HlmsPbs* hlmsPbs = nullptr;
createHlms(hlmsPbs);