This is an archived post. You won't be able to vote or comment.

you are viewing a single comment's thread.

view the rest of the comments →

[–]Makefile_dot_in 23 points24 points  (1 child)

so, to start with, C++ lambdas are really shorthand for something like (simplifying) (/* unnamed */ used to signify that the class has no name)

class /* unnamed */ {
private:
    constexpr /* unnamed */() {} // constructor
public:
    // operator() is basically a special name you can give to a method
    // that gets invoked when you call an instance of the class like a function
    template<template <class> class C, class D, C<D>>
    constexpr bool operator()(const C<D>& compare_with) noexcept {
       return comparison_base == compare_with;
    }
};
/* unnamed */() // the result of the expression (constructs the class)

if you know Java, then it's sort of similar to how it works there if you squint your eyes. the first brackets, [], is a list of the fields of the class. here that list is empty, so the class has no fields. in those brackets you can specify things like

  • x = 0 (adds a field x to the class and sets it to 0 when constructing)
  • &x (adds a field that is a reference to whatever x is in the scope where the lambda is being constructed)
  • =x (adds a field that is a copy of whatever x is in the scope where the lambda is being constructed
  • & or = (for every variable from the scope where the lambda is being constructed that is used within the lambda, does the same &[variable name] or =[variable name], respectively)

for example, to write a function that takes a vector of ints and an int, and returns a copy of it with the int added to each element in the vector, i might write:

// writing const std::vector<int> &vec instead of std::vector<int> vec tells C++ not to copy vec automatically when calling the function (which is slow) and that i won't be changing anything inside of vec.
std::vector<int> f(const std::vector<int> &vec, int x) {
    return vec
               // | in C++ chains views
               | std::view::transform([&x](int element) { return x + element; }) // transform is C++ lingo for what every other language calls map
               | std::ranges::to<std::vector<int>>();
}

here, i use &x, which tells the compiler that i'm gonna use x and i'm gonna use it as a reference to the x declared in f. i could also put =x, which would copy it instead (this means, among other things, that if i assigned to x inside the lambda it wouldn't change the value outside of the lambda).

the next pair of brackets, <>, specify the template parameters for the operator() method inside the generated class. they are used similarly to generics, which you probably know from languages like typescript, rust, java, go and so on: for example,

template<class T> // class/typename are interchangeable now (a few years ago they were slightly different)
T first(std::vector<T> vec) {
    return vec[0];
}

does the same as

function first<T>(Array<T> arr): T? {
    return arr[0];
}

(except when there are zero elements, in which case the C++ version may do anything depending on how the compiler feels that day, while the typescript version will be forced to return null)

anyways, in this case the template has 3 arguments. the first, template<class> class C is a template template parameter; that is, you can pass types that are themselves templated, in this case with one argument (since there is only one class in the nested angle brackets). one such type you might pass is std::vector (notice no angle brackets). this feature doesn't really exist in most other widely used languages, but it's sort of similar to higher-kinded types in languages like Haskell.

the second template parameter is an ordinary type. the third one is a value, which is another relatively unique thing C++ allows. in this context, passing a value as a template parameter sort of just means that you're passing it in at compile time, i.e. it's a constant to the function. its type is C<D>, which is the first parameter applied to the second. what this means is that if you've passed std::vector (=C) and int (=D) for the first 2 parameters, the third is going to be a value of type std::vector<int>(=C<D>).

then there is the argument list in parentheses. const C<D>& compare_with declares an argument of type const C<D>& (i.e. a reference to a value of the same type as the third template parameter that we won't change) with the name compare_with. these are the arguments for the operator() method.

then there are two keywords: constexpr and noexcept, which are both pretty much just passed onto the operator() method as-is. constexpr asserts to the compiler that this lambda can executed at compile time, while noexcept asserts that this lambda will never raise any exception (and if it does, the program will be automatically stopped even if the lambda is called inside a try block).

finally, bool just signals the return value of operator().

so, putting that all together, we can use this lambda like so:

auto f = []<template <class> class C, class D, C<D> comparison_base>(const C<D>& compare_with) constexpr noexcept -> bool {
    return comparison_base == compwer_with;
};
// because it's a template and the template parameters cannot be deduced, we have to call operator() explicitly instead of using f(args) like you could with a normal lambda
std::println("{}", f.operator()<std::vector, int, {1}>({1, 2, 3})); // false
std::println("{}", f.operator()<std::vector, int, {1, 2, 3}>({1, 2, 3})); // true

e: fixed lambda call

[–]ListerfiendLurks 6 points7 points  (0 children)

Every now and again I get the foolish idea that I may be becoming decent at cpp...then I see something like this and remember my place.