查看原文
其他

如何更优雅地迭代 std::tuple ?

CPP开发者 2023-07-27

The following article is from CPP编程客 Author 里缪

std::tuple属于异构数据类型结构,可以存储不同类型的数据。
目前,Expansion Statements还未进标准,在编译期迭代std::tuple仍旧是个比较麻烦的事情。不过,随着C++元编程的发展,借助许多其他特性,暂时也能够稍微优雅地解决这个问题。
本篇便来介绍一下C++11-2?(大概26)当中解决该问题的不同方式。

01 C++11:原始迭代法

std::tuple默认支持的访问方式为「索引式访问」,通过这种方式可以逐个访问结构中的元素。简单的例子:

1int main() {
2    std::tuple<intdoubleconst char*> tp {55.19"May 19"};
3
4    // 索引式访问
5    std::get<0>(tp) = 6;
6    std::cout << std::get<0>(tp) << '\n';
7    std::cout << std::get<1>(tp) << '\n';
8    std::cout << std::get<2>(tp) << '\n';
9}

此时若要遍历std::tuple中的元素,重复工作很多,比较麻烦。

一种解决方法是借助可变参数模板,再通过递归来展开参数包,实现如下:

1template<typename Tuple>
2void print_tuple(const Tuple& tp) {
3}
4
5template<typename Tuple, std::size_t I, std::size_t...Is>
6void print_tuple(const Tuple& tp) {
7    std::cout << std::get<I>(tp) << '\n';
8    print_tuple<Tuple, Is...>(tp);
9}

现在,就可以使用该实现打印std::tuple,如下:

print_tuple<decltype(tp), 012>(tp);

另一种解决方法是在一的基础上,借助逗号表达式和初始化列表来展开参数包,代码如下:

template<typename Tuple, std::size_t... Is>
void print_tuple(const Tuple& tp) {
    std::initializer_list<int>{([&tp]() {
        std::cout << std::get<Is>(tp) << '\n';
    }(), 0)...};
}

02 C++14:std::index_sequence迭代法

到了C++14,标准中增加了std::index_sequence用以表示索引序列,通过std::make_index_sequence可以自动生成索引序列。

于是,借助这两个工具便可以替代C++11中的手动编写索引序列,使用起来更加方便。实现如下:

template<typename Tuple, std::size_t... Is>
void print_tuple(const Tuple& tp, std::index_sequence<Is...>) {
    std::initializer_list<int>{([&tp]() {
        std::cout << std::get<Is>(tp) << '\n';
    }(), 0)...};
}

现在,可以这样使用:

std::tuple<intdoubleconst char*> tp {55.19"May 19"};
print_tuple(tp, std::make_index_sequence<3>{});

因为std::tuple的元素个数能够通过std::tuple_size获取,所以上述代码还可以进一步优化。代码如下:

template<typename Tuple, std::size_t... Is>
void print_tuple_impl(const Tuple& tp, std::index_sequence<Is...>) {
    std::initializer_list<int>{([&tp]() {
        std::cout << std::get<Is>(tp) << '\n';
    }(), 0)...};
}

template<typename Tuple, std::size_t TupleSize = std::tuple_size<Tuple>::value>
void print_tuple(const Tuple& tp) {
    print_tuple_impl(tp, std::make_index_sequence<TupleSize>{});
}


int main() {
    std::tuple<intdoubleconst char*> tp {55.19"May 19"};

    print_tuple(tp);
}

现在,虽说实现起来依然稍显麻烦,但使用起来已经比较方便了。

03 C++17:std::apply迭代法

到了C++17,则更进一步。

首先,拥有了Fold Expressions,展开参数包能够更加方便。

其次,增加了CTAD,使得std::tuple的用法更加简洁,不用指定模板参数,可以自行推导。

最后,新添了std::apply,该函数得以隐藏索引序列的具体使用,让代码更加简洁。

因此,现在可以更加优雅地解决上述问题,实现如下:

template<typename Tuple>
void print_tuple(const Tuple& tp) {
    std::cout << "(";
    if constexpr (std::tuple_size_v<Tuple> > 0)
        std::apply(
            [](const auto& head, const auto&... tails) 
{
                std::cout << head;
                ((std::cout << ", " << tails), ...);
            }, tp
        );
    std::cout << ")";
}

在此基础上,还可以重载operator <<,使得可以直接通过输出流打印std::tuple

实现代码如下:

template<typename Tuple>
std::ostream& print_tuple(std::ostream& os, const Tuple& tp) {
    os << "(";
    if constexpr (std::tuple_size_v<Tuple> > 0)
        std::apply(
            [&os](const auto& head, const auto&... tails) 
{
                os << head;
                ((os << ", " << tails), ...);
            }, tp
        );
    std::cout << ")";
    return os;
}

template<typename Tuple>
std::ostream& operator <<(std::ostream& os, const Tuple& tp) {
    return print_tuple(os, tp);
}

现在,使用起来就非常方便了,测试前述例子,将会输出:

std::tuple tp {55.19"May 19"};
std::cout << tp; // output: (5, 5.19, May 19) 

此外,欲编写一个针对std::tuplefor_each函数亦非难事,实现如下:

template<typename Tuple, typename Fn>
void for_each_tuple(Tuple&& tp, Fn&& fn) {
    std::apply(
        [&fn](auto&&... args) {
            (fn(std::forward<decltype(args)>(args)), ...);
        }, 
        std::forward<Tuple>(tp)
    );
}

int main() {
    std::tuple tp {55.19"May 19"};
    for_each_tuple(tp, [](auto&& v) {
        std::cout << v << " ";
    });
}

这个实现代码优雅,且威力强大,可以非常方便地操纵std::tuple的每一个元素。

04 C++20:使用Template Lambda进一步优化

C++17实现版本的for_each_tuple采用了generic lambda,在C++20可以使用template lambda消除其中的类型推导。

代码如下:

template<typename Tuple, typename Fn>
void for_each_tuple(Tuple&& tp, Fn&& fn) {
    std::apply(
        [&fn]<typename... T>(T&&... args) {
            (fn(std::forward<T>(args)), ...);
        }, 
        std::forward<Tuple>(tp)
    );
}

05 C++2?:Expansion Statements

上述各种实现,都颇有技巧性。

迭代这种异构数据类型结构的组件,提案中引入了Expansion Statements。可以直接这样编写代码:

auto tup = std::make_tuple(0'a'3.14);
template for (auto& elem : tup)
    std::cout << elem << '\n'
;

同样,这也是第三阶段元编程的重要工具之一,配合标准反射库,迭代复数式元函数返回的结果集。具体内容可以参考「反射主题」第一章和第四章。

此外,还有一个提案提出了扩展参数包的概念,根据该提案最终可以写出如下代码:

auto [...pack] = std::make_tuple(0'a'3.14);
((std::cout << pack << '\n'), ...);

这里的pack便是一个「结构化绑定参数包」,可以在任何展开函数参数包的地方使用。

06 总结

由一个迭代std::tuple的需求,就能带领大家领略C++11到未来的发展历程,发现需求痛点,以及新特性是如何解决这些痛点的。

同时,这也表现了C++元编程的发展历程,由晦涩的模板代码,到结构越发清晰的代码,再到第三阶段元编程的未来新特性,实现该需求越来越简单。

此外,大家可以发现,在第三阶段元编程的特性到来之前,C++17是可以相对优雅地实现该需求的最低C++版本。

希望大家能够理清本篇内容。如果你还有其他迷惑点,欢迎在评论区写下你的问题。

也欢迎给文章点赞、在看、分享。

- EOF -


加主页君微信,不仅C/C++技能+1

主页君日常还会在个人微信分享C/C++开发学习资源技术文章精选,不定期分享一些有意思的活动岗位内推以及如何用技术做业余项目

加个微信,打开一扇窗


推荐阅读  点击标题可跳转

1、C++ 反射:通识

2、C++ 反射 第四章 标准

3、C++ 反射:反射信息的自动生成!


关注『CPP开发者』

看精选C/C++技术文章

点赞和在看就是最大的支持❤️

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存