使用可变参数模板助手多次 std::variant 访问

Multiple std::variant visit with variadic templated helper

我正在尝试制作一个有助于处理 N std::variant 类型的函数。

注意:我正在尝试验证所有路径的编译时间。所以 std::optionalstd::holds_alternative 对我来说不可行。

实现如下:

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...);
}

使用该函数的示例是:

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);
}

但是我得到一个编译器错误:

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)
    ^

为什么我提供的 lambda 与函数签名不匹配?

已尝试修复 1

我试过在 ab 中移动,但仍然不起作用:

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

一个简单的解决方案是使用函数对象 + 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{};
    }
}

用法示例:

#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)

如果你坚持使用std::variant而不是std::optional(这可能是因为对它们中的任何一个有一些误解),这个想法是一样的——你需要检查所有参数是否是"empty" 首先(可能使用 std::holds_alternative),然后展开参数。

以下调用有效:

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));
}

或将您的函数签名切换为:

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

然后你可以在没有 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);
}

抱歉。我认为问题是双重的:

  1. 您不使用 std::move 将左值(变量 ab)传递给采用 xvalue 参数的函数(Possible<Types>&&... 扩展。)
  2. 您在将 lambda 传递给您的函数时尝试将 std::function<void(const int&, const int&)> 转换为 std::function<void(int, int)>

给定其他参数,编译器将类型与 (int, int) 匹配。然后它会尝试寻找 std::function<void(int, int)> 作为第一个参数。相反,它获得类型为 void(*)(const int&, const int&) 的函数 lambda。因此签名不匹配。

我的建议是从标准库中获取提示,而不是尝试使用特定类型的 std::function 对象,而是为函数类型添加模板参数 FuncType,并且使用它传入函数指针。我认为这可能就是标准算法将函数类型作为模板参数的原因,即使它们可以推断出应该从其他模板参数传递的近似函数签名。

正如已接受的答案一样,一个问题是您不应该在 Possible<Types>&& 中使用 &&。这意味着您只能接受右值参数,但在您的用法示例中,ab 是左值。最简单的处理方法是使用转发引用来处理所有个案例。另一个问题是您允许从 std::function 参数推导出 Types... 。 lambda 的参数可能与 Possible 不完全匹配,因此您应该防止从 std::function 推导出 Types... 并且只从 Possible 中获取它们.最简单的方法是在参数类型的模板参数中加入某种类型的计算,这使得编译器放弃尝试对该参数进行推导。有时,你只是使用一个虚拟的 type_identity_t 函数,但是,在这里,我们可以做一些更有趣的事情,一石二鸟

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...);
}

您在这里失去的是,您不能再直接提供 "present" 值来代替 Possible 值,而无需在 Possible 构造函数中显式包装它。这没有损失;只是捕获它而不是传递它。您获得的是,您可以使用函数调用或您拥有的东西来初始化 Possible,而不必先手动将它们保存在变量中。与当前接受的答案不同,您的示例按原样使用此定义。

Possible<Types>&& 实际上是右值引用,而不是转发引用。 您必须添加重载来处理不同的情况。

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>&&...);

template<typename... Types> void ifAll(std::function<void(Types...)>, Possible<Types>&...)中,Types需要推导两次,推导不匹配就会出错

在你的情况下,你有第一个 const int&, const bool&(自 CTAD 与 C++17) 然后 int, bool。不匹配,所以报错。

解决问题的几种方法:

  • 修复调用站点(脆弱的解决方案):

    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
    
  • 使一个参数不可推导:

    template<typename... Types>
    void ifAll(std::function<void(std::identity_type_t<Types>...)>, Possible<Types>&...)
    
  • 添加额外的模板参数:

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

    可能与一些 SFINAE。

  • 完全改变论点(我会选择一个):

    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.
    

    可能与一些 SFINAE。