Hurricane Development
Hurricane Development

Reputation: 2464

Multiple std::variant visit with variadic templated helper

I am attempting to make a function that helps handling N std::variant types.

Note: I am trying to make all pathways compile time validated. So std::optional and std::holds_alternative are not viable for me.

The implementation is as follows:

template<typename T>
using Possible = std::variant<std::monostate, T>;

template<typename... Types>
void ifAll(std::function<void(Types...)> all, Possible<Types>&&... possibles)
{
    std::visit(
        [&](auto&&... args) {
            if constexpr ((... &&
                           std::is_same_v<std::decay_t<decltype(args)>, Types>))
            {
                return all(std::forward<Types>(args)...);
            }
            else
            {
                std::cout << "At least one type is monostate" << std::endl;
            }
        },
        possibles...);
}

And an example of using the function is:

int main()
{
    Possible<int>  a = 16;
    Possible<bool> b = true;

    ifAll([](const int& x, const bool& y)
              -> void { std::cout << "All types set!" << std::endl; },
          a,
          b);
}

However I get a compiler error:

TestFile.cc: error: no matching function for call to 'ifAll'
    ifAll([](const int& x, const bool& y)
    ^~~~~

TestFile.cc: note: candidate template ignored: could not match
    'function<void (type-parameter-0-0...)>' against '(lambda at
    TestFile.cc)'

void ifAll(std::function<void(Types...)> all, Possible<Types>&&... possibles)
    ^

Why does the lambda I provide not match the function signature?

Attempted Fix 1

I tried moving in a and b which still does not work:

ifAll([](const int& x, const bool& y)
              -> void { std::cout << "All types set!" << std::endl; },
          std::move(a),
          std::move(b));

Upvotes: 9

Views: 1418

Answers (5)

Jarod42
Jarod42

Reputation: 218343

Possible<Types>&& is actually rvalue reference, and not forwarding reference. You have so to add overloads to handle the different cases.

template<F, typename... Types> void ifAll(F, const Possible<Types>&...);
template<F, typename... Types> void ifAll(F, Possible<Types>&...);
template<F, typename... Types> void ifAll(F, Possible<Types>&&...);

In template<typename... Types> void ifAll(std::function<void(Types...)>, Possible<Types>&...), Types has to be deduced twice, and so error happens when deduction mismatches.

In your case, you have first const int&, const bool& (Since CTAD with C++17) and then int, bool. Mismatch, so the error.

Several ways to fix the issues:

  • Fix call site (fragile solution):

    std::function<int, bool> f = [](const int& x, const bool& y)
                  -> void { std::cout << "All types set!" << std::endl; };
    ifAll(fun, a, b); // Assuming overload with lvalue references
    
  • Make one parameter non deducible:

    template<typename... Types>
    void ifAll(std::function<void(std::identity_type_t<Types>...)>, Possible<Types>&...)
    
  • Add extra template parameters:

    template<typename... Args, typename... Types>
    void ifAll(std::function<void(Args...)>, Possible<Types>&...)
    

    Possibly with some SFINAE.

  • Change completely argument (I would go for than one):

    template<F, typename... Types>
    void ifAll(F, Possible<Types>&...)
    // or even
    template<F, typename... Ts>
    void ifAll(F, Ts&&...); // Forwarding reference, no extra overloads to add.
    

    Possibly with some SFINAE.

Upvotes: 1

HTNW
HTNW

Reputation: 29193

As in the accepted answer, one problem is that you shouldn't have the && in Possible<Types>&&. This means you can only accept rvalue arguments, but in your usage example, a and b are lvalues. The easiest way to deal is to use forwarding references to handle all cases. The other problem is that you allow Types... to be deduced from the std::function argument. It's possible for the lambda's arguments to not exactly match the Possibles, and so you should prevent Types... from being deduced from the std::function and only take them from the Possibles. The easiest way to do this is to stick some kind of type computation in the template argument in the argument type, which makes the compiler give up trying to do deduction on that argument. Sometimes, you just use a dummy type_identity_t function, but, here, we can do something more interesting and get both birds with one stone

