[HN Gopher] The Evolutions of Lambdas in C++14, C++17 and C++20
       ___________________________________________________________________
        
       The Evolutions of Lambdas in C++14, C++17 and C++20
        
       Author : ingve
       Score  : 155 points
       Date   : 2021-12-13 07:55 UTC (15 hours ago)
        
 (HTM) web link (www.fluentcpp.com)
 (TXT) w3m dump (www.fluentcpp.com)
        
       | kello wrote:
       | I had a friend show me some of the stuff you can do in C++
       | nowadays. I had no idea C++ had evolved so much!
        
       | bluescarni wrote:
       | Another new lambda feature in C++20 is default constructability.
       | I am surprised this is not mentioned in the article.
        
       | b20000 wrote:
       | i was in a leetcode BS interview a while ago with a googler who
       | thought i could not write code in c++ because i could not
       | remember lambda syntax. i only use maybe 30% of c++ features in
       | my software and have been doing this for over 20 years. it's only
       | recently i started using lambdas more, but still stay away from
       | them because of the potential hidden allocations.
        
         | Kranar wrote:
         | I interview a lot of C++ developers who have 20 or more years
         | of experience I feel really bad when the vast majority of them
         | fail to keep up with modern standards or take the time to
         | understand their tools resulting in all kinds of misconceptions
         | based on outdated information.
         | 
         | Lambda expressions do not involve any kind of hidden
         | allocations, their definition is precisely formalized and can
         | be reviewed in S 7.5.5 of the standard. Even if you don't care
         | to read the standard, there is no shortage of resources online
         | that explain what a lambda expression is, and none of them
         | involve anything to do with hidden allocations:
         | 
         | https://en.cppreference.com/w/cpp/language/lambda
         | 
         | I'm sorry to pick on you specifically, but it's a major problem
         | that is entirely unnecessary. It's almost heartbreaking that
         | the people who should have the most experience in a subject
         | based on decades of knowledge are often the ones who carry the
         | biggest misconceptions and spread the most misinformation.
        
         | steerablesafe wrote:
         | What kind of hidden allocations?
        
           | huhtenberg wrote:
           | That are used to hold captured variables and values.
           | 
           | Internally, lambdas are function pointers + the context, and
           | the context may or may not be dynamically allocated depending
           | on how lambdas are used.
        
             | bstamour wrote:
             | Internally, lambdas are structs with the "function call"
             | operator overload. Context is done via members of the
             | struct: capture by-value, and the struct copies them and
             | stores its own, capture by reference, and the struct member
             | is a reference. There should be zero heap allocation in any
             | case.
        
               | jjtheblunt wrote:
               | If I'm reading this right, then "lambda" in C++ isn't
               | "lambda" in Scheme/CommonLisp, where captured variables
               | need be heap allocated rather than stack allocated to
               | construct closures?
        
               | gpderetta wrote:
               | It is complicated. In C++ closures can close over local
               | variables either by value or by reference (the choice can
               | be made for each variable closed over).
               | 
               | When closing over a variable by reference, if the lambda
               | need to survive the local scope heap allocating the
               | closure itself won't help. Instead you need to explicitly
               | heap allocate the closed over variable itself (and close
               | over the, usually smart, pointer).
               | 
               | When closing over by vale, there is no such issue, closed
               | over variables are copied over along the lambda and it
               | can be safely, for example, be returned from a function.
               | 
               | Copying might be expensive if the lambda is closing over
               | an expensive to copy object, but move semantics are
               | always an option.
               | 
               | Lambdas are value types, they are usually copied around.
               | so when closing over ither va
        
               | bstamour wrote:
               | You're right. Lambdas in C++ are syntactic sugar over
               | something the core language has allowed you to do since
               | before C++98. Since you control which variables you
               | capture into the closure, and how, when writing the
               | lambda, all of the information regarding how to make it
               | (e.g. it's size, data members, etc) is static, and so it
               | does not require the use of the heap at all. A concrete
               | example. The following C++ examples do the same thing,
               | and neither touch the heap:                   // A. Using
               | a struct. The old manual way.         struct my_lambda {
               | my_lambda(int c) : c_(c) {}             int operator()
               | (int x) const {                 return x == c_;
               | }             int c_;         };                  auto
               | f(vector<int> numbers) {             my_lambda lam(2);
               | // remove all the matching elements
               | erase_if(numbers, lam);             return numbers;
               | }              // B. Using a lambda         auto
               | g(vector<int> numbers) {             int c = 2;
               | erase_if(numbers, [c](int x) { return x == c; });
               | return numbers;         }
        
               | huhtenberg wrote:
               | You capture by value a variable that allocates and you
               | get a heap allocation.
        
               | kllrnohj wrote:
               | That's dependent on the captured type's copy (or move)
               | behavior, and is completely different from your claim
               | that how the lambda is used can influence this. Lambdas
               | themselves never heap allocate.
        
             | MauranKilom wrote:
             | People who think that lambdas may cause heap allocations
             | are confusing lambdas with std::function.
             | 
             | Lambdas are essentially syntactic sugar for an anonymous
             | struct holding references to/copies of captured variables
             | and an operator() method with your code. Not 100% precise,
             | but memory-wise that's how it works (and has to work). No
             | dynamic memory allocation is involved here at all.
             | 
             | If you put a lambda (or any other callable) in a
             | std::function, the std::function will copy that functor
             | object, either into its own small-buffer-optimized in-place
             | storage or (above a certain functor size) into heap-
             | allocated memory.
        
             | agent327 wrote:
             | There is no dynamic allocation at all, unless you assign
             | the lambda to an std::function, in which case it _may_
             | require dynamic allocation if the size of the captured
             | variables exceed the internal storage capacity (typically
             | 32 bytes) of std::function. There are types (usually called
             | 'function_view') floating around that don't dynamically
             | allocate at all, and that can be used as a cheap
             | alternative when the function is not stored for later use.
        
         | cyber_kinetist wrote:
         | If you're not capturing variables that allocate memory (like
         | std::vector or std::string), there will be no allocations. And
         | if you're that sensitive about hidden allocations, chances are
         | that you're going to avoid most of these STL types in the first
         | place (no allocating constructors or RAII), so you won't
         | probably need to worry about this. Even the people using C-like
         | C++ ("Orthodox" C++) seem to use lambdas liberally when it make
         | sense.
         | 
         | (Or maybe you're talking about putting a lambda into an
         | std::function, which is a totally different thing and will
         | probably incur heap allocation?)
        
       | mgaunard wrote:
       | Template lambdas is in my opinion the most useful evolution and
       | has been a long time coming. I figure it was held back by
       | template haters who want terse syntax and don't understand how
       | it's much more powerful than auto/concepts.
       | 
       | I wouldn't say it's a smaller evolution than previous iterations.
        
         | ncmncm wrote:
         | Agreed. C++11 was a major evolutionary step for the language,
         | but C++14 made it fun, too. C++20 is as large a step as C++11,
         | but it will take as long for people to absorb. C++23 will bring
         | usability improvements for async/await and maturity to library
         | ranges.
        
       | samsaga2 wrote:
       | Is there any flag in some c++ compiler that forces to you to use
       | the newer features? For example, forbidding non-auto
       | declarations.
        
         | MauranKilom wrote:
         | There is tooling along those lines. I wouldn't expect to find a
         | tool that _forbids_ non-auto declarations, but there are
         | certainly some that _suggest_ when something could 've been
         | auto.
        
           | MontagFTB wrote:
           | Right. clang-tidy lets you specify rules you'd like it to
           | enforce in the code you pass through it. It's not a part of
           | the compilation process, but uses the same underlying driver
           | (if you're building with clang)
        
         | cjfd wrote:
         | Quite apart from not being possible forbidding non-auto
         | declarations also sounds like a terrible idea. I am not really
         | that fond of the auto keyword. In some cases it is necessary
         | and/or helpful but it is also very helpful to be able to tell
         | what the type of a variable is at a glance. I think the auto
         | keyword should be used sparingly.
        
         | criddell wrote:
         | No.
        
       | stabbles wrote:
       | C++23 drop the word auto?                   myLambda = [](&&x){
       | std::cout << x << '\n'; };
       | 
       | what's the point of having to write auto everywhere
        
         | bruce343434 wrote:
         | syntactical disambiguation, there's already something called
         | "the most vexing parse". C++ is full of little holes like these
         | and getting rid of auto would surely at least double compile
         | times or at worst make parsing C++ actually impossible.
        
           | gsliepen wrote:
           | Yes, those things creep up in various places. Some are fixed,
           | like when you have std::vector<std::vector<int>> where it
           | used to give an error because it thought >> was an operator.
           | But recently I discovered this one:                   int
           | x[100] = {}; // just a regular array         int val =
           | x[[]{return 42;}()]; // compile error!
           | 
           | Once again, greedy parsing is the culprit; here it thinks [[
           | is the start of an attribute.
        
             | klyrs wrote:
             | On one hand, nice catch... on the other hand... why do you
             | want to do that??
        
           | pjmlp wrote:
           | That is what happens when a language makes the compromise to
           | be copy-paste compatible with https://cdecl.org/
        
             | ncmncm wrote:
             | More to the point, it means the language continues to
             | interpret header files from C libraries the same way that
             | the C compiler always has.
        
               | pjmlp wrote:
               | It goes both ways, back in CFront days, and when it was
               | picking up steam across MS-DOS, Mac OS, UNIX, among
               | others.
               | 
               | Unfortunely it is also an hindrance to fix some issues
               | that prevent C++ to ever embrace safety, or better
               | implementation of static analysers.
        
               | ncmncm wrote:
               | Both which ways? C was adopting C++ features before C90.
               | Void type, function prototypes, scoped struct member
               | tags.
               | 
               | Declaration syntax is not an impediment to safety or to
               | static analyzers, except insofar as it is trickier to
               | code a parser for them.
        
         | steerablesafe wrote:
         | Problem is, function parameter names are optional, so you can
         | already have something like `[](x){}`, where `x` is a type.
        
         | kzrdude wrote:
         | As seen from Python vs Lua: without a keyword for introduction
         | it's less clear which scope a variable belongs to. Scopes are
         | part of deterministic destruction in C++ and should not be
         | taken lightly.
         | 
         | As a general preference, I'd always want a keyword to introduce
         | a new variable, and most languages seem to agree even if they
         | didn't have to.
        
           | gpderetta wrote:
           | In this case x must obviously be in a new scope as all
           | function parameters. There isn't possibly be an ambiguity
           | here.
        
             | rich_sasha wrote:
             | I think grandparent is referring to myLambda - that could
             | be a new variable or overwriting of an existing one.
             | 
             | I agree with them, in Python the number of times something
             | like this happens:                   myVariable = 10
             | myVariablr = clever_function(myVariable)
             | 
             | (note the typo in variable name)
        
               | CraigJPerry wrote:
               | >> in Python the number of times something like this
               | happens
               | 
               | Anec-data but I've been writing python for 15+ years,
               | I've contributed to various popular open source projects.
               | I've never seen this in a code review. I've certainly
               | never seen this kind of mistake released.
        
               | NavinF wrote:
               | I've seen a variation of this bug several times in just
               | the last year:                   myVariable = 1
               | if not something_unusual():             myVariablr = 2
               | return myVariablr
               | 
               | This code will work just fine until something_unusual()
               | returns True and then it crashes with UnboundLocalError:
               | local variable 'myVariablr' referenced before assignment
               | 
               | This really sucks when it happens during a long-running
               | job. Say for example you're looping through an array in
               | chunks and distributing each chunk across GPUs. The last
               | chunk will have fewer elements than all the others, maybe
               | even 1 element. If you don't pad the chunk, it will go
               | through a different (and likely underutilized) code path
               | downstream and crash like this.
        
               | CraigJPerry wrote:
               | VSCode (pylance) & PyCharm both immediately identify
               | myVariable as unused - this is inline in the editor,
               | before any compilation or testing.
               | 
               | Assuming somehow this code made it into a pull request
               | (although as above, there would be no reason for that) -
               | then flake8 flags myVariable as unused, your CI pipeline
               | wouldn't get as far as notifying a peer that the PR is
               | ready for review.
        
               | formerly_proven wrote:
               | Dynamic languages often benefit from some tool
               | assistance. Both your and GPs experience can happen at
               | the same time: GP is probably using a linter or an IDE
               | (like Pycharm) which will point out that "myVariablr" may
               | be undefined.
        
               | rich_sasha wrote:
               | If you're returning myVariable instead, that's "fine" -
               | it is always defined.
               | 
               | You might get a warning that myVariabl_r_ is unused, but
               | that's also not a given. The bar for being 'used' is
               | pretty low. For example, in this                 def
               | main():           x = 10           l = []
               | l.append(x)           return 5
               | 
               | pyflakes believes both x and l are "used", even though
               | neither affects function return value.
        
               | formerly_proven wrote:
               | In Python even the trivial definition of "used" (i.e.
               | "bound name which is referred to afterwards") is not that
               | easy (e.g. getattr), linters go for the literal
               | definition instead ("bound name, which is statically
               | referred to afterwards"). This works for 99 % of code.
               | Your definition of "used" (~"influences the observable
               | result") is of course fully within the dominion of Rice's
               | theorem, though this particular example would be caught
               | by any data-flow analyzer.
        
               | fractalb wrote:
               | Those type of errors will be caught by the compiler. It's
               | definitely a problem for python-like languages (but not
               | for compiled languages) because there's no compilation
               | step involved before running the code.
        
               | NavinF wrote:
               | It's a compiled language in the same sense that Java is
               | compiled. Most people use the CPython implementation
               | which compiles .py files to .pyc files before executing
               | them. That compiler can't catch bugs of this form because
               | it's not always a bug. That's just how dynamic languages
               | work.
               | 
               | But if Python had a different syntax like "let var =
               | value" for creating new variables rather than mutating
               | existing ones, we wouldn't have this specific problem.
               | That's what this thread is about.
               | 
               | If C++ inferred "auto" for all
               | assignments/initializations, it would have a similar
               | problem where typos in assignments effectively lead to
               | that line of code getting skipped.
        
               | gpderetta wrote:
               | Oh I didn't even realize that OP had removed auto from
               | myVariable. Of course I strongly agree that auto or some
               | other syntactic marker is needed there!
        
       | gpderetta wrote:
       | Regarding returning lambdas, of course it could be done even in
       | c++11 via std::function.
        
         | steerablesafe wrote:
         | That is not really the same. `std::function` is a heavy-weight
         | type-erased wrapper over callable types.
        
           | nickysielicki wrote:
           | Unfortunately it's the only way to get a recursive lambda, as
           | far as I'm aware.
        
             | gpderetta wrote:
             | You can use a fix point combinator.
        
         | mhogomchungu wrote:
         | Problems with std::function:-
         | 
         | 1. It can not hold move only callable objects.
         | 
         | 2. It heap allocate stored callable object if the object is
         | large enough.
        
           | stargrazer wrote:
           | Can you point to better alternate ways or idioms?
        
             | MontagFTB wrote:
             | stlab has a task type that works around a couple of these
             | issues: https://stlab.cc/libraries/concurrency/task/task/
        
             | jjtheblunt wrote:
             | (joke) : Common Lisp
        
             | MauranKilom wrote:
             | It's possible to write something like
             | `std::unique_function` (which uniquely owns the stored
             | callable and can be moved but not copied). That's often
             | preferable for storing functors.
             | 
             | Example: https://github.com/facebook/folly/blob/main/folly/
             | docs/Funct...
        
           | nly wrote:
           | 3. Type-erased (its primary use case, but hinders
           | optimization)
           | 
           | 4. Can't be constexpr
        
             | tcbawo wrote:
             | Also, std::functions are not comparable. This makes it
             | harder to use for registration/de-registration or an
             | Observer architecture.
        
           | pvarangot wrote:
           | I think lambdas also heap allocate, at least on embedded
           | C++11/14 projects I remember having to have a little of heap
           | for them.
        
       | vitus wrote:
       | > Even if you don't need to handle several types, this can be
       | useful to avoid repetition and make the code more compact and
       | readable.
       | 
       | ...
       | 
       | > namespace1::namespace2::namespace3::ACertainTypeOfWidget
       | 
       | Deeply nested namespaces are problematic in themselves due to
       | namespace resolution (e.g. see https://abseil.io/tips/130), but
       | templates should never be an answer to "my type is too long to
       | type out". You're hurting yourself at compile-time, and your code
       | is now more error-prone as you can pass in any argument.
       | 
       | Just add a using-decl (or a namespace alias if the base name
       | isn't meaningful enough for you).
        
         | cjfd wrote:
         | Yes, deeply nested namespaces are not nice. I think boost
         | really dropped the ball on this. boost::asio::ip::tcp should
         | have been boost::tcp. Are they trying to be as bad as java?
        
           | tialaramex wrote:
           | As the article explains it's not "as bad as Java" it's worse,
           | what Java does actually _makes sense_ in Java, maybe you have
           | to type slightly more characters but Java is already a
           | verbose language best suited to heavy tool-assist. However it
           | doesn 't make sense in C++ because the benefits Java gets
           | don't apply and the price is heavier.
           | 
           | There are a bunch of things like this in C++ where
           | superficially C++ feature X is like feature Y in another
           | language, and so C++ programmers wrongly assume the problems
           | with feature X must also plague feature Y. I'm sure the
           | reverse happens too.
        
             | kllrnohj wrote:
             | Your argument seems to be "Java can be bad at this because
             | it assumes an IDE, but C++ can't because it can't assume an
             | IDE". Which is nonsense. If anything nested namespaces make
             | sense in C++ but _don 't_ in Java, since Java has no
             | nesting semantics and doesn't have using aliases. In Java
             | it's just raw noise, whereas in C++ it's actual structure.
             | Java isn't getting "benefits" from deep package paths.
             | 
             | Absl's arguments are that you shouldn't use that structure
             | much, so you should avoid deep nesting (specifically _deep_
             | nesting, not _no_ nesting). But it actually _does_
             | something in C++, which isn 't true in Java. And that's
             | really the core of Absl's complaints about nesting - don't
             | copy your Java package name (which is irrelevant useless
             | noise) to your C++ code (where it's actual structure, and
             | collisions can result in issues)
        
             | jlarocco wrote:
             | > There are a bunch of things like this in C++ where
             | superficially C++ feature X is like feature Y in another
             | language, and so C++ programmers wrongly assume the
             | problems with feature X must also plague feature Y. I'm
             | sure the reverse happens too.
             | 
             | Yes! I've noticed this also. Several recent C++ changes are
             | copying idioms from other languages, and sometimes the
             | standards committee seems to have missed the point or
             | misunderstood how the features are used in the language
             | they're copying.
             | 
             | The results are superficially similar, but not quite what
             | I'd expect after knowing the feature from the source
             | language.
        
         | MauranKilom wrote:
         | > templates should never be an answer to "my type is too long
         | to type out"
         | 
         | Eh, in some cases it's a wash.
         | std::vector<std::unique_ptr<mynamespace::MyWidget>> objects;
         | // ...         auto it = std::find_if(objects.begin(),
         | objects.end(), [](auto& widget) { return widget->x == 1; });
         | 
         | Sure, you could repeat the type
         | `std::unique_ptr<mynamespace::MyWidget>` in the lambda, but
         | that's just noise. You don't spell out the types like that
         | either in, say, C#. Yes, compilation time is an epsilon slower,
         | but that consideration loses to readability any time of day.
         | (Otherwise you could just remove all comments and indentation
         | from your code - that also makes compilation faster!)
        
           | adamrezich wrote:
           | I am fairly certain template expansion and skipping over
           | whitespace while parsing are two entirely orthogonal things.
           | using a template might make compilation an "epsilon" slower
           | in one instance of such a use, but then those epsilons add
           | up...
        
           | jcelerier wrote:
           | Is it really slower ? The compiler has to compute the type of
           | the right hand side in any case. It just has one less
           | computation to do now (type checking the conversion to the
           | left hand type)
        
       | 1024core wrote:
       | Generalised capture              In C++11, lambdas can only
       | capture existing objects in their scope:         int z = 42;
       | auto myLambda = [z](int x){ std::cout << x << '-' << z + 2 <<
       | '\n'; };         ...         But with the powerful generalised
       | lambda capture, we can initialise captured values with about
       | anything.         int z = 42;         auto myLambda = [y = z +
       | 2](int x){ std::cout << x << '-' << y << '\n'; };
       | 
       | Am I the only one who doesn't see much of a difference here?
        
         | ynik wrote:
         | Generalized capture is primarily useful for move semantics:
         | std::function<int()> make_func(std::unique_ptr<int> ptr) {
         | return [p = std::move(ptr)]() { return *p; }        }
         | 
         | Without generalized capture syntax, there's no good way to
         | transfer ownership into the closure (a regular `[ptr]` capture
         | would attempt to make a copy, and a by-ref `[&ptr]` capture
         | would lead to a use-after-free).
        
         | [deleted]
        
       ___________________________________________________________________
       (page generated 2021-12-13 23:01 UTC)