获取std::tuple元素作为std::variant

5

给定一个变量类型:

using Variant = std::variant<bool, char, int, float, double, std::string>;

还有一个元组类型,其中的元素被限制为此变体类型(可能存在重复和省略,但没有额外的类型):

using Tuple = std::tuple<char, int, int, double, std::string>;

如何在运行时实现按给定索引获取和设置元组元素作为 Variant 的方法:

Variant Get(const Tuple & val, size_t index);
void Set(Tuple & val, size_t index, const Variant & elem_v);

我在我的代码中有两种实现方式,但我印象中可能有更好的实现方式。我的第一个实现方式使用 std::function,而第二个则建立了一组指向某些 Accessor 指针的数组,这限制了移动和复制我的对象(因为其地址会改变)。我想知道是否有人知道正确的实现方式。

编辑1:

以下示例可能会澄清我的意思:

Tuple t = std::make_tuple(1, 2, 3, 5.0 "abc");
Variant v = Get(t, 1);
assert(std::get<int>(v) == 2);
Set(t, 5, Variant("xyz"));
assert(std::get<5>(t) == std::string("xyz"));

这到底是什么意思?Variant和tuple是完全不同的东西。你为什么要从一个转向另一个? - Barry
@Barry Variant 包含一个元组元素。请参见 EDIT1。 - Alexey Starinsky
2
@Barry 我不认为 OP 想要从一种类型转换到另一种类型。相反,他们似乎只是想使用在编译时未知的索引访问元组元素,并使用 std::variant 来处理该元素可能具有的各种可能类型。 - François Andrieux
@FrançoisAndrieux 是的,我错过了not,抱歉。 - Holt
你的“Set”函数看起来有毒。变量知道哪个被激活了。要么索引与已激活的匹配(好的),要么不匹配(这意味着什么,崩溃?未定义行为?抛出异常?) - Yakk - Adam Nevraumont
4个回答

8
我将继续推荐Boost.Mp11用于所有元编程事项,因为它总是有相应的函数。在这种情况下,我们需要mp_with_index函数。该函数将运行时索引提升为编译时索引。
Variant Get(Tuple const& val, size_t index)
{
    return mp_with_index<std::tuple_size_v<Tuple>>(
        index,
        [&](auto I){ return Variant(std::get<I>(val)); }
        );
}

考虑到在 OP 中,元组和变体的索引甚至不对齐,因此需要实际访问 Variant 而不是依赖索引的 Set。在这里使用 is_assignable 作为约束条件,但可以根据问题进行调整(例如,可能应该是 is_same)。
void Set(Tuple& val, size_t index, Variant const& elem_v)
{
    mp_with_index<std::tuple_size_v<Tuple>>(
        index,
        [&](auto I){
            std::visit([&](auto const& alt){
                if constexpr (std::is_assignable_v<
                        std::tuple_element_t<Tuple, I>,
                        decltype(alt)>)
                {
                    std::get<I>(val) = alt;
                } else {
                    throw /* something */;
                }
            }, elem_v);
        });
}

如果您需要确保Tuple中的每个类型在Variant中仅出现一次,并且希望直接从该类型分配而不进行任何转换,则可以简化为:
void Set(Tuple& val, size_t index, Variant const& elem_v)
{
    mp_with_index<std::tuple_size_v<Tuple>>(
        index,
        [&](auto I){
            using T = std::tuple_element_t<Tuple, I>;
            std::get<I>(val) = std::get<T>(elem_v);
        });
}

如果变体未与该类型配对,则会抛出异常。


1
I是隐式的constexpr吗?我以前从未见过将函数参数用作模板参数。 - François Andrieux
@FrançoisAndrieux I 是一个 integral_constant<size_t, K>,其中 K 为某个值。该类型转换为 size_t 的结果是一个常量表达式。 - Barry
mp_with_index源代码中的最大模板参数为15。这是否意味着此代码无法编译包含16个或更多元素的Tuple或Variant? - Alexey Starinsky
2
@AlexeyStarinsky 这是不正确的。没有这样的最大值。 - Barry
@Barry:不是吗?我看到有一些证据表明确实存在。https://www.boost.org/doc/libs/1_70_0/boost/mp11/detail/mp_with_index.hpp - Lightness Races in Orbit
显示剩余4条评论