template<typename T>
struct get_handler_argument;
template<typename T>
struct get_handler_argument<Possible<T>> { using type = T&; };
template<typename T>
struct get_handler_argument<Possible<T> const> { using type = T const&; };
template<typename T>
using get_handler_argument_t = typename get_handler_argument<std::remove_reference_t<T>>::type;

template<typename... Vars>
void ifAll(std::function<void(get_handler_argument_t<Vars>...)> all, Vars&&... possibles) {
    std::visit(
        [&](auto&&... args) {
            if constexpr ((... &&
                           std::is_same_v<std::decay_t<decltype(args)>, std::decay_t<get_handler_argument_t<Vars>>>)) {
                return all(std::forward<get_handler_argument_t<Vars>>(args)...);
            } else {
                std::cout << "At least one type is monostate" << std::endl;
            }
        },
        possibles...);
}

What you lose here is that you can no longer directly provide a "present" value in place of a Possible value without explicitly wrapping it in the Possible constructor. This is no loss; just capture it instead of passing it. What you gain is that you can use a function call or what-have-you to initialize the Possibles, instead of having to save them in variables manually first. Your example works as-is with this definition, unlike the currently accepted answer.

Upvotes: 0

Anonymous1847
Anonymous1847

Reputation: 2598

My apologies. I believe the problem is rather twofold:

  1. You do not use std::move on passing lvalues (the variables a, b) to a function taking xvalue arguments (the Possible<Types>&&... expansion.)
  2. You try to convert std::function<void(const int&, const int&)> to std::function<void(int, int)> when passing the lambda to your function.

The compiler matches Types to (int, int) given the other arguments. It then tries to look for a std::function<void(int, int)> as the first argument. Instead, it gets a function lambda of the type void(*)(const int&, const int&). Thus there is a signature mismatch.

My suggestion would be to take a hint from the standard library, and instead of trying to use std::function objects of specific type, instead add a template parameter FuncType for the function type, and pass in the function pointer using that. I think this might be why standard algorithms take function type as a template parameter, even when they can deduce the approximate function signature that should be passed from the other template arguments.

Upvotes: 1

L. F.
L. F.

Reputation: 20649

An easy solution is to use a function object + std::optional:

#include <functional>
#include <optional>

struct Error {};

template <typename F, typename... Args>
decltype(auto) if_all(F&& f, Args&&... args)
{
    if ((args && ...)) {
        return std::invoke(std::forward<F>(f), *std::forward<Args>(args)...);
    } else {
        throw Error{};
    }
}

Usage example:

#include <functional>
#include <iostream>

int main()
{
    std::optional<int> a{5};
    std::optional<int> b{10};
    std::cout << if_all(std::plus{}, a, b) << '\n';
}

(live demo)

If you insist to use std::variant instead of std::optional (which is probably because of some misunderstandings about either of them), the idea is the same — you need to check if all arguments are "empty" first (maybe using std::holds_alternative), and unwrap the arguments after.

Upvotes: 1

NutCracker
NutCracker

Reputation: 12303

Following call would work:

int main() {
    Possible<int>  a = 16;
    Possible<bool> b = true;

    std::function<void(int, bool)> fun = [](int x, bool y) -> void {
        std::cout << "All types set!" << std::endl;
    };

    ifAll(fun,
          std::move(a),
          std::move(b));
}

or switch your function signature to:

template <typename... Types>
void ifAll(std::function<void(Types...)> const& all, Possible<Types>&... possibles)

and then you can call it without std::move:

int main() {
    Possible<int>  a = 16;
    Possible<bool> b = true;

    std::function<void(int, bool)> fun = [](int x, bool y) -> void {
        std::cout << "All types set!" << std::endl;
    };

    ifAll(fun, a, b);
}

Upvotes: 4

Related Questions