Lambda expressions as class template parameters

44,468

Solution 1

As of C++20, this answer is now outdated. C++20 introduces stateless lambdas in unevaluated contexts1:

This restriction was originally designed to prevent lambdas from appearing in signatures, which would have opened a can of worm for mangling because lambdas are required to have unique types. However, the restriction is much stronger than it needs to be, and it is indeed possible to achieve the same effect without it

Some restrictions are still in place (e.g. lambdas still can't appear on function signatures), but the described usecase is now completely valid and the declaration of a variable is no longer necessary.


I'm asking if you can do something like:

Foo<decltype([]()->void { })> foo;

No you can't, because lambda expressions shall not appear in an unevaluated context (such as decltype and sizeof, amongst others). C++0x FDIS, 5.1.2 [expr.prim.lambda] p2

The evaluation of a lambda-expression results in a prvalue temporary (12.2). This temporary is called the closure object. A lambda-expression shall not appear in an unevaluated operand (Clause 5). [ Note: A closure object behaves like a function object (20.8).—end note ] (emphasis mine)

You would need to first create a specific lambda and then use decltype on that:

auto my_comp = [](const std::string& left, const std::string& right) -> bool {
  // whatever
}

typedef std::unordered_map<
  std::string,
  std::string,
  std::hash<std::string>,
  decltype(my_comp)
  > map_type;

That is because each lambda-derived closure object could have a completely different type, they're like anonymous functions after all.

Solution 2

@Xeo gave you the reason, so I'll give you the work around.

Often times you do not wish to name a closure, in this case, you can use std::function, which is a type:

typedef std::unordered_map<
  std::string,
  std::string,
  std::hash<std::string>,
  std::function<bool(std::string const&, std::string const&)>
  > map_type;

Note that it captures exactly the signature of the function, and no more.

Then you may simply write the lambda when building the map.

Note that with unordered_map, if you change the equality comparison, you'd better change the hash to match the behavior. Objects that compare equal shall have the same hash.

Solution 3

C++20 answer: yes!

You can totally do something like

Foo<decltype([]()->void { })> foo;

As c++20 allows stateless lambdas in unevaluated contexts.

Solution 4

You can't do this with a closure, because the state is not contained in the type.

If your lambda is stateless (no captures), then you should be ok. In this case the lambda decays to an ordinary function pointer, which you can use as a template argument instead of some lambda type.

gcc doesn't like it though. http://ideone.com/bHM3n

Share:
44,468
Channel72
Author by

Channel72

Updated on July 08, 2020

Comments

  • Channel72
    Channel72 almost 4 years

    Can lambda expressions be used as class template parameters? (Note this is a very different question than this one, which asks if a lambda expression itself can be templated.)

    I'm asking if you can do something like:

    template <class Functor> 
    struct Foo { };
    // ...
    Foo<decltype([]()->void { })> foo;
    

    This would be useful in cases where, for example, a class template has various parameters like equal_to or something, which are usually implemented as one-liner functors. For example, suppose I want to instantiate a hash table which uses my own custom equality comparison function. I'd like to be able to say something like:

    typedef std::unordered_map<
      std::string,
      std::string,
      std::hash<std::string>,
      decltype([](const std::string& s1, const std::string& s2)->bool 
        { /* Custom implementation of equal_to */ })
      > map_type;
    

    But I tested this on GCC 4.4 and 4.6, and it doesn't work, apparently because the anonymous type created by a lambda expression doesn't have a default constructor. (I recall a similar issue with boost::bind.) Is there some reason the draft standard doesn't allow this, or am I wrong and it is allowed but GCC is just behind in their implementation?

  • There is nothing we can do
    There is nothing we can do about 13 years
    @Xeo the "anonymous functions" aren't really functions. This should be said aloud, for it is confusing.
  • There is nothing we can do
    There is nothing we can do about 13 years
    @Xeo connect.microsoft.com/VisualStudio/feedback/details/636117/… - After reading this one really wonders why anyone even came up with the idea of calling them unnamed fncs? Wouldn't it be better and actually correct if they were called "officialy" unnamed objects? I will never, ever ever call lambda expression unnamed fnc for the reason that it just simply isn't a fnc.
  • Ben Voigt
    Ben Voigt about 13 years
    @There: Without captures, they are unnameable functions. With captures, they are unnameable functors. Using "fncs" as an abbreviation is really bad because it doesn't distinguish functions from functors.
  • There is nothing we can do
    There is nothing we can do about 13 years
    @Ben fnctr and fnc are very distinctive ;) ok, but where is it saying (the thing you've said about them being either this or that ;), would you mind to "link me" to the source?
  • Xeo
    Xeo about 13 years
    @There: Same paragraph as mentioned in the answer, but p6: "The closure type for a lambda-expression with no lambda-capture has a public non-virtual non-explicit const conversion function to pointer to function having the same parameter and return types as the closure type’s function call operator. The value returned by this conversion function shall be the address of a function that, when invoked, has the same effect as invoking the closure type’s function call operator."
  • Ben Voigt
    Ben Voigt about 13 years
    @There: section 5.1.2, paras 1, 2, and 6. p1 and p2 explain that lambdas create function objects (functors). p6 says that a non-capturing lambda corresponds to an ordinary free function (enabling it to be stored in an ordinary function pointer).
  • Ben Voigt
    Ben Voigt about 13 years
    Not useful, std::function doesn't contain the operator() behavior that the actual lambda type does.
  • Ben Voigt
    Ben Voigt about 13 years
    I think the goal is for the type to express the behavior, and std::function only expresses the signature.
  • ijprest
    ijprest about 13 years
    Confirmed that the workaround you give works as expected on VC2010.
  • ijprest
    ijprest about 13 years
    Using the workaround that Xeo posted, you can use captures... you would just need to pass your lambda to the unordered_map in the constructor (because it can't construct one itself).
  • There is nothing we can do
    There is nothing we can do about 13 years
    @Ben it may "correspond" to an ordinary free function but it is still an object not a function.
  • Ben Voigt
    Ben Voigt about 13 years
    @There: The result of the lambda expression is an object, but the logic is in an ordinary function (rather than a member function). And the object decays to a pointer to this ordinary function.
  • Ben Voigt
    Ben Voigt about 13 years
    @ijpriest: I don't see anything in Xeo's answer which passes a lambda object around. The lambda object is created for the sole purpose of passing to decltype. Which isn't to say that more complex usage isn't possible, but I think you'll run into trouble with needing the lambda to be at global scope in order to name the template instance, which does preclude capturing. You could use a combination of std::function as suggested by @DeadMG and a lambda object specified at runtime, but then you sacrifice the efficiency of compile-time polymorphism.
  • There is nothing we can do
    There is nothing we can do about 13 years
    @Ben I'm glad you say that it's an object. LAMBDAS AREN'T UNNAMED FUNCTIONS.
  • Xeo
    Xeo about 13 years
    @There: But they are convertible to unnamed functions.
  • There is nothing we can do
    There is nothing we can do about 13 years
    @Xeo and 0 is convertible to nullptr but you wouldn't call it nullptr would you? And true is convertible to 1 and yet you wouldn't call it "a literal" 1 would you? And just because X is convertible to Y it doesn't mean that X is Y. That's why for example you don't eat 5 euro or dollars or whatever the currency is in country you live in for a lunch but you eat something into which you've "converted" those 5 euro.
  • Ben Voigt
    Ben Voigt about 13 years
    @There: Is a function not simply an object for which the function call operator (operator()()) is defined to run the associated code? One could say that a captureless lambda is the very definition of an unnameable function.
  • Xeo
    Xeo about 13 years
    @Ben: A function is basically always a function pointer, which can even be dereferenced (try (*main)() as an example). What you get back from the dereference is a function designator (I think that's what it's called) which immediatly and silently is converted back to a function pointer (you can do (********main)().
  • Ben Voigt
    Ben Voigt about 13 years
    @Xeo: In C, yes. In C++, function designators aren't converted to function pointers if the function-call operator is applied, in most other contexts the implicit conversion does take place. Point is function pointers are objects. So just because a lambda is an object does not mean a captureless lambda is not a function. One notes that captureless lambda objects and function designators both implicitly decay to function pointers.
  • There is nothing we can do
    There is nothing we can do about 13 years
    @Ben arrays also decay "on demand" to a pointer and yet you call them arrays not pointers. The point is that lambdas aren't unnamed fncs. They are unnamed objects. And when you're saying "Is a function not simply an object for..." I can say one thing: Not in C++ sense.
  • Joseph Garvin
    Joseph Garvin about 12 years
    @BenVoigt: How is it different? I thought std::function was based on boost::function, which behaves the same, doesn't it?
  • Ben Voigt
    Ben Voigt about 12 years
    @Joseph: The behavior is contained in a particular instance of function. But the template is using only the type, not an instance, and thus there is no behavior. The hashtable implementation is going to create a new instance whenever it wants to invoke the functor, and function is abstract -- it can't be instantiated.
  • Matthieu M.
    Matthieu M. about 12 years
    @BenVoigt: a default constructed function throws std::bad_function_call if invoked (it is basically useless). What it means in that on top of providing the type used for storage, you should provide a comparison function instance when building an instance of map_type; for example a lambda. Note that the comparison object is only built once, not at every invocation. It can be stateful if you ask it to be and function is not abstract. Really. See the doc.
  • Ben Voigt
    Ben Voigt about 12 years
    @MatthieuM: Yes, there's a mismatch between the design in this question, and proper usage of std::function. Although you could design a hashtable that uses std::function, it would be a totally different design from what Channel72 requested.
  • Matthieu M.
    Matthieu M. about 12 years
    @BenVoigt: which is why I am talking about a work around rather than a solution, since it does not match exactly the requirements.
  • Ben Voigt
    Ben Voigt about 12 years
    @MatthieuM: Right. My comment was explaining this to Joseph, who asked about this today, as indicated by the @Joseph in said comment.
  • Periata Breatta
    Periata Breatta over 7 years
    @ijprest -- no, you can't use the workaround posted be Xeo for this. Trying it gives an error like this one: error: use of deleted function ‘<lambda(int, int)>::<lambda>()’.
  • ijprest
    ijprest over 7 years
    @PeriataBreatta: Hence my 'pass your lambda ... can't construct one itself' qualifier. You would have to call the long-form of the unordered_map constructor, which allows you to pass an instance of the lambda as the last parameter.
  • Periata Breatta
    Periata Breatta over 7 years
    @ijprest - yes... unfortunately not all template classes have such a long form constructor. :(
  • Ben Voigt
    Ben Voigt over 7 years
    @ijprest: When this answer was written, local types weren't permitted as template arguments. That's why I previously commented "needing the lambda to be at global scope in order to name the template instance". The restriction finally did get lifted in a new C++ standard, making this possible.