const 元组到 const 元组?

const tuple to tuple of consts?

如果我有一个元组成员变量,其中元组中的类型是 class 模板的参数包,我可以使用静态成员函数将函数应用于元组中的每个对象,如下所示:

template<size_t I = 0, typename F, typename... Tp>
static void apply_to_foos(std::tuple<Tp...>& t, F func)  {
    auto& foo = std::get<I>(t);
    func(foo);
    if constexpr (I + 1 != sizeof...(Tp))
        apply_to_foos<I + 1>(t, func);
}

例如,如果 foos_ 是元组成员变量,我可以通过以下方式在 foos 上实现一个成员函数:

void frobnicate() {
   apply_to_foos( foos_, [](auto& f){f.frob();} );
}

等但是,如果 frobnicate 是 const,我 运行 会遇到 foos_ 现在将是 const 的问题,而 apply_to_foos 想要一个非常量元组。因此,例如这将不起作用(如果 foos 是某种容器)

size_t size() const {
   size_t sz = 0;
   apply_to_foos( foos_, [&sz](auto& f){ sz += f.size();} );
   return sz;
}

我可以实现 apply_to_foos 的重载,将 const std::tuple<Tp...>&、const_cast 带到非常量元组并调用原始重载,但是,好吧,那么我只是抛弃常量。

令人讨厌的是 apply_to_foos 唯一关心常量的部分是签名。只要尊重 const-ness,其他一切都将使用 const 值,例如lambda 将需要引用 const 值等。如果我可以从 const 元组转换为 const 元组,那么我可以像这样实现 const 重载:

template<size_t I = 0, typename F, typename... Tp>
static void apply_to_foos(const std::tuple<Tp...>& t, F func)  {
    auto& tup_of_consts = const_tuple_to_tuple_of_consts(t);
    apply_to_foos(tup_of_consts, func);
}

size() 成员函数会自然地工作......我觉得一定有更简单的方法来处理这个问题?

如果不复制包含的项目,您不能将 const std::tuple<T1, T2> 转换为 std::tuple<const T1, const T2>

尽管如此,您有两种选择:

1。推断 t 的类型并完全回避问题:

template<size_t I = 0, typename F, typename T>
static void apply_to_foos(T& t, F func)  {
    auto& foo = std::get<I>(t);
    func(foo);
    if constexpr (I + 1 != std::tuple_size<T>::value)
        apply_to_foos<I + 1>(t, func);
}

Live Demo

推导出t的类型,两种情况都可以。如果传递了一个 const 元组,那么 T 将被推断为 const std::tuple<...> ,从而使 t 成为 const std::tuple<...>& 的类型,如果传递了一个非 const 元组那么 T 将推导为 std::tuple<...>t 的类型将是 std::tuple<...>&。唯一需要的其他更改是使用 std::tuple_size<T>::value 代替 sizeof...(Tp)。请注意,这也允许 apply_to_foos 与其他类型一起使用,例如 std::arraystd::pair.

您可能还想应用完美转发以允许 apply_to_foos 使用右值并保留元组元素的值类别。例如:

template<size_t I = 0, typename F, typename T>
static void apply_to_foos(T&& t, F func)  {
    func(std::get<I>(std::forward<T>(t)));
    if constexpr (I + 1 != std::tuple_size<std::remove_reference_t<T>>::value)
        apply_to_foos<I + 1>(std::forward<T>(t), func);
}

Live Demo

2。创建一个包含对原始元组内容的引用的新元组

创建 apply_to_foos 的第二个重载,它接受对 const 元组的引用,并创建一个 const 对原始元组元素的引用的临时元组:

template<typename F, typename... Tp>
static void apply_to_foos(const std::tuple<Tp...>& t, F func) {
    std::tuple<std::add_const_t<Tp>&...> tuple_of_const = t;
    apply_to_foos(tuple_of_const, func);
}

Live Demo

这可以正常工作,但它在保存值类别方面存在问题。元组右值的元素将作为 const 左值传递给回调函数,例如不能轻易移动。

认为 这是 const_cast 假装你的可变参数是常量的好用法。可能是UB,我不是专家

基本上,将逻辑放在 const 版本中以保证 const 的正确性。然后非 const 版本在从引用中删除 const 后调用 const 版本。

首先,从引用中删除常量(可能是更好的方法):

// badly named but turns `const int &` -> `int &`
template <typename T>
struct remove_const_ref {
  using type = std::add_lvalue_reference_t<
      std::remove_const_t<std::remove_reference_t<T>>>;
};
template <typename T>
using remove_const_ref_t = typename remove_const_ref<T>::type;

然后,常量版本:

template <size_t I = 0, typename F, typename... Tp>
static void apply_to_foos(const std::tuple<Tp...> &t, F func) {
  auto &foo = std::get<I>(t);
  func(foo);
  if constexpr (I + 1 != sizeof...(Tp)) apply_to_foos<I + 1>(t, func);
}

以及非常量版本,它只是强制转换和调用:

template <size_t I = 0, typename F, typename... Tp>
static void apply_to_foos(std::tuple<Tp...> &t, F func) {
  apply_to_foos(std::as_const(t), [&func](const auto &v) {
    func(const_cast<remove_const_ref_t<decltype(v)>>(v));
  });
}

示例:

int main() {
  std::tuple thingMut = {1, 2, 3, 4};
  const std::tuple thingConst = {1, 2, 3, 4};

  apply_to_foos(thingMut, [](auto &v) {
    v++;
    std::cout << "mut frob: " << v << std::endl;
  });
  apply_to_foos(thingConst, [](auto &v) {
    // v++;  error: you cannot assign to a variable that is const
    std::cout << "const frob: " << v << std::endl;
  });
}