3
以下是依赖递归来尝试将运行时索引匹配到编译时索引的get_runtime和set_runtime函数的可能实现方式:
template <class Variant, class Tuple, std::size_t Index = 0>
Variant get_runtime(Tuple &&tuple, std::size_t index) {
    if constexpr (Index == std::tuple_size_v<std::decay_t<Tuple>>) {
        throw "Index out of range for tuple";
    }
    else {
        if (index == Index) {
            return Variant{std::get<Index>(tuple)};
        }
        return get_runtime<Variant, Tuple, Index + 1>(
            std::forward<Tuple>(tuple), index);
    }
}


template <class Tuple, class Variant, std::size_t Index = 0>
void set_runtime(Tuple &tuple, std::size_t index, Variant const& variant) {
    if constexpr (Index == std::tuple_size_v<std::decay_t<Tuple>>) {
        throw "Index out of range for tuple";
    }
    else {
        if (index == Index) {
            // Note: You should check here that variant holds the correct type
            // before assigning.
            std::get<Index>(tuple) = 
                std::get<std::tuple_element_t<Index, Tuple>>(variant);
        }
        else {
            set_runtime<Tuple, Variant, Index + 1>(tuple, index, variant);
        }
    }
}

你可以像使用 GetSet 一样使用它们:
using Variant = std::variant<bool, char, int, float, double, std::string>;
using Tuple = std::tuple<char, int, int, double, std::string>;

Tuple t = std::make_tuple(1, 2, 3, 5.0, "abc");
Variant v = get_runtime<Variant>(t, 1);
assert(std::get<int>(v) == 2);
set_runtime(t, 4, Variant("xyz"));
assert(std::get<4>(t) == std::string("xyz"));

为什么要使用递归函数而不是折叠表达式? - Alexey Starinsky
1
@AlexeyStarinsky 因为我更熟悉这些 ;) 我不确定启用优化后使用折叠表达式与这种编译时递归相比,您是否会获得任何好处。 - Holt
1
@AlexeyStarinsky 看了其他答案,我发现在这种情况下,折叠表达式会创建混乱的代码,不太容易理解,而有了 if constexpr 的可用性,递归版本就非常简单明了。但也许某个地方有一个非常好的折叠表达式实现方式 ;) - Holt
是的,可能使用递归看起来比使用带有折叠表达式的伪lambda更好。 - Alexey Starinsky
你编译过这个吗?请参见 https://wandbox.org/permlink/nzVOagQjwOKf45kY - Alexey Starinsky
@AlexeyStarinsky 我复制粘贴了你的示例,忘记修正了5,你需要将其更改为4。你还需要将"xyz"更改为std::string{"xyz"}或使用s字面量,因为"xyz"会用bool值填充变体。请参见https://wandbox.org/permlink/JzRpvP4RqYYWmfyE - Holt

1
template <size_t... I>
Variant GetHelper(const Tuple& val, size_t index, std::index_sequence<I...>)
{
    Variant value;
    int temp[] = {
        ([&]
        {
            if (index == I)
                value = std::get<I>(val);
        }(), 0)... };
    return value;
}

Variant Get(const Tuple& val, size_t index)
{
    return GetHelper(val, index, std::make_index_sequence<std::tuple_size_v<Tuple>>{});
}

template <size_t... I>
void SetHelper(Tuple& val, size_t index, Variant elem_v, std::index_sequence<I...>)
{
    int temp[] = {
        ([&]
        {
            using type = std::tuple_element_t<I, Tuple>;
            if (index == I)
                std::get<I>(val) = std::get<type>(elem_v);
        }(), 0)... };
}

void Set(Tuple& val, size_t index, Variant elem_v)
{
    SetHelper(val, index, elem_v, std::make_index_sequence<std::tuple_size_v<Tuple>>{});
}

