1

I am learning C++ using the resource listed here. In particular, i read about lambda expressions in C++ Primer by Lippman. There i came to know that lambdas are function objects. Moreover, classes generated from lambda expressions do not have a default constructor. So take for example:

auto wc = find_if(words.begin(), words.end(),
            [sz](const string &a){
                    return s.size() >= sz;
            };

It is written that the above lambda expression will generate a class that will look something like:

class SizeComp {
    SizeComp(size_t n): sz(n) { } // parameter for each captured
variable
    // call operator with the same return type, parameters, and body as the lambda
    bool operator()(const string &s) const
        { return s.size() >= sz; }
private:
    size_t sz; // a data member for each variable captured by value
};

So what happens is that an unnamed object of this compiler generated class is created and is passed as the third argument to the std::find_if shown above. I can understand this.

Now it is my thinking/understanding that since this compiler generated class has no default ctor, so the third argument that is passed to the std::find_if must not be created using a default ctor since that would fail. So, internally the object that is passed should be created using the parameterized ctor something like:

auto wc = find_if(words.begin(), words.end(), SizeComp(sz)); //note the third argument uses the parameterized ctor

My first question is that since there is no default ctor in case of an empty capture list in C++11, so how will an object of that compiler generated class be created. I mean in case of an empty capture list, there will be no data members inside the compiler generated class so there will be no parameterized ctor. Which means that this compiler generated class has neither default nor parameterized ctor. Then how can an object of this class be generated. For example, the situation shown below won't work when the capture list is empty:

auto wc = find_if(words.begin(), words.end(),
            [](const string &a){           //NOTE THE CAPTURE LIST IS EMPTY
                    return s.size() >= 5;
            };

Now, how will the third argument to std::find_if be created:

auto wc = find_if(words.begin(), words.end(), CompGeneratedClass()); //this can't work because there is no default ctor

I also know that for a normal/ordinary user-defined class if we provide a user-defined ctor, then the compiler will not generate a default ctor for that class. So, it seems to me that since in case of an empty capture list lambda expression there is no user-defined ctor, the compiler should generate a default ctor for this compiler generated class. But the C++ standard says that there won't be a default ctor. So my second question is that why does this compiler generated class has different behavior than an user-defined class.


Also, note that this explanation is not only limited to C++ Primer by Lippman. I've seen the same explanation in CppCon presentations as well.

Jason
  • 36,170
  • 5
  • 26
  • 60
  • Not sure that the class object needs to be constructed at all. A combination of `inline` and `static` attributes, plus the fact that no reference to `this` is needed, would doubtless allow the compiler to work round that issue. – Adrian Mole Apr 12 '22 at 14:51
  • 1
    As of C++20 a lambda with an empty capture list will generate a default ctor. As for why it was decided that is should not previously and why this was reversed, I am curious as well. – Chuu Apr 12 '22 at 14:52
  • @Chuu Yes i read that too(that in C++20 empty capture list lambda will have default ctor), and got curious which is why i mentioned `c++11` in my question. – Jason Apr 12 '22 at 14:54
  • 2
    lambda expressions are special. [The lambda expression is a prvalue expression of unique unnamed non-union non-aggregate class type](https://en.cppreference.com/w/cpp/language/lambda). Ie you don't need to calll a constructor to construct it. Also you can only get your hands on the type if you have an object first. – 463035818_is_not_an_ai Apr 12 '22 at 14:55
  • 1
    Who says the closure type for a captureless lambda doesn’t get a constructor with an unnameable type as a parameter? That’s more or less what happens with captures anyway (even in C++20). – Davis Herring Apr 12 '22 at 15:27
  • Naive me would have expected, that if the capture list is empty, no anonymous struct would be generated at all, but that it just degenerates to a plain old function pointer. – BitTickler Apr 13 '22 at 04:01
  • @BitTickler: A lambda closure is more than a simple function as you can have a templated method in it. So you are able to generate a pointer to this "empty" object and call all instances of the lambda function template. If it is only a function, you can't pick a pointer as you can't specify the signature of it. – Klaus Apr 13 '22 at 07:37

1 Answers1

2

The problem here is that of frame of mind. While the Primer is explaining the behavior in ways that one could easily understand, it is not to be taken literally. The normative text describes behavior, not ways to achieve it. A compiler doesn't actually need to create a class the same way we do.

For instance, a vendor can always sneak in some secret tag into the constructor's parameter list. Imagine this is what it generates:

struct __secret_tag_at_src_line{};

struct __lambda_at_src_line{
   __lambda_at_src_line(__secret_tag_at_src_line, /*Other arguments*/)
   // ...
};

// ...

auto wc = find_if(words.begin(), words.end(), __lambda_at_src_line(__secret_tag_at_src_line{}, ...)};

We can't name that tag or even know of its existence, so we can't create the lambda on another line. And the class will never have a default constructor. That's a way to do it. But even then, a compiler doesn't have to do that.

The implementation is not bound by the same rules as us; the standard gives great leeway to implementations, so long as the program is translated correctly. A compiler could just bless a byte and say "this is a lambda", no need to call a constructor. Why?! Because it accomplishes what the standard says; the observable behavior is intact (think about it, can the initialization of a capture-less lambda be observed by a C++ program?)

Sometimes implementations need to "cheat"; heck, it's been known that implementations will call private constructors to get the job done. But it's not really cheating, because the same rules do not apply.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • When you wrote: `__lambda_at_src_line(__secret_tag_at_src_line{}, ...)` does the expression in the first argument `__secret_tag_at_src_line{}` is list initialization? Or aggregate initialization or something else . – Jason Apr 13 '22 at 08:27
  • @Rick - All of the above, since the syntax and semantics are intertwined. But if you want to pin point it, then the tag is an aggregate so it creates a small object by aggregate initialization. Having said all that, that has no bearing on the rest of the answer. – StoryTeller - Unslander Monica Apr 13 '22 at 08:30
  • So as long as the implementation produces the observable behavior required by the standard C++, the compilers are free to use techniques that are not mentioned in the standard itself. For example, some implementations use an implied this parameter for non-static member functions and pass the address of the object to the implied this parameter. Is my understanding correct? – Jason Apr 13 '22 at 08:39
  • @Rick- Yes. Same as virtual function dispatch relies on techniques that aren't prescribed by the standard. – StoryTeller - Unslander Monica Apr 13 '22 at 08:41
  • Ok,+1 for the clarification.I have one more query.But it still means that the compilers are not allowed to create a default ctor for the lambda right?since the standard specifically says that classes generated from lambdas will not have defautl ctor.I mean,say a given implementation is able to successfully produce the required observable behavior but that implementation does so internally by having a default ctor for the class generated by the lambda.Is this allowed?Or the implementation still have to follow the rule that no default ctor will be generated even if required behavior wasobserved? – Jason Apr 13 '22 at 08:45
  • @Rick - As I tried to explain already, everything is allowed so long as a well-formed program is translated correctly and an ill-formed one produces a diagnostic. If the implementation produces a "default constructor", but the lambda's type keeps behaving like it doesn't have one and the diagnostics are issues upon attempts at default construction, then that's fine too. – StoryTeller - Unslander Monica Apr 13 '22 at 08:51
  • Also, note that this explanation is not only limited to C++ Primer by Lippman. I've seen the same explanation in [CppCon](https://youtu.be/IgNUBw3vcO4?t=2019) slides as well. – Jason Apr 13 '22 at 11:44
  • @Rick - Popularity and accuracy are not in any direct correlation. – StoryTeller - Unslander Monica Apr 13 '22 at 12:14
  • Yes i agree. My point was that these resources should at least point out this important subtle thing about lambdas since this question follows logically from their given description. You may disagree with me here but that's perfectly okay. It's just my point of view. – Jason Apr 13 '22 at 12:17
  • @Rick - I'm not sure they do. They are about providing intuition, not language lawyering. Things getting auto-magically created is not new (see initializer_list). I doubt the majority of folks are troubled by that minutiae. – StoryTeller - Unslander Monica Apr 13 '22 at 12:21
  • I completely disagree with you on this. It's their responsibility to provide the correct consistent explanation (when they're explaining something at a professional level) – Jason Apr 13 '22 at 13:16
  • @Rick - A rather idiosyncratic take on the matter. It's optimizing for the wrong metric, but I don't think we'll come to an agreement on much here. Anyway, as far as the question itself, I consider it answered, so I'm off. Best of luck in your learning. – StoryTeller - Unslander Monica Apr 13 '22 at 13:23
  • It's not idiosyncratic to ask for an explanation. In science, we go step by step. If any of those steps leads to an unforseen problem, then it isn't bad to ask for a clarification. Moreover, as teachers, people(teachers themselves) should make sure that what they're explaining is consistent. I am not saying that they should explain each and every detail but atleast they should make sure that their sayings don't contradict each other. Anyways, this is getting more and more opinion based. – Jason Apr 14 '22 at 16:05