2

I am attempting to create a wrapper around an std::function for reasons not explained here. I know that the following code works.

std::function<void()> function = [&]() -> void {};

The following is my wrapper around the std::function, however, it does not work when I try to construct it with a lambda.


template<typename Type, typename ...Args>
class custom_function {
public:
    using function_type = std::function<Type(Args...)>;

    custom_function(const function_type &other) {
        function = other;
    }

private:
    function_type function;
};

// error: conversion from 'main()::<lambda()>' to non-scalar type 'custom_function<void>' requested
custom_function<void> function = [&]() -> void {

};

I thought that this would work since a lambda can be assigned to a std::function. If I add the following constructor, the code now compiles.

template<typename Type, typename ...Args>
class custom_function {
    // ...

    template<typename Lambda>
    custom_function(const Lambda &other) {
        function = other;
    }

    // ...
};

// this is now valid
custom_function<void> function = [&]() -> void {

};

Why does this constructor work but the previous constructor did not? Why is...

custom_function(const function_type &other) {
    function = other;
}

different from...

template<typename Lambda>
custom_function(const Lambda &other) {
    function = other;
}

I'm compiling with C++17 using G++ on Windows. Thanks

Jaan
  • 330
  • 2
  • 9

1 Answers1

2

It's one too many implicit conversion steps. Generally speaking, C will let you get away with one level of indirection. So we could call a function that expects a double and pass it an int, and things will work fine, because there's an implicit conversion from double to int. Now let's look at your code.

custom_function(const function_type &other) {
  function = other;
}

This is a converting constructor, which is just a fancy way of saying it's a constructor that takes one argument of some other type. When we write

custom_function<void> f = [&]() -> void {

};

This is a copy initialization. In principle, it's constructing something on the right hand side and then assigning it to the left. Since we don't explicitly call a constructor on the right hand side, we have to convert our lambda into custom_function<void>. We could do that if either

  • There was an conversion operator to custom_function<void> defined on the right hand type (which will never be the case here, since the right hand type is a lambda type), or
  • There was a converting constructor on custom_function<void> that takes a lambda as argument. This won't work here, since the only constructor takes a std::function. And we're already talking about a conversion, so we're not going to consider doing another conversion to get to std::function.

When you replace your constructor with a template function that can take any type, then that template function suffices as a valid conversion from the lambda type directly to your custom function type.

Note that you can also use brace initialization to directly call your constructor. This will work with either constructor.

custom_function<void> f { [&]() -> void {} };

That's because this is an actual explicit constructor call and therefore will consider implicit conversions to get from the argument type to the declared constructor parameter type.


See also this discussion on the different initialization techniques.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
Silvio Mayolo
  • 62,821
  • 6
  • 74
  • 116
  • In short, the converting constructor is not chosen because lambda is not `std::function`. – 273K Oct 21 '22 at 23:47
  • @273K Isn't that the point of the converting constructor though? To convert the lambda to the `std::function`? Please correct me if I am wrong (evidently I am since the code I wrote doesn't compile). Thanks for the well explained answer. – Jaan Oct 21 '22 at 23:49
  • 1
    The converting constructor (at least, your first one) converts `std::function` to `custom_function`. It says nothing about lambdas. If you want to accept a lambda *as a conversion argument*, you need the template one. If you're calling the constructor explicitly (i.e. not as part of a conversion) then C++ will fill in the gap and go from lambda to `std::function` in order to call your function. – Silvio Mayolo Oct 21 '22 at 23:50
  • @SilvioMayolo I think I understand now. The difference is in the fact that I invoked the assignment operator which automatically called the constructor, and not the constructor directly. Thanks again – Jaan Oct 21 '22 at 23:53
  • 1
    Exactly! Technically, the `=` on that line is called *copy initialization*, which is different than a traditional assignment operator in subtle ways. But your instinct is spot-on, just a subtle distinction of terminology. – Silvio Mayolo Oct 21 '22 at 23:54
  • @Jaan No, this is the point of converting `std::function` to `custom_function`. C++ does not do several steps conversions, otherwise there is a chance to convert all types to a single type. – 273K Oct 21 '22 at 23:56