Skip to content

Commit c924cfb

Browse files
committed
[generator] complete lib, add tests
1 parent 5d9ffdd commit c924cfb

File tree

2 files changed

+213
-22
lines changed

2 files changed

+213
-22
lines changed

include/itlib/generator.hpp

Lines changed: 83 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -61,35 +61,33 @@
6161

6262
namespace itlib {
6363

64-
namespace genimpl {
65-
// tempting to include expected here so we could have optional of ref and
66-
// ditch the T& specialization
67-
// ... but we promised to make standalone libs
64+
// why std::optional still doesn't have T& specialization is beyond me
65+
// it's tempting to include expected here so we could have optional of ref and
66+
// ditch our T& specialization, but we promised to make standalone libs
6867

6968
template <typename T>
70-
class val_holder : public std::optional<T> {};
69+
class generator_value : public std::optional<T> {};
7170

7271
template <typename T>
73-
class val_holder<T&> {
74-
T* val = nullptr;
72+
class generator_value <T&> {
73+
T* m_val = nullptr;
7574
public:
76-
void emplace(T& v) noexcept { val = &v; }
77-
void reset() noexcept { val = nullptr; }
78-
T& operator*() noexcept { return *val; }
79-
bool has_value() const noexcept { return val != nullptr; }
75+
void emplace(T& v) noexcept { m_val = &v; }
76+
void reset() noexcept { m_val = nullptr; }
77+
T& operator*() noexcept { return *m_val; }
78+
bool has_value() const noexcept { return m_val != nullptr; }
8079
explicit operator bool() const noexcept { return has_value(); }
8180
};
8281

83-
} // namespace genimpl
84-
8582
template <typename T>
8683
class generator {
8784
public:
85+
// return const ref in case we're generating values, otherwise keep the ref type
8886
using value_ret_t = std::conditional_t<std::is_reference_v<T>, T, const T&>;
8987

90-
class promise_type {
91-
genimpl::val_holder<T> m_val;
92-
public:
88+
struct promise_type {
89+
generator_value<T> m_val;
90+
9391
promise_type() noexcept = default;
9492

9593
~promise_type() = default;
@@ -99,25 +97,89 @@ class generator {
9997
std::suspend_always initial_suspend() noexcept { return {}; }
10098
std::suspend_always final_suspend() noexcept { return {}; }
10199
std::suspend_always yield_value(T value) noexcept { // assume T is noexcept move constructible
102-
m_val = std::move(value);
100+
if constexpr (std::is_reference_v<T>) {
101+
m_val.emplace(value);
102+
}
103+
else {
104+
m_val.emplace(std::move(value));
105+
}
103106
return {};
104107
}
105108
void return_void() noexcept {}
106109
void unhandled_exception() { throw; }
107110

108-
value_ret_t val() const noexcept {
111+
value_ret_t val() & noexcept {
109112
return *m_val;
110113
}
114+
T&& val() && noexcept {
115+
return std::move(*m_val);
116+
}
117+
void clear_value() noexcept {
118+
m_val.reset();
119+
}
111120
};
112121

122+
using handle_t = std::coroutine_handle<promise_type>;
123+
113124
~generator() {
114125
if (m_handle) m_handle.destroy();
115126
}
116127

117-
// std::optional interface
118-
genimpl::val_holder<T> next() {}
128+
// next (optional-based) interface
129+
130+
// NOTE: this won't return true until next() has returned an empty optional at least once
131+
bool done() const noexcept {
132+
return m_handle.done();
133+
}
134+
135+
generator_value<T> next() {
136+
if (done()) return {};
137+
m_handle.promise().clear_value();
138+
m_handle.resume();
139+
return std::move(m_handle.promise().m_val);
140+
}
141+
142+
// iterator-like/range-for interface
143+
144+
// emphasize that this is not a real iterator
145+
class pseudo_iterator {
146+
handle_t m_handle;
147+
public:
148+
using value_type = std::decay_t<T>;
149+
using reference = value_ret_t;
150+
151+
pseudo_iterator() noexcept = default;
152+
explicit pseudo_iterator(handle_t handle) noexcept : m_handle(handle) {}
153+
154+
reference operator*() const noexcept {
155+
return m_handle.promise().val();
156+
}
157+
158+
pseudo_iterator& operator++() {
159+
m_handle.promise().clear_value();
160+
m_handle.resume();
161+
return *this;
162+
}
163+
164+
struct end_t {};
165+
166+
// we're not really an iterator, but we can pretend to be one
167+
friend bool operator==(const pseudo_iterator& i, end_t) noexcept { return i.m_handle.done(); }
168+
friend bool operator==(end_t, const pseudo_iterator& i) noexcept { return i.m_handle.done(); }
169+
friend bool operator!=(const pseudo_iterator& i, end_t) noexcept { return !i.m_handle.done(); }
170+
friend bool operator!=(end_t, const pseudo_iterator& i) noexcept { return !i.m_handle.done(); }
171+
};
172+
173+
pseudo_iterator begin() {
174+
m_handle.resume();
175+
return pseudo_iterator{m_handle};
176+
}
177+
178+
pseudo_iterator::end_t end() {
179+
return {};
180+
}
181+
119182
private:
120-
using handle_t = std::coroutine_handle<promise_type>;
121183
handle_t m_handle;
122184
explicit generator(handle_t handle) noexcept : m_handle(handle) {}
123185
};

test/t-generator-20.cpp

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,133 @@
22
// SPDX-License-Identifier: MIT
33
//
44
#include <itlib/generator.hpp>
5-
#include <doctest/doctest.h>
5+
6+
#include <doctest/doctest.h>
7+
#include <doctest/util/lifetime_counter.hpp>
8+
9+
#include <stdexcept>
10+
#include <vector>
11+
#include <span>
12+
13+
itlib::generator<int> range(int begin, int end) {
14+
for (int i = begin; i < end; ++i) {
15+
if (i == 103) throw std::runtime_error("test exception");
16+
co_yield i;
17+
}
18+
}
19+
20+
TEST_CASE("simple") {
21+
int i = 50;
22+
23+
// range for
24+
for (int x : range(i, i+10)) {
25+
CHECK(x == i);
26+
++i;
27+
}
28+
CHECK(i == 60);
29+
30+
// next
31+
auto r = range(1, 5);
32+
CHECK(*r.next() == 1);
33+
CHECK_FALSE(r.done());
34+
CHECK(*r.next() == 2);
35+
CHECK(r.next().value() == 3);
36+
CHECK(r.next().value() == 4);
37+
CHECK_FALSE(r.next().has_value());
38+
CHECK_FALSE(r.next().has_value()); // check once again to make sure it's safe
39+
CHECK(r.done());
40+
41+
// exceptions
42+
43+
i = 0;
44+
CHECK_THROWS_WITH_AS(
45+
[&]() {
46+
for (int x : range(100, 105)) {
47+
i = x;
48+
}
49+
}(),
50+
"test exception",
51+
std::runtime_error
52+
);
53+
CHECK(i == 102);
54+
55+
auto tr = range(101, 105);
56+
CHECK_NOTHROW(tr.next());
57+
CHECK_NOTHROW(tr.next());
58+
CHECK_THROWS_WITH_AS(tr.next(), "test exception", std::runtime_error);
59+
}
60+
61+
template <typename T>
62+
itlib::generator<T&> ref_gen(std::span<T> vals) {
63+
for (T& v : vals) {
64+
co_yield v;
65+
}
66+
}
67+
68+
TEST_CASE("ref") {
69+
std::vector<int> ints = {1, 2, 3, 4, 5};
70+
auto g = ref_gen(std::span(ints));
71+
for (int& i : g) {
72+
i += 10;
73+
}
74+
CHECK(ints == std::vector<int>{11, 12, 13, 14, 15});
75+
76+
auto cg = ref_gen(std::span<const int>(ints));
77+
const int& a = *cg.next();
78+
const int& b = *cg.next();
79+
for (const int& i : cg) {
80+
CHECK(i > 12);
81+
CHECK(i < 16);
82+
}
83+
CHECK(cg.done());
84+
CHECK(a == 11);
85+
CHECK(&a == ints.data());
86+
CHECK(b == 12);
87+
CHECK(&b == ints.data() + 1);
88+
}
89+
90+
struct value : doctest::util::lifetime_counter<value>
91+
{
92+
value() = default;
93+
explicit value(int i) : val(i) {}
94+
int val = 0;
95+
};
96+
97+
itlib::generator<value> value_range(int begin, int end) {
98+
for (int i = begin; i < end; ++i) {
99+
co_yield value(i);
100+
}
101+
}
102+
103+
TEST_CASE("lifetime") {
104+
doctest::util::lifetime_counter_sentry lcsentry(value::root_lifetime_stats());
105+
106+
int i = 0;
107+
{
108+
value::lifetime_stats ls;
109+
110+
auto r = value_range(0, 5);
111+
for (value v : r) {
112+
CHECK(v.val == i);
113+
++i;
114+
}
115+
CHECK(i == 5);
116+
117+
CHECK(ls.living == 0);
118+
CHECK(ls.copies == 5);
119+
CHECK(ls.m_ctr == 5);
120+
}
121+
122+
{
123+
value::lifetime_stats ls;
124+
125+
auto r = value_range(0, 3);
126+
auto v1 = r.next();
127+
auto v2 = r.next();
128+
auto v3 = r.next();
129+
auto vend = r.next();
130+
CHECK(ls.living == 3);
131+
CHECK(ls.copies == 0);
132+
CHECK(ls.m_ctr == 6);
133+
}
134+
}

0 commit comments

Comments
 (0)