解释:

使用 std::index_sequence 通过编译时常量索引 I 访问每个元组元素。为每个索引创建一个 lambda,如果索引匹配,则执行所需的操作并立即调用它(注意在 lambda 后面加上 ())。使用语法 int temp[] = { (some_void_func(), 0)... } 实际调用每个 lambda(不能直接使用展开语法 ... 在 void 函数上,因此需要将其赋值给 int 数组进行这一技巧)。

或者,您可以让您的 lambda 返回一些虚拟的 int。然后,您可以通过展开直接调用它们。


1
首先,一些机制。 alternative 是整数常量的变体,这些常量是无状态的。我们可以使用 visit 来将有界运行时值转换为编译时值。
template<class T, T...Is>
using alternative = std::variant< std::integral_constant<T, Is>... >;

template<class List>
struct alternative_from_sequence;
template<class T, T...Is>
struct alternative_from_sequence< std::integer_sequence<T,Is...> > {
  using type=alternative<T, Is...>;
};
template<class T, T Max>
using make_alternative = typename alternative_from_sequence<
  std::make_integer_sequence<T, Max>
>::type;

template<class T, T Max, T Cur=Max-1>
make_alternative<T, Max> get_alternative( T value, std::integral_constant< T, Max > ={} ) {
  if(Cur == 0 || value == Cur) {
    return std::integral_constant<T, Cur>{};
  }
  if constexpr (Cur > 0)
  {
    return get_alternative<T, Max, Cur-1>( value );
  }
}
template<class...Ts>
auto get_alternative( std::variant<Ts...> const& v ) {
    return get_alternative<std::size_t, sizeof...(Ts) >( v.index() );
}

现在是你实际的问题。这个Get需要你传递你的Variant类型:
template<class Variant, class...Ts>
Variant Get(std::tuple<Ts...> const & val, size_t index) {
  auto which = get_alternative<std::size_t, sizeof...(Ts)>( index );
  return std::visit( [&val]( auto i )->Variant {
    return std::get<i>(val);
  }, which );
}

你的Set函数似乎有毒,如果类型不匹配,则没有实际的解决方法。我将添加一个返回值来说明赋值是否失败:

您的 Set 函数似乎存在问题;如果类型不匹配,就没有实际解决方案。我将添加一个返回值,说明是否赋值失败:

template<class...Ts, class...Vs>
bool Set(
  std::tuple<Ts...> & val,
  std::size_t index,
  const std::variant<Vs...>& elem_v
) {
  auto tuple_which = get_alternative<std::size_t, sizeof...(Ts)>( index );
  auto variant_which = get_alternative( elem_v );
  return std::visit( [&val, &elem_v](auto tuple_i, auto variant_i) {
    using variant_type = std::variant_alternative_t<variant_i, std::variant<Vs...>>;
    using tuple_type = std::tuple_element_t< tuple_i, std::tuple<Ts...> >;

    if constexpr (!std::is_assignable<tuple_type&, variant_type const&>{}) {
      return false;
    } else {
      std::get<tuple_i>(val) = std::get<variant_i>(elem_v);
      return true;
    }
  }, tuple_which, variant_which );
}

这个Set如果类型不可分配,则返回false。

实时示例


变量知道类型的索引,但不知道元组中元素的索引。 - Alexey Starinsky
@AlexeyStarinsky 他们完全对应,是吗?哦,他们不对应。好的。 - Yakk - Adam Nevraumont
不,元组中可以有多个相同类型的元素,例如。 - Alexey Starinsky
@AlexeyStarinsky 如果你的代码请求将 std::string 复制到 int 上会发生什么?(或者应该发生什么) - Yakk - Adam Nevraumont
如果你询问的是像 Set(t, 1, Variant("xyz")); 这样的内容,那么它是类型不匹配的(应该会抛出异常或者需要进行转换)。 - Alexey Starinsky
@AlexeyStarinsky 我让 Set 在那种情况下返回 false。在 C++ 中没有从字符串到整数的转换。 - Yakk - Adam Nevraumont

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接