Michael Francis
Michael Francis

Reputation: 8737

Passing lambdas directly to functions

I'm writing an Option class which represents a value that may or may not exist. The if_opt function is intended to take an Option and a function which will be called on the value held in the Option, but only if the value exists.

template <class T>
class Option {
private:
    std::shared_ptr<T> m_value;
public:
    explicit operator bool()const noexcept
    {
        return (bool)m_value;
    }

    Option() = default;

    explicit Option(T value)
    {
        m_value =  std::make_shared<T>(value);
    }

    template <class U>
    friend void if_opt(Option<U>&, std::function<void(U&)>);
};

template <class T>
void if_opt(Option<T>& opt, std::function<void(T&)> f)
{
    if (opt) f(*opt.m_value);
};

I've noticed that this works if I use it like so:

Option<int> none;
Option<int> some(10);

function<void(int&)> f1 = [](int& none)
{
    cout << "This should never be reached" << endl;
};

function<void(int&)> f2 = [](int& some)
{
    cout << "The value of some is: " << some << endl;
};

if_opt(none, f1);

if_opt(some, f2);

But I'd like to be able to put the lambda expression directly in the call, but when I do:

if_opt(none, [](int&)
{
    cout << "This should never be reached" << endl;
});

if_opt(some, [](int& some)
{
    cout << "The value of some is: " << some << endl;
});

I get an error:

error: no matching function for call to 'if_opt(Option<int>&, main()::<lambda(int&)>)'

I know that the type of a lambda expression is undefined in the standard, and that it merely has to be assignable to std::function<R(T)>, so this sort of makes sense, but is there a way that I can get the lambda argument to implicitly convert to a std::function<void(T&)> so that I can define the lambda in the call to if_opt the way I attempted?

Upvotes: 0

Views: 85

Answers (1)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275200

std::function<Sig> is a type erasure tool. It erases (almost) everything about the value it stores excpet that it can be invoked with Sig.

Template argument deduction takes a passed in type and deduces what types should be used, then the template function is generated and (usually) called.

These are almost inverses of each other. Doing deduction on a type erasure template is code smell, and almost always a bad idea.

So that is your fundamental design error.


There are a number of ways to fix your code.

First, if_opt shouldn't be a template.

friend void if_opt(Option<T>& opt, std::function<void(T&)> f){
  if (opt) f(*opt.m_value);
}

this creates what I call a Koenig friend. You have to define the body inline. Really, that U type is pointless, and can even lead to errors in some cases.

But the type erasure here is also pointless. Fixing that returns the template, but now for a good reason.

template<class F>
friend void if_opt(Option<T>& opt, F&& f){
  if (opt) f(*opt.m_value);
}

this is a better design.

You can go and invest in SFINAE overload resolution code, but I wouldn't bother.

template<class F,
  std::conditional_t<true, bool,std::result_of_t<F&(T&)>> = true
>
friend void if_opt(Option<T>& opt, F&& f){
  if (opt) f(*opt.m_value);
}

the above is obscure and has minimal marginal advantages over the one above it.

Upvotes: 2

Related Questions