From aaaf212dcacfd96b26eb3efd8e529802a4a01c57 Mon Sep 17 00:00:00 2001 From: Kp Date: Wed, 26 Feb 2020 05:07:34 +0000 Subject: [PATCH] Add unit test for zip iterator --- SConstruct | 3 ++ common/main/d_zip.h | 86 ++++++++++++++++++++++++++++++----------- common/unittest/zip.cpp | 67 ++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 23 deletions(-) create mode 100644 common/unittest/zip.cpp diff --git a/SConstruct b/SConstruct index 19893adce..d43468e53 100644 --- a/SConstruct +++ b/SConstruct @@ -4404,6 +4404,9 @@ class DXXArchive(DXXCommon): RuntimeTest('test-xrange', ( 'common/unittest/xrange.cpp', )), + RuntimeTest('test-zip', ( + 'common/unittest/zip.cpp', + )), ) del RuntimeTest diff --git a/common/main/d_zip.h b/common/main/d_zip.h index b4930e6d9..d2d0bbb10 100644 --- a/common/main/d_zip.h +++ b/common/main/d_zip.h @@ -6,38 +6,76 @@ */ #pragma once +#include #include #include #include "dxxsconf.h" #include "compiler-integer_sequence.h" #include "ephemeral_range.h" -/* This iterator terminates when the first zipped range terminates. The caller - * is responsible for ensuring that the first zipped range is not longer than - * any other zipped range. +namespace d_zip { + +namespace detail { + +template +void discard_arguments(T &&...) +{ +} + +template +void increment_iterator(std::tuple &iterator, std::index_sequence) +{ + /* Order of evaluation is irrelevant, so pass the results to a + * discarding function. This permits the compiler to evaluate the + * expression elements in any order. + */ + discard_arguments(++(std::get(iterator))...); +} + +template +auto dereference_iterator(const std::tuple &iterator, std::index_sequence) +{ + /* std::make_tuple is not appropriate here, because the result of + * dereferencing the iterator may be a reference, and the resulting + * tuple should store the reference, not the underlying object. + * Calling std::make_tuple would decay such references into the + * underlying type. + * + * std::tie is not appropriate here, because it captures all + * arguments as `T &`, so it fails to compile if the result of + * dereferencing the iterator is not a reference. + */ + return std::tuple< + decltype(*(std::get(iterator)))... + >(*(std::get(iterator))...); +} + +} + +} + +/* This iterator terminates when the first zipped range terminates. The + * caller is responsible for ensuring that use of the zip_iterator does + * not increment past the end of any zipped range. This can be done by + * ensuring that the first zipped range is not longer than any other + * zipped range, or by ensuring that external logic stops the traversal + * before the zip_iterator increments past the end. + * + * There is no initialization time check that the below loop would be + * safe, since a check external to the zip_iterator could stop before + * undefined behaviour occurs. + + for (auto i = zip_range.begin(), e = zip_range.end(); i != e; ++i) + { + if (condition()) + break; + } + */ template class zip_iterator : std::tuple { using base_type = std::tuple; - template - void discard_arguments(T &&...) - { - } - template - void increment(std::index_sequence) - { - /* Order of evaluation is irrelevant, so pass the results to - * a discarding function. This permits the compiler to - * evaluate the expression elements in any order. - */ - discard_arguments(++(std::get(*this))...); - } - template - auto dereference(std::index_sequence) const - { - return std::tie(*(std::get(*this))...); - } protected: /* Prior to C++17, range-based for insisted on the same type for * `begin` and `end`, so method `end_internal` must return a full iterator, @@ -65,11 +103,11 @@ public: using base_type::base_type; auto operator*() const { - return dereference(index_type()); + return d_zip::detail::dereference_iterator(*this, index_type()); } zip_iterator &operator++() { - increment(index_type()); + d_zip::detail::increment_iterator(*this, index_type()); return *this; } bool operator!=(const zip_iterator &i) const @@ -101,6 +139,8 @@ public: template static auto zip(range0 &&r0, rangeN &&... rN) { + using std::begin; + using std::end; using R = zipped_range; static_assert((!any_ephemeral_range::value), "cannot zip storage of ephemeral ranges"); return R(typename R::iterator(begin(r0), begin(rN)...), end(r0)); diff --git a/common/unittest/zip.cpp b/common/unittest/zip.cpp new file mode 100644 index 000000000..a47ce9e88 --- /dev/null +++ b/common/unittest/zip.cpp @@ -0,0 +1,67 @@ +#include "d_range.h" +#include "d_zip.h" +#include +#include + +#define BOOST_TEST_DYN_LINK +#define BOOST_TEST_MODULE Rebirth zip +#include + +/* Test that a zipped range is empty when the component range is empty. + */ +BOOST_AUTO_TEST_CASE(zip_empty) +{ + bool empty = true; + std::array a; + for (auto &&v : zip(a)) + { + (void)v; + empty = false; + } + BOOST_TEST(empty); +} + +/* Test that a zipped range is the length of the first sequence. + * + * Note that there is no test for when the first sequence is longer than + * the later ones, since such a test would increment some iterators past + * their end. + */ +BOOST_AUTO_TEST_CASE(zip_length) +{ + unsigned count = 0; + std::array a; + short b[4]; + for (auto &&v : zip(a, b)) + { + (void)v; + ++ count; + } + BOOST_TEST(count == a.size()); +} + +/* Test that a zipped value references the underlying storage. + */ +BOOST_AUTO_TEST_CASE(zip_passthrough_modifications) +{ + std::array a{{1}}; + short b[1]{2}; + for (auto &&v : zip(a, b)) + { + ++ std::get<0>(v); + ++ std::get<1>(v); + } + BOOST_TEST(a[0] == 2); + BOOST_TEST(b[0] == 3); +} + +/* Test that a zipped range can zip an xrange, and produce the correct + * underlying value. + */ +BOOST_AUTO_TEST_CASE(zip_xrange) +{ + for (auto &&v : zip(xrange(1u))) + { + BOOST_TEST(std::get<0>(v) == 0); + } +}