diff --git a/.gitignore b/.gitignore index 259148f..6db9208 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,5 @@ *.exe *.out *.app +.vscode/ +build/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..460003e --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,20 @@ +# Copyright Lewis Baker, Corentin Jabot +# Licensed under Boost Software License 1.0 + +cmake_minimum_required(VERSION 3.12) + +project(stdgenerator LANGUAGES CXX + VERSION 0.1) + +add_library(stdgenerator INTERFACE) + +target_include_directories(stdgenerator + INTERFACE + $) + +target_compile_features(stdgenerator INTERFACE cxx_std_20) + +enable_testing() +include(CTest) + +add_subdirectory("tests") diff --git a/include/generator b/include/generator new file mode 100644 index 0000000..20a95f4 --- /dev/null +++ b/include/generator @@ -0,0 +1,802 @@ +#ifndef __STD_GENERATOR_INCLUDED +#define __STD_GENERATOR_INCLUDED +/////////////////////////////////////////////////////////////////////////////// +// Reference implementation of std::generator proposal P2168. +// +// See https://wg21.link/P2168 for details. +// +/////////////////////////////////////////////////////////////////////////////// +// Copyright Lewis Baker, Corentin Jabot +// +// Use, modification and distribution is subject to the Boost Software License, +// Version 1.0. +// (See accompanying file LICENSE or http://www.boost.org/LICENSE_1_0.txt) +/////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#if __has_include() +#include +#else +// Fallback for older experimental implementations of coroutines. +#include +namespace std { +using std::experimental::coroutine_handle; +using std::experimental::coroutine_traits; +using std::experimental::noop_coroutine; +using std::experimental::suspend_always; +using std::experimental::suspend_never; +} // namespace std +#endif + +#include +#include +#include +#include +#include +#include +#include + +#if __has_include() +# include +#else + +// Placeholder implementation of the bits we need from header +// when we don't have the header (e.g. Clang 12 and earlier). +namespace std { + +// Don't create naming conflicts with recent libc++ which defines std::iter_reference_t +// in but doesn't yet provide a header. +template +using __iter_reference_t = decltype(*std::declval<_T&>()); + +template +using iter_value_t = + typename std::iterator_traits>::value_type; + +namespace ranges { + +namespace __begin { +void begin(); + +struct _fn { + template + requires requires(_Range& __r) { + __r.begin(); + } + auto operator()(_Range&& __r) const + noexcept(noexcept(__r.begin())) + -> decltype(__r.begin()) { + return __r.begin(); + } + + template + requires + (!requires(_Range& __r) { __r.begin(); }) && + requires(_Range& __r) { begin(__r); } + auto operator()(_Range&& __r) const + noexcept(noexcept(begin(__r))) + -> decltype(begin(__r)) { + return begin(__r); + } +}; + +} // namespace __begin + +inline namespace __begin_cpo { +inline constexpr __begin::_fn begin = {}; +} + +namespace __end { +void end(); + +struct _fn { + template + requires requires(_Range& __r) { __r.end(); } + auto operator()(_Range&& __r) const + noexcept(noexcept(__r.end())) + -> decltype(__r.end()) { + return __r.end(); + } + + template + requires + (!requires(_Range& __r) { __r.end(); }) && + requires(_Range& __r) { end(__r); } + auto operator()(_Range&& __r) const + noexcept(noexcept(end(__r))) + -> decltype(end(__r)) { + return end(__r); + } +}; +} // namespace __end + +inline namespace _end_cpo { +inline constexpr __end::_fn end = {}; +} + +template +using iterator_t = decltype(begin(std::declval<_Range>())); + +template +using sentinel_t = decltype(end(std::declval<_Range>())); + +template +using range_reference_t = __iter_reference_t>; + +template +using range_value_t = iter_value_t>; + +template +concept range = requires(_T& __t) { + ranges::begin(__t); + ranges::end(__t); +}; + +} // namespace ranges +} // namespace std + +#endif // !__has_include() + + +/////////////////////////////////////////////////////////////////////////////// +// Begin implementation + +// Define NESTED_GENERATOR=0 before including this file to disable nested +// generator support. e.g. to compare performance between nested and non-nested +// implementations. +#ifndef NESTED_GENERATOR +# define NESTED_GENERATOR 1 +#endif + +namespace std { + +template +class __manual_lifetime { + public: + __manual_lifetime() noexcept {} + ~__manual_lifetime() {} + + template + _T& construct(_Args&&... __args) noexcept(std::is_nothrow_constructible_v<_T, _Args...>) { + return *::new (static_cast(std::addressof(__value_))) _T((_Args&&)__args...); + } + + void destruct() noexcept(std::is_nothrow_destructible_v<_T>) { + __value_.~_T(); + } + + _T& get() & noexcept { + return __value_; + } + _T&& get() && noexcept { + return static_cast<_T&&>(__value_); + } + const _T& get() const & noexcept { + return __value_; + } + const _T&& get() const && noexcept { + return static_cast(__value_); + } + + private: + union { + std::remove_const_t<_T> __value_; + }; +}; + +template +class __manual_lifetime<_T&> { + public: + __manual_lifetime() noexcept : __value_(nullptr) {} + ~__manual_lifetime() {} + + _T& construct(_T& __value) noexcept { + __value_ = std::addressof(__value); + return __value; + } + + void destruct() noexcept {} + + _T& get() const noexcept { + return *__value_; + } + + private: + _T* __value_; +}; + +template +class __manual_lifetime<_T&&> { + public: + __manual_lifetime() noexcept : __value_(nullptr) {} + ~__manual_lifetime() {} + + _T&& construct(_T&& __value) noexcept { + __value_ = std::addressof(__value); + return static_cast<_T&&>(__value); + } + + void destruct() noexcept {} + + _T&& get() const noexcept { + return static_cast<_T&&>(*__value_); + } + + private: + _T* __value_; +}; + +namespace ranges { + +template +struct elements_of { + explicit constexpr elements_of(_Rng&& __rng) noexcept + : __range_(static_cast<_Rng&&>(__rng)) + {} + + constexpr elements_of(elements_of&&) noexcept = default; + + constexpr elements_of(const elements_of &) = delete; + constexpr elements_of &operator=(const elements_of &) = delete; + constexpr elements_of &operator=(elements_of &&) = delete; + + constexpr _Rng&& get() && noexcept { + return static_cast<_Rng&&>(__range_); + } + +private: + _Rng &&__range_; // \expos +}; + +template +elements_of(_Rng &&) -> elements_of<_Rng>; + +} // namespace ranges + +template +static constexpr bool __allocator_needs_to_be_stored = + !std::allocator_traits<_Alloc>::is_always_equal::value || + !std::is_default_constructible_v<_Alloc>; + +// Round s up to next multiple of a. +constexpr size_t __aligned_allocation_size(size_t s, size_t a) { + return (s + a - 1) & ~(a - 1); +} + +struct use_allocator_arg {}; + +template , + typename _Allocator = use_allocator_arg> +class generator; + +template +class __promise_base_alloc { + static constexpr std::size_t __offset_of_allocator(std::size_t __frameSize) noexcept { + return __aligned_allocation_size(__frameSize, alignof(_Alloc)); + } + + static constexpr std::size_t __padded_frame_size(std::size_t __frameSize) noexcept { + return __offset_of_allocator(__frameSize) + sizeof(_Alloc); + } + + static _Alloc& __get_allocator(void* __frame, std::size_t __frameSize) noexcept { + return *reinterpret_cast<_Alloc*>( + static_cast(__frame) + __offset_of_allocator(__frameSize)); + } + +public: + template + static void* operator new(std::size_t __frameSize, std::allocator_arg_t, _Alloc __alloc, _Args&...) { + void* __frame = __alloc.allocate(__padded_frame_size(__frameSize)); + + // Store allocator at end of the coroutine frame. + // Assuming the allocator's move constructor is non-throwing (a requirement for allocators) + ::new (static_cast(std::addressof(__get_allocator(__frame, __frameSize)))) _Alloc(std::move(__alloc)); + + return __frame; + } + + template + static void* operator new(std::size_t __frameSize, _This&, std::allocator_arg_t, _Alloc __alloc, _Args&...) { + return __promise_base_alloc::operator new(__frameSize, std::allocator_arg, std::move(__alloc)); + } + + static void operator delete(void* __ptr, std::size_t __frameSize) noexcept { + _Alloc& __alloc = __get_allocator(__ptr, __frameSize); + _Alloc __localAlloc(std::move(__alloc)); + __alloc.~Alloc(); + __localAlloc.deallocate(static_cast(__ptr), __padded_frame_size(__frameSize)); + } +}; + +template + requires (!__allocator_needs_to_be_stored<_Alloc>) +class __promise_base_alloc<_Alloc> { +public: + static void* operator new(std::size_t __size) { + _Alloc __alloc; + return __alloc.allocate(__size); + } + + static void operator delete(void* __ptr, std::size_t __size) noexcept { + _Alloc __alloc; + __alloc.deallocate(static_cast(__ptr), __size); + } +}; + +template +struct __generator_promise_base +{ + template + friend class generator; + +#if NESTED_GENERATOR + __generator_promise_base* __root_; + std::coroutine_handle<> __parentOrLeaf_; + std::exception_ptr *__exception_ = nullptr; +#endif + __manual_lifetime<_Ref> __value_; + +#if NESTED_GENERATOR + explicit __generator_promise_base(std::coroutine_handle<> thisCoro) noexcept + : __root_(this) + , __parentOrLeaf_(thisCoro) + {} +#else + __generator_promise_base() noexcept = default; +#endif + + std::suspend_always initial_suspend() noexcept { + return {}; + } + + void return_void() noexcept {} + + void unhandled_exception() { +#if NESTED_GENERATOR + if (__exception_ != nullptr) { + *__exception_ = std::current_exception(); + } +#endif + throw; + } + +#if NESTED_GENERATOR + // Transfers control back to the parent of a nested coroutine + struct __final_awaiter { + bool await_ready() noexcept { + return false; + } + + template + std::coroutine_handle<> + await_suspend(std::coroutine_handle<_Promise> __h) noexcept { + _Promise& __promise = __h.promise(); + __generator_promise_base& __root = *__promise.__root_; + if (&__root != &__promise) { + auto __parent = __promise.__parentOrLeaf_; + __root.__parentOrLeaf_ = __parent; + return __parent; + } + return std::noop_coroutine(); + } + + void await_resume() noexcept {} + }; + + __final_awaiter final_suspend() noexcept { + return {}; + } + + std::suspend_always yield_value(_Ref&& __x) + noexcept(std::is_nothrow_move_constructible_v<_Ref>) { + __root_->__value_.construct((_Ref&&)__x); + return {}; + } + + template + requires + (!std::is_reference_v<_Ref>) && + std::is_convertible_v<_T, _Ref> + std::suspend_always yield_value(_T&& __x) + noexcept(std::is_nothrow_constructible_v<_Ref, _T>) { + __root_->__value_.construct((_T&&)__x); + return {}; + } + + template + struct __yield_sequence_awaiter { + _Gen __gen_; + std::exception_ptr __exception_; + + __yield_sequence_awaiter(_Gen&& __g) noexcept + // Taking ownership of the generator ensures frame are destroyed + // in the reverse order of their creation + : __gen_((_Gen&&)__g) { + } + + bool await_ready() noexcept { + return !__gen_.__get_coro(); + } + + // set the parent, root and exceptions pointer and + // resume the nested + template + std::coroutine_handle<> + await_suspend(std::coroutine_handle<_Promise> __h) noexcept { + __generator_promise_base& __current = __h.promise(); + __generator_promise_base& __nested = *__gen_.__get_promise(); + __generator_promise_base& __root = *__current.__root_; + + __nested.__root_ = __current.__root_; + __nested.__parentOrLeaf_ = __h; + __root.__parentOrLeaf_ = __gen_.__get_coro(); + __nested.__exception_ = &__exception_; + // Immediately resume the nested coroutine (nested generator) + return __gen_.__get_coro(); + } + + void await_resume() { + if (__exception_) { + std::rethrow_exception(std::move(__exception_)); + } + } + }; + + template + __yield_sequence_awaiter> + yield_value(std::ranges::elements_of> __g) noexcept { + return std::move(__g).get(); + } + + template + __yield_sequence_awaiter> yield_value(std::ranges::elements_of<_Rng> __x) { + // TODO: Consider propagating parent coroutine's allocator to child generator + // coroutine here. + return yield_value(std::ranges::elements_of([](_Rng&& __rng) -> generator<_Ref> { + auto __it = std::ranges::begin(__rng); + auto __itEnd = std::ranges::end(__rng); + while (__it != __itEnd) { + co_yield *__it; + ++__it; + } + }(std::move(__x).get()))); + } + + void resume() { + __parentOrLeaf_.resume(); + } + +#else // !NESTED_GENERATOR + std::suspend_always yield_value(_Ref&& __x) noexcept(std::is_nothrow_move_constructible_v<_Ref>) { + __value_.construct((_Ref&&)__x); + return {}; + } + + template + requires (!std::is_reference_v<_Ref>) && std::is_convertible_v<_T, _Ref> + std::suspend_always yield_value(_T&& __x) noexcept(std::is_nothrow_constructible_v<_Ref, _T>) { + __value_.construct((_T&&)__x); + return {}; + } + + std::suspend_always final_suspend() noexcept { + return {}; + } +#endif + + // Disable use of co_await within this coroutine. + void await_transform() = delete; +}; + +template +struct __generator_promise; + +template +struct __generator_promise, _ByteAllocator> final + : public __generator_promise_base<_Ref> + , public __promise_base_alloc<_ByteAllocator> { + __generator_promise() noexcept +#if NESTED_GENERATOR + : __generator_promise_base<_Ref>(std::coroutine_handle<__generator_promise>::from_promise(*this)) +#endif + {} + + generator<_Ref, _Value, _Alloc> get_return_object() noexcept { + return generator<_Ref, _Value, _Alloc>{ + std::coroutine_handle<__generator_promise>::from_promise(*this) + }; + } +}; + +// Type-erased allocator with default allocator behaviour. +template +struct coroutine_traits, _Args...> { + using promise_type = __generator_promise, std::allocator>; +}; + +// Type-erased allocator with std::allocator_arg parameter +template +struct coroutine_traits, allocator_arg_t, _Alloc, _Args...> { +private: + using __byte_allocator = typename std::allocator_traits>::template rebind_alloc; +public: + using promise_type = __generator_promise, __byte_allocator>; +}; + +// Type-erased allocator with std::allocator_arg parameter (non-static member functions) +template +struct coroutine_traits, _This, allocator_arg_t, _Alloc, _Args...> { +private: + using __byte_allocator = typename std::allocator_traits>::template rebind_alloc; +public: + using promise_type = __generator_promise, __byte_allocator>; +}; + +// Generator with specified allocator type +template +struct coroutine_traits, _Args...> { + using __byte_allocator = typename std::allocator_traits>::template rebind_alloc; +public: + using promise_type = __generator_promise, __byte_allocator>; +}; + +template +class generator { + using __byte_allocator = typename std::allocator_traits>::template rebind_alloc; +public: + using promise_type = __generator_promise, __byte_allocator>; + friend promise_type; +private: + using __coroutine_handle = std::coroutine_handle; +public: + + generator() noexcept = default; + + generator(generator&& __other) noexcept + : __coro_(std::exchange(__other.__coro_, {})) + , __started_(std::exchange(__other.__started_, false)) { + } + + ~generator() noexcept { + if (__coro_) { + if (__started_ && !__coro_.done()) { + __coro_.promise().__value_.destruct(); + } + __coro_.destroy(); + } + } + + generator& operator=(generator g) noexcept { + swap(g); + return *this; + } + + void swap(generator& __other) noexcept { + std::swap(__coro_, __other.__coro_); + std::swap(__started_, __other.__started_); + } + + struct sentinel {}; + + class iterator { + public: + using iterator_category = std::input_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = _Value; + using reference = _Ref; + using pointer = std::add_pointer_t<_Ref>; + + iterator() noexcept = default; + iterator(const iterator &) = delete; + + iterator(iterator&& __other) noexcept + : __coro_(std::exchange(__other.__coro_, {})) { + } + + iterator& operator=(iterator&& __other) { + std::swap(__coro_, __other.__coro_); + return *this; + } + + ~iterator() { + } + + friend bool operator==(const iterator &it, sentinel) noexcept { + return !it.coro_ || it.coro_.done(); + } + + iterator &operator++() { + __coro_.promise().value_.destruct(); +#if NESTED_GENERATOR + __coro_.promise().resume(); +#else + __coro_.resume(); +#endif + return *this; + } + void operator++(int) { + (void)operator++(); + } + + reference operator*() const noexcept { + return static_cast(__coro_.promise().value_.get()); + } + + private: + friend generator; + + explicit iterator(__coroutine_handle __coro) noexcept + : __coro_(__coro) {} + + __coroutine_handle __coro_; + }; + + iterator begin() { + if (__coro_) { + assert(!__started_); + __started_ = true; + __coro_.resume(); + } + return iterator{__coro_}; + } + + sentinel end() noexcept { + return {}; + } + +private: + explicit generator(__coroutine_handle __coro) noexcept + : __coro_(__coro) { + } + +public: // to get around access restrictions for __yield_sequence_awaitable + std::coroutine_handle<> __get_coro() noexcept { return __coro_; } + promise_type* __get_promise() noexcept { return std::addressof(__coro_.promise()); } + +private: + __coroutine_handle __coro_; + bool __started_ = false; +}; + +// Specialisation for type-erased allocator implementation. +template +class generator<_Ref, _Value, use_allocator_arg> { + using __promise_base = __generator_promise_base<_Ref>; +public: + + generator() noexcept = default; + + generator(generator&& __other) noexcept + : __promise_(std::exchange(__other.__promise_, nullptr)) + , __coro_(std::exchange(__other.__coro_, {})) + , __started_(std::exchange(__other.__started_, false)) { + } + + ~generator() noexcept { + if (__coro_) { + if (__started_ && !__coro_.done()) { + __promise_->__value_.destruct(); + } + __coro_.destroy(); + } + } + + generator& operator=(generator g) noexcept { + swap(g); + return *this; + } + + void swap(generator& __other) noexcept { + std::swap(__promise_, __other.__promise_); + std::swap(__coro_, __other.__coro_); + std::swap(__started_, __other.__started_); + } + + struct sentinel {}; + + class iterator { + public: + using iterator_category = std::input_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = _Value; + using reference = _Ref; + using pointer = std::add_pointer_t<_Ref>; + + iterator() noexcept = default; + iterator(const iterator &) = delete; + + iterator(iterator&& __other) noexcept + : __promise_(std::exchange(__other.__promise_, nullptr)) + , __coro_(std::exchange(__other.__coro_, {})) + {} + + iterator& operator=(iterator&& __other) { + std::swap(__promise_, __other.__promise_); + std::swap(__coro_, __other.__coro_); + return *this; + } + + ~iterator() = default; + + friend bool operator==(const iterator &it, sentinel) noexcept { + return !it.__coro_ || it.__coro_.done(); + } + + iterator& operator++() { + __promise_->__value_.destruct(); +#if NESTED_GENERATOR + __promise_->resume(); +#else + __coro_.resume(); +#endif + return *this; + } + + void operator++(int) { + (void)operator++(); + } + + reference operator*() const noexcept { + return static_cast(__promise_->__value_.get()); + } + + private: + friend generator; + + explicit iterator(__promise_base* __promise, std::coroutine_handle<> __coro) noexcept + : __promise_(__promise) + , __coro_(__coro) + {} + + __promise_base* __promise_; + std::coroutine_handle<> __coro_; + }; + + iterator begin() { + if (__coro_) { + assert(!__started_); + __started_ = true; + __coro_.resume(); + } + return iterator{__promise_, __coro_}; + } + + sentinel end() noexcept { + return {}; + } + +private: + template + friend struct __generator_promise; + + template + explicit generator(std::coroutine_handle<_Promise> __coro) noexcept + : __promise_(std::addressof(__coro.promise())) + , __coro_(__coro) + {} + +public: // to get around access restrictions for __yield_sequence_awaitable + std::coroutine_handle<> __get_coro() noexcept { return __coro_; } + __promise_base* __get_promise() noexcept { return __promise_; } + +private: + __promise_base* __promise_; + std::coroutine_handle<> __coro_; + bool __started_ = false; +}; + +#if __has_include() +namespace ranges { + +template +constexpr inline bool enable_view> = true; + +} // namespace ranges +#endif + +} // namespace std + +#endif // __STD_GENERATOR_INCLUDED diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..8fcfa01 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,11 @@ +# Copyright Lewis Baker, Corentin Jabot +# Licensed under Boost Software License 1.0 + +file(GLOB test-sources "*_test.cpp") +foreach(file-path ${test-sources}) + string( REPLACE ".cpp" "" file-path-without-ext ${file-path} ) + get_filename_component(file-name ${file-path-without-ext} NAME) + add_executable( ${file-name} ${file-path}) + target_link_libraries(${file-name} PUBLIC stdgenerator) + add_test(NAME "test-${file-name}" COMMAND ${file-name}) +endforeach() diff --git a/tests/check.hpp b/tests/check.hpp new file mode 100644 index 0000000..b5df8de --- /dev/null +++ b/tests/check.hpp @@ -0,0 +1,34 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright Lewis Baker, Corentin Jabot +// +// Use, modification and distribution is subject to the Boost Software License, +// Version 1.0. +// (See accompanying file LICENSE or http://www.boost.org/LICENSE_1_0.txt) +/////////////////////////////////////////////////////////////////////////////// +#ifndef CHECK_HPP_INCLUDED +#define CHECK_HPP_INCLUDED + +#pragma once + +#include +#include + +#define STRINGIFY2(X) #X +#define STRINGIFY(X) STRINGIFY2(X) + +#define CHECK(X) \ + do { \ + if (X) ; else { \ + ::std::puts("FAIL: " __FILE__ "(" STRINGIFY(__LINE__) ") '" #X "' was not true"); \ + ::std::abort(); \ + } \ + } while (false) + +#define RUN(X) \ + do { \ + std::puts("-> " #X); \ + std::fflush(stdout); \ + X(); \ + } while (false) + +#endif diff --git a/tests/nested_test.cpp b/tests/nested_test.cpp new file mode 100644 index 0000000..0e114b5 --- /dev/null +++ b/tests/nested_test.cpp @@ -0,0 +1,184 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright Lewis Baker, Corentin Jabot +// +// Use, modification and distribution is subject to the Boost Software License, +// Version 1.0. +// (See accompanying file LICENSE or http://www.boost.org/LICENSE_1_0.txt) +/////////////////////////////////////////////////////////////////////////////// +#include +#include +#include + +#include "check.hpp" + +void test_yielding_elements_of_default_constructed_generator() { + bool started = false; + bool finished = false; + auto makeGen = [&]() -> std::generator { + started = true; + co_yield std::ranges::elements_of(std::generator{}); + finished = true; + }; + + auto gen = makeGen(); + CHECK(!started); + CHECK(!finished); + auto it = gen.begin(); + CHECK(started); + CHECK(finished); + CHECK(it == gen.end()); +} + +void test_yielding_elements_of_empty_generator() { + bool started1 = false; + bool started2 = false; + bool finished = false; + auto makeGen = [&]() -> std::generator { + started1 = true; + co_yield std::ranges::elements_of([&]() -> std::generator { + started2 = true; + co_return; + }()); + finished = true; + }; + + auto gen = makeGen(); + CHECK(!started1); + CHECK(!started2); + CHECK(!finished); + auto it = gen.begin(); + CHECK(started1); + CHECK(started2); + CHECK(finished); + CHECK(it == gen.end()); +} + +void test_yielding_elements_of_nested_one_level() { + int checkpoint = 0; + auto makeGen = [&]() -> std::generator { + checkpoint = 1; + co_yield 1; + checkpoint = 2; + co_yield std::ranges::elements_of([&]() -> std::generator { + checkpoint = 3; + co_yield 2; + checkpoint = 4; + }()); + checkpoint = 5; + co_yield 3; + checkpoint = 6; + }; + + auto gen = makeGen(); + CHECK(checkpoint == 0); + auto it = gen.begin(); + CHECK(checkpoint == 1); + CHECK(it != gen.end()); + CHECK(*it == 1); + ++it; + CHECK(checkpoint == 3); + CHECK(it != gen.end()); + CHECK(*it == 2); + ++it; + CHECK(checkpoint == 5); + CHECK(it != gen.end()); + CHECK(*it == 3); + ++it; + CHECK(checkpoint == 6); + CHECK(it == gen.end()); +} + +void test_yielding_elements_of_recursive() { + auto makeGen = [](auto& makeGen, int depth) -> std::generator { + co_yield depth; + if (depth > 0) { + co_yield std::ranges::elements_of(makeGen(makeGen, depth - 1)); + co_yield -depth; + } + }; + + auto gen = makeGen(makeGen, 3); + auto it = gen.begin(); + CHECK(it != gen.end()); + CHECK(*it == 3); + ++it; + CHECK(it != gen.end()); + CHECK(*it == 2); + ++it; + CHECK(it != gen.end()); + CHECK(*it == 1); + ++it; + CHECK(it != gen.end()); + CHECK(*it == 0); + ++it; + CHECK(it != gen.end()); + CHECK(*it == -1); + ++it; + CHECK(it != gen.end()); + CHECK(*it == -2); + ++it; + CHECK(it != gen.end()); + CHECK(*it == -3); + ++it; + CHECK(it == gen.end()); +} + +void test_yielding_elements_of_generator_with_different_value_type() { + auto strings = [](int x) -> std::generator { + co_yield std::to_string(x); + + // This should still perform O(1) nested suspend/resume operations even + // though the value_type is different. + // Note that there's not really a way to test this property, though. + co_yield std::ranges::elements_of([]() -> std::generator { + co_yield "foo"; + co_yield "bar"; + }()); + + co_yield std::to_string(x + 1); + }(42); + + auto it = strings.begin(); + CHECK(it != strings.end()); + CHECK(*it == "42"); + ++it; + CHECK(it != strings.end()); + CHECK(*it == "foo"); + ++it; + CHECK(it != strings.end()); + CHECK(*it == "bar"); + ++it; + CHECK(it != strings.end()); + CHECK(*it == "43"); + ++it; + CHECK(it == strings.end()); +} + +void test_yielding_elements_of_generator_with_different_reference_type() { + // TODO +} + +void test_yielding_elements_of_generator_with_different_allocator_type() { + // TODO +} + +void test_yielding_elements_of_vector() { + // TODO +} + +void test_nested_generator_scopes_exit_innermost_scope_first() { + // TODO +} + +int main() { + RUN(test_yielding_elements_of_default_constructed_generator); + RUN(test_yielding_elements_of_empty_generator); + RUN(test_yielding_elements_of_nested_one_level); + RUN(test_yielding_elements_of_recursive); + RUN(test_yielding_elements_of_generator_with_different_value_type); + RUN(test_yielding_elements_of_generator_with_different_reference_type); + RUN(test_yielding_elements_of_generator_with_different_allocator_type); + RUN(test_yielding_elements_of_vector); + RUN(test_nested_generator_scopes_exit_innermost_scope_first); + return 0; +} \ No newline at end of file diff --git a/tests/simple_test.cpp b/tests/simple_test.cpp new file mode 100644 index 0000000..3fa5836 --- /dev/null +++ b/tests/simple_test.cpp @@ -0,0 +1,179 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright Lewis Baker, Corentin Jabot +// +// Use, modification and distribution is subject to the Boost Software License, +// Version 1.0. +// (See accompanying file LICENSE or http://www.boost.org/LICENSE_1_0.txt) +/////////////////////////////////////////////////////////////////////////////// +#include +#include +#include + +#include "check.hpp" + +// Check some basic properties of the type at compile-time. + +// A generator should be a 'range' +static_assert(std::ranges::range>); + +// A generator should also be a 'view' +static_assert(std::ranges::view>); + +// +static_assert(std::is_same_v< + std::ranges::range_reference_t>, + const std::string&>); +static_assert(std::is_same_v< + std::ranges::range_value_t>, + std::string>); + +void test_default_constructor() { + std::generator g; + CHECK(g.begin() == g.end()); +} + +void test_empty_generator() { + bool started = false; + auto makeGen = [&]() -> std::generator { + started = true; + co_return; + }; + auto gen = makeGen(); + CHECK(!started); + auto it = gen.begin(); + CHECK(started); + CHECK(it == gen.end()); +} + +void test_move_constructor() { + auto g = []() -> std::generator { co_yield 42; }(); + auto g2 = std::move(g); + auto it = g2.begin(); + CHECK(it != g2.end()); + CHECK(*it == 42); + ++it; + CHECK(it == g2.end()); +} + +void test_range_based_for_loop() { + std::generator g = []() -> std::generator { co_yield 42; }(); + size_t count = 0; + for (decltype(auto) x : g) { + static_assert(std::is_same_v); + CHECK(x == 42); + ++count; + } + CHECK(count == 1); +} + +void test_range_based_for_loop_2() { + static size_t count; + count = 0; + + struct X { + X() { ++count; } + X(const X&) { ++count; } + X(X&&) { ++count; } + ~X() { --count; } + }; + + auto g = []() -> std::generator { + co_yield X{}; + co_yield X{}; + }(); + + size_t elementCount = 0; + + for (decltype(auto) x : g) { + static_assert(std::is_same_v); + + // 1. temporary in co_yield expression + // 2. reference value stored in promise + // 3. iteration variable + CHECK(count == 3); + ++elementCount; + } + + CHECK(count == 0); + CHECK(elementCount == 2); +} + +void test_range_based_for_loop_3() { + static size_t count; + count = 0; + + struct X { + X() { ++count; } + X(const X&) { ++count; } + X(X&&) { ++count; } + ~X() { --count; } + }; + + auto g = []() -> std::generator { + co_yield X{}; + co_yield X{}; + }(); + + size_t elementCount = 0; + + for (decltype(auto) x : g) { + static_assert(std::is_same_v); + + // 1. temporary in co_yield expression + CHECK(count == 1); + ++elementCount; + } + + CHECK(count == 0); + CHECK(elementCount == 2); +} + +void test_dereference_iterator_copies_reference() { + static size_t ctorCount = 0; + static size_t dtorCount = 0; + struct X { + X() { ++ctorCount; } + X(const X&) { ++ctorCount; } + X(X&&) { ++ctorCount; } + ~X() { ++dtorCount; } + }; + + { + auto g = []() -> std::generator { + co_yield X{}; + }(); + + CHECK(ctorCount == 0); + CHECK(dtorCount == 0); + + auto it = g.begin(); + + CHECK(ctorCount > 0); + CHECK(dtorCount == 0); + + for (int i = 0; i < 3; ++i) { + auto beforeCtorCount = ctorCount; + auto beforeDtorCount = dtorCount; + { + decltype(auto) x = *it; + CHECK(ctorCount == beforeCtorCount + 1); + CHECK(dtorCount == beforeDtorCount); + } + CHECK(ctorCount = beforeCtorCount + 1); + CHECK(dtorCount == beforeDtorCount + 1); + } + } + + CHECK(ctorCount == dtorCount); +} + +int main() { + RUN(test_default_constructor); + RUN(test_empty_generator); + RUN(test_move_constructor); + RUN(test_range_based_for_loop); + RUN(test_range_based_for_loop_2); + RUN(test_range_based_for_loop_3); + RUN(test_dereference_iterator_copies_reference); + return 0; +}