diff --git a/.github/workflows/code_testing.yaml b/.github/workflows/code_testing.yaml index 5e30997..452cf4e 100644 --- a/.github/workflows/code_testing.yaml +++ b/.github/workflows/code_testing.yaml @@ -12,12 +12,12 @@ jobs: fail-fast: false matrix: config: - - os: ubuntu-22.04 - compiler: clang-15 - os: ubuntu-22.04 compiler: clang-16 - os: ubuntu-22.04 compiler: clang-17 + - os: ubuntu-22.04 + compiler: clang-18 - os: ubuntu-22.04 compiler: gcc-12 @@ -30,7 +30,7 @@ jobs: shell: bash steps: - - name: Add repos for for gcc-13 and clang-16 + - name: Add repos for for gcc-13 and clang-16,.. uses: dice-group/cpp-conan-release-reusable-workflow/.github/actions/setup_apt@main - name: Install CMake @@ -66,7 +66,7 @@ jobs: uses: dice-group/cpp-conan-release-reusable-workflow/.github/actions/add_conan_provider@main - name: Configure CMake - run: cmake -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=On -DBUILD_EXAMPLES=On -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=conan_provider.cmake -DCONAN_INSTALL_ARGS="--build=missing;-o=boost/*:header_only=True" -G Ninja -B build . + run: cmake -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined" -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=On -DBUILD_EXAMPLES=On -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=conan_provider.cmake -G Ninja -B build . env: CC: ${{ steps.install_cc.outputs.cc }} CXX: ${{ steps.install_cc.outputs.cxx }} diff --git a/CMakeLists.txt b/CMakeLists.txt index ef0b6cb..0058d97 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.24) project( dice-template-library - VERSION 1.5.1 + VERSION 1.6.0 DESCRIPTION "This template library is a collection of template-oriented code that we, the Data Science Group at UPB, found pretty handy. It contains: `switch_cases` (Use runtime values in compile-time context), `integral_template_tuple` (Create a tuple-like structure that instantiates a template for a range of values), `integral_template_variant` (A wrapper type for `std::variant` guarantees to only contain variants of the form `T` and `for_{types,values,range}` (Compile time for loops for types, values or ranges))." HOMEPAGE_URL "https://dice-research.org/") @@ -10,6 +10,10 @@ project( option(BUILD_TESTING "build tests" OFF) option(BUILD_EXAMPLES "build examples" OFF) +if (PROJECT_IS_TOP_LEVEL) + set(CONAN_INSTALL_ARGS "${CONAN_INSTALL_ARGS};-o=boost/*:header_only=True") +endif () + # conan requires cmake build type to be specified and it is generally a good idea if (NOT EXISTS ${CMAKE_CURRENT_BINARY_DIR}/CMakeCache.txt) if (NOT CMAKE_BUILD_TYPE) diff --git a/README.md b/README.md index b57dde5..4f0a9fb 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,10 @@ It contains: - `for_{types,values,range}`: Compile time for loops for types, values or ranges - `polymorphic_allocator`: Like `std::pmr::polymorphic_allocator` but with static dispatch - `DICE_DEFER`/`DICE_DEFER_TO_SUCCES`/`DICE_DEFER_TO_FAIL`: On-the-fly RAII for types that do not support it natively (similar to go's `defer` keyword) +- `overloaded`: Composition for `std::variant` visitor lambdas +- `flex_array`: A combination of `std::array` and `std::span` +- `tuple_algorithms`: Some algorithms for iterating tuples +- `generator`: The reference implementation of `std::generator` from [P2502R2](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2502r2.pdf) ## Usage @@ -71,6 +75,13 @@ Some algorithms for iterating tuples, for example `tuple_fold` a fold/reduce imp A combination of `std::array` and `std::span` where the size is either statically known or a runtime variable depending on the `extent` template parameter +### `generator` +The reference implementation of `std::generator` from [P2502R2](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2502r2.pdf). +By default, the generator and corresponding utilities are exported under the `dice::template_library::` namespace. +If you want this generator to serve as a drop in replacement for `std::generator` until it arrives +use `#define DICE_TEMPLATELIBRARY_GENERATOR_STD_COMPAT 1` before including the generator header. That will export +all generator related things under namespace `std::`. + ### Further Examples Compilable code examples can be found in [examples](./examples). The example build requires the cmake @@ -90,7 +101,7 @@ add FetchContent_Declare( dice-template-library GIT_REPOSITORY "https://github.com/dice-group/dice-template-library.git" - GIT_TAG v1.5.1 + GIT_TAG v1.6.0 GIT_SHALLOW TRUE) FetchContent_MakeAvailable(dice-template-library) @@ -109,7 +120,7 @@ target_link_libraries(your_target ### conan You can use it with [conan](https://conan.io/). -To do so, you need to add `dice-template-library/1.5.1` to the `[requires]` section of your conan file. +To do so, you need to add `dice-template-library/1.6.0` to the `[requires]` section of your conan file. ## Build and Run Tests and Examples diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 59328c2..78d9517 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -58,3 +58,10 @@ target_link_libraries(example_flex_array PRIVATE dice-template-library::dice-template-library ) + +add_executable(example_generator + example_generator.cpp) +target_link_libraries(example_generator + PRIVATE + dice-template-library::dice-template-library +) diff --git a/examples/example_generator.cpp b/examples/example_generator.cpp new file mode 100644 index 0000000..c9e2a3c --- /dev/null +++ b/examples/example_generator.cpp @@ -0,0 +1,46 @@ +#include +#include + +#include + +namespace dtl = dice::template_library; + +template +struct Tree { + T value; + Tree *left = nullptr; + Tree *right = nullptr; + + [[nodiscard]] dtl::generator traverse_inorder() const { + if (left != nullptr) { + co_yield dtl::ranges::elements_of(left->traverse_inorder()); + } + + co_yield value; + + if (right != nullptr) { + co_yield dtl::ranges::elements_of(right->traverse_inorder()); + } + } +}; + +int main() { + // D + // B F + // A C E G + Tree leaf1{'A'}; + Tree leaf2{'C'}; + Tree leaf3{'E'}; + Tree leaf4{'G'}; + Tree branch1{'B', &leaf1, &leaf2}; + Tree branch2{'F', &leaf3, &leaf4}; + Tree root{'D', &branch1, &branch2}; + + std::string output; + for (char const x : root.traverse_inorder()) { + output.push_back(x); + } + + assert(output == "ABCDEFG"); + std::cout << output << '\n'; +} diff --git a/include/dice/template-library/generator.hpp b/include/dice/template-library/generator.hpp new file mode 100644 index 0000000..432b86a --- /dev/null +++ b/include/dice/template-library/generator.hpp @@ -0,0 +1,528 @@ +#ifndef DICE_TEMPLATELIBRARY_GENERATOR_HPP +#define DICE_TEMPLATELIBRARY_GENERATOR_HPP + +//////////////////////////////////////////////////////////////// +// Reference implementation of std::generator proposal P2502R2 +// https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2502r2.pdf + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef DICE_TEMPLATELIBRARY_GENERATOR_STD_COMPAT +#define DICE_TEMPLATELIBRARY_GENERATOR_NAMESPACE std +#else +#define DICE_TEMPLATELIBRARY_GENERATOR_NAMESPACE dice::template_library +#endif // DICE_TEMPLATELIBRARY_GENERATOR_STD_COMPAT + + +#ifdef _MSC_VER +#define EMPTY_BASES __declspec(empty_bases) +#ifdef __clang__ +#define NO_UNIQUE_ADDRESS +#else +#define NO_UNIQUE_ADDRESS [[msvc::no_unique_address]] +#endif +#else +#define EMPTY_BASES +#define NO_UNIQUE_ADDRESS [[no_unique_address]] +#endif + +namespace DICE_TEMPLATELIBRARY_GENERATOR_NAMESPACE { + struct alignas(__STDCPP_DEFAULT_NEW_ALIGNMENT__) _Aligned_block { + unsigned char _Pad[__STDCPP_DEFAULT_NEW_ALIGNMENT__]; + }; + + template + using _Rebind = typename ::std::allocator_traits<_Alloc>::template rebind_alloc<_Aligned_block>; + + template + concept _Has_real_pointers = + ::std::same_as<_Alloc, void> || ::std::is_pointer_v::pointer>; + + template + class _Promise_allocator { // statically specified allocator type + private: + using _Alloc = _Rebind<_Allocator>; + + static void* _Allocate(_Alloc _Al, const size_t _Size) { + if constexpr (::std::default_initializable< + _Alloc> && ::std::allocator_traits<_Alloc>::is_always_equal::value) { + // do not store stateless allocator + const size_t _Count = (_Size + sizeof(_Aligned_block) - 1) / sizeof(_Aligned_block); + return _Al.allocate(_Count); + } else { + // store stateful allocator + static constexpr size_t _Align = + (::std::max)(alignof(_Alloc), sizeof(_Aligned_block)); + const size_t _Count = + (_Size + sizeof(_Alloc) + _Align - 1) / sizeof(_Aligned_block); + void* const _Ptr = _Al.allocate(_Count); + const auto _Al_address = + (reinterpret_cast(_Ptr) + _Size + alignof(_Alloc) - 1) + & ~(alignof(_Alloc) - 1); + ::new (reinterpret_cast(_Al_address)) _Alloc(::std::move(_Al)); + return _Ptr; + } + } + + public: + static void* operator new(const size_t _Size) requires ::std::default_initializable<_Alloc> { + return _Allocate(_Alloc{}, _Size); + } + + template + requires ::std::convertible_to + static void* operator new( + const size_t _Size, ::std::allocator_arg_t, const _Alloc2& _Al, const _Args&...) { + return _Allocate(static_cast<_Alloc>(static_cast<_Allocator>(_Al)), _Size); + } + + template + requires ::std::convertible_to + static void* operator new(const size_t _Size, const _This&, ::std::allocator_arg_t, + const _Alloc2& _Al, const _Args&...) { + return _Allocate(static_cast<_Alloc>(static_cast<_Allocator>(_Al)), _Size); + } + + static void operator delete(void* const _Ptr, const size_t _Size) noexcept { + if constexpr (::std::default_initializable< + _Alloc> && ::std::allocator_traits<_Alloc>::is_always_equal::value) { + // make stateless allocator + _Alloc _Al{}; + const size_t _Count = (_Size + sizeof(_Aligned_block) - 1) / sizeof(_Aligned_block); + _Al.deallocate(static_cast<_Aligned_block*>(_Ptr), _Count); + } else { + // retrieve stateful allocator + const auto _Al_address = + (reinterpret_cast(_Ptr) + _Size + alignof(_Alloc) - 1) + & ~(alignof(_Alloc) - 1); + auto& _Stored_al = *reinterpret_cast<_Alloc*>(_Al_address); + _Alloc _Al{::std::move(_Stored_al)}; + _Stored_al.~_Alloc(); + + static constexpr size_t _Align = + (::std::max)(alignof(_Alloc), sizeof(_Aligned_block)); + const size_t _Count = + (_Size + sizeof(_Alloc) + _Align - 1) / sizeof(_Aligned_block); + _Al.deallocate(static_cast<_Aligned_block*>(_Ptr), _Count); + } + } + }; + + template <> + class _Promise_allocator { // type-erased allocator + private: + using _Dealloc_fn = void (*)(void*, size_t); + + template + static void* _Allocate(const _ProtoAlloc& _Proto, size_t _Size) { + using _Alloc = _Rebind<_ProtoAlloc>; + auto _Al = static_cast<_Alloc>(_Proto); + + if constexpr (::std::default_initializable< + _Alloc> && ::std::allocator_traits<_Alloc>::is_always_equal::value) { + // don't store stateless allocator + const _Dealloc_fn _Dealloc = [](void* const _Ptr, const size_t _Size) { + _Alloc _Al{}; + const size_t _Count = (_Size + sizeof(_Dealloc_fn) + sizeof(_Aligned_block) - 1) + / sizeof(_Aligned_block); + _Al.deallocate(static_cast<_Aligned_block*>(_Ptr), _Count); + }; + + const size_t _Count = (_Size + sizeof(_Dealloc_fn) + sizeof(_Aligned_block) - 1) + / sizeof(_Aligned_block); + void* const _Ptr = _Al.allocate(_Count); + ::memcpy(static_cast(_Ptr) + _Size, &_Dealloc, sizeof(_Dealloc)); + return _Ptr; + } else { + // store stateful allocator + static constexpr size_t _Align = + (::std::max)(alignof(_Alloc), sizeof(_Aligned_block)); + + const _Dealloc_fn _Dealloc = [](void* const _Ptr, size_t _Size) { + _Size += sizeof(_Dealloc_fn); + const auto _Al_address = + (reinterpret_cast(_Ptr) + _Size + alignof(_Alloc) - 1) + & ~(alignof(_Alloc) - 1); + auto& _Stored_al = *reinterpret_cast(_Al_address); + _Alloc _Al{::std::move(_Stored_al)}; + _Stored_al.~_Alloc(); + + const size_t _Count = + (_Size + sizeof(_Al) + _Align - 1) / sizeof(_Aligned_block); + _Al.deallocate(static_cast<_Aligned_block*>(_Ptr), _Count); + }; + + const size_t _Count = (_Size + sizeof(_Dealloc_fn) + sizeof(_Al) + _Align - 1) + / sizeof(_Aligned_block); + void* const _Ptr = _Al.allocate(_Count); + ::memcpy(static_cast(_Ptr) + _Size, &_Dealloc, sizeof(_Dealloc)); + _Size += sizeof(_Dealloc_fn); + const auto _Al_address = + (reinterpret_cast(_Ptr) + _Size + alignof(_Alloc) - 1) + & ~(alignof(_Alloc) - 1); + ::new (reinterpret_cast(_Al_address)) _Alloc{::std::move(_Al)}; + return _Ptr; + } + } + + public: + static void* operator new(const size_t _Size) { // default: new/delete + void* const _Ptr = ::operator new[](_Size + sizeof(_Dealloc_fn)); + const _Dealloc_fn _Dealloc = [](void* const _Ptr, const size_t _Size) { +#ifdef __cpp_sized_deallocation + ::operator delete[](_Ptr, _Size + sizeof(_Dealloc_fn)); +#else + (void) _Size; + ::operator delete[](_Ptr); +#endif + }; + ::memcpy(static_cast(_Ptr) + _Size, &_Dealloc, sizeof(_Dealloc_fn)); + return _Ptr; + } + + template + static void* operator new( + const size_t _Size, ::std::allocator_arg_t, const _Alloc& _Al, const _Args&...) { + static_assert( + _Has_real_pointers<_Alloc>, "coroutine allocators must use true pointers"); + return _Allocate(_Al, _Size); + } + + template + static void* operator new( + const size_t _Size, const _This&, ::std::allocator_arg_t, const _Alloc& _Al, const _Args&...) { + static_assert( + _Has_real_pointers<_Alloc>, "coroutine allocators must use true pointers"); + return _Allocate(_Al, _Size); + } + + static void operator delete(void* const _Ptr, const size_t _Size) noexcept { + _Dealloc_fn _Dealloc; + ::memcpy(&_Dealloc, static_cast(_Ptr) + _Size, sizeof(_Dealloc_fn)); + _Dealloc(_Ptr, _Size); + } + }; + + namespace ranges { + template <::std::ranges::range _Rng, class _Alloc = ::std::allocator<::std::byte>> + struct elements_of { + NO_UNIQUE_ADDRESS _Rng range; + NO_UNIQUE_ADDRESS _Alloc allocator{}; + }; + + template > + elements_of(_Rng&&, _Alloc = {}) -> elements_of<_Rng&&, _Alloc>; + } // namespace ranges + + template + class generator; + + template + using _Gen_value_t = ::std::conditional_t<::std::is_void_v<_Vty>, ::std::remove_cvref_t<_Rty>, _Vty>; + template + using _Gen_reference_t = ::std::conditional_t<::std::is_void_v<_Vty>, _Rty&&, _Rty>; + template + using _Gen_yield_t = ::std::conditional_t<::std::is_reference_v<_Ref>, _Ref, const _Ref&>; + + template + class _Gen_promise_base { + public: + static_assert(::std::is_reference_v<_Yielded>); + + /* [[nodiscard]] */ ::std::suspend_always initial_suspend() noexcept { + return {}; + } + + [[nodiscard]] auto final_suspend() noexcept { + return _Final_awaiter{}; + } + + [[nodiscard]] ::std::suspend_always yield_value(_Yielded _Val) noexcept { + _Ptr = ::std::addressof(_Val); + return {}; + } + + // clang-format off + [[nodiscard]] auto yield_value(const ::std::remove_reference_t<_Yielded>& _Val) + noexcept(::std::is_nothrow_constructible_v<::std::remove_cvref_t<_Yielded>, const ::std::remove_reference_t<_Yielded>&>) + requires (::std::is_rvalue_reference_v<_Yielded> && + ::std::constructible_from<::std::remove_cvref_t<_Yielded>, const ::std::remove_reference_t<_Yielded>&>) { + // clang-format on + return _Element_awaiter{_Val}; + } + + // clang-format off + template + requires ::std::same_as<_Gen_yield_t<_Gen_reference_t<_Rty, _Vty>>, _Yielded> + [[nodiscard]] auto yield_value( + ::DICE_TEMPLATELIBRARY_GENERATOR_NAMESPACE::ranges::elements_of&&, _Unused> _Elem) noexcept { + // clang-format on + return _Nested_awaitable<_Rty, _Vty, _Alloc>{std::move(_Elem.range)}; + } + + // clang-format off + template <::std::ranges::input_range _Rng, class _Alloc> + requires ::std::convertible_to<::std::ranges::range_reference_t<_Rng>, _Yielded> + [[nodiscard]] auto yield_value(::DICE_TEMPLATELIBRARY_GENERATOR_NAMESPACE::ranges::elements_of<_Rng, _Alloc> _Elem) noexcept { + // clang-format on + using _Vty = ::std::ranges::range_value_t<_Rng>; + return _Nested_awaitable<_Yielded, _Vty, _Alloc>{ + [](::std::allocator_arg_t, _Alloc, ::std::ranges::iterator_t<_Rng> _It, + const ::std::ranges::sentinel_t<_Rng> _Se) + -> generator<_Yielded, _Vty, _Alloc> { + for (; _It != _Se; ++_It) { + co_yield static_cast<_Yielded>(*_It); + } + }(::std::allocator_arg, _Elem.allocator, ::std::ranges::begin(_Elem.range), + ::std::ranges::end(_Elem.range))}; + } + + void await_transform() = delete; + + void return_void() noexcept {} + + void unhandled_exception() { + if (_Info) { + _Info->_Except = ::std::current_exception(); + } else { + throw; + } + } + + private: + struct _Element_awaiter { + ::std::remove_cvref_t<_Yielded> _Val; + + [[nodiscard]] constexpr bool await_ready() const noexcept { + return false; + } + + template + constexpr void await_suspend(::std::coroutine_handle<_Promise> _Handle) noexcept { +#ifdef __cpp_lib_is_pointer_interconvertible + static_assert(::std::is_pointer_interconvertible_base_of_v<_Gen_promise_base, _Promise>); +#endif // __cpp_lib_is_pointer_interconvertible + + _Gen_promise_base& _Current = _Handle.promise(); + _Current._Ptr = ::std::addressof(_Val); + } + + constexpr void await_resume() const noexcept {} + }; + + struct _Nest_info { + ::std::exception_ptr _Except; + ::std::coroutine_handle<_Gen_promise_base> _Parent; + ::std::coroutine_handle<_Gen_promise_base> _Root; + }; + + struct _Final_awaiter { + [[nodiscard]] bool await_ready() noexcept { + return false; + } + + template + [[nodiscard]] ::std::coroutine_handle<> await_suspend( + ::std::coroutine_handle<_Promise> _Handle) noexcept { +#ifdef __cpp_lib_is_pointer_interconvertible + static_assert(::std::is_pointer_interconvertible_base_of_v<_Gen_promise_base, _Promise>); +#endif // __cpp_lib_is_pointer_interconvertible + + _Gen_promise_base& _Current = _Handle.promise(); + if (!_Current._Info) { + return ::std::noop_coroutine(); + } + + ::std::coroutine_handle<_Gen_promise_base> _Cont = _Current._Info->_Parent; + _Current._Info->_Root.promise()._Top = _Cont; + _Current._Info = nullptr; + return _Cont; + } + + void await_resume() noexcept {} + }; + + template + struct _Nested_awaitable { + static_assert(::std::same_as<_Gen_yield_t<_Gen_reference_t<_Rty, _Vty>>, _Yielded>); + + _Nest_info _Nested; + generator<_Rty, _Vty, _Alloc> _Gen; + + explicit _Nested_awaitable(generator<_Rty, _Vty, _Alloc>&& _Gen_) noexcept + : _Gen(::std::move(_Gen_)) {} + + [[nodiscard]] bool await_ready() noexcept { + return !_Gen._Coro; + } + + template + [[nodiscard]] ::std::coroutine_handle<_Gen_promise_base> await_suspend( + ::std::coroutine_handle<_Promise> _Current) noexcept { +#ifdef __cpp_lib_is_pointer_interconvertible + static_assert(::std::is_pointer_interconvertible_base_of_v<_Gen_promise_base, _Promise>); +#endif // __cpp_lib_is_pointer_interconvertible + auto _Target = + ::std::coroutine_handle<_Gen_promise_base>::from_address(_Gen._Coro.address()); + _Nested._Parent = + ::std::coroutine_handle<_Gen_promise_base>::from_address(_Current.address()); + _Gen_promise_base& _Parent_promise = _Nested._Parent.promise(); + if (_Parent_promise._Info) { + _Nested._Root = _Parent_promise._Info->_Root; + } else { + _Nested._Root = _Nested._Parent; + } + _Nested._Root.promise()._Top = _Target; + _Target.promise()._Info = ::std::addressof(_Nested); + return _Target; + } + + void await_resume() { + if (_Nested._Except) { + ::std::rethrow_exception(::std::move(_Nested._Except)); + } + } + }; + + template + friend class _Gen_iter; + + // _Top and _Info are mutually exclusive, and could potentially be merged. + ::std::coroutine_handle<_Gen_promise_base> _Top = + ::std::coroutine_handle<_Gen_promise_base>::from_promise(*this); + ::std::add_pointer_t<_Yielded> _Ptr = nullptr; + _Nest_info* _Info = nullptr; + }; + + struct _Gen_secret_tag {}; + + template + class _Gen_iter { + public: + using value_type = _Value; + using difference_type = ptrdiff_t; + + _Gen_iter(_Gen_iter&& _That) noexcept : _Coro{::std::exchange(_That._Coro, {})} {} + + _Gen_iter& operator=(_Gen_iter&& _That) noexcept { + _Coro = ::std::exchange(_That._Coro, {}); + return *this; + } + + [[nodiscard]] _Ref operator*() const noexcept { + assert(!_Coro.done() && "Can't dereference generator end iterator"); + return static_cast<_Ref>(*_Coro.promise()._Top.promise()._Ptr); + } + + _Gen_iter& operator++() { + assert(!_Coro.done() && "Can't increment generator end iterator"); + _Coro.promise()._Top.resume(); + return *this; + } + + void operator++(int) { + ++*this; + } + + [[nodiscard]] bool operator==(::std::default_sentinel_t) const noexcept { + return _Coro.done(); + } + + private: + template + friend class generator; + + explicit _Gen_iter(_Gen_secret_tag, + ::std::coroutine_handle<_Gen_promise_base<_Gen_yield_t<_Ref>>> _Coro_) noexcept + : _Coro{_Coro_} {} + + ::std::coroutine_handle<_Gen_promise_base<_Gen_yield_t<_Ref>>> _Coro; + }; + + template + class generator : public ::std::ranges::view_interface> { + private: + using _Value = _Gen_value_t<_Rty, _Vty>; + static_assert(::std::same_as<::std::remove_cvref_t<_Value>, _Value> && ::std::is_object_v<_Value>, + "generator's value type must be a cv-unqualified object type"); + + // clang-format off + using _Ref = _Gen_reference_t<_Rty, _Vty>; + static_assert(::std::is_reference_v<_Ref> + || (::std::is_object_v<_Ref> && ::std::same_as<::std::remove_cv_t<_Ref>, _Ref> && ::std::copy_constructible<_Ref>), + "generator's second argument must be a reference type or a cv-unqualified " + "copy-constructible object type"); + + using _RRef = ::std::conditional_t<::std::is_lvalue_reference_v<_Ref>, ::std::remove_reference_t<_Ref>&&, _Ref>; + + static_assert(::std::common_reference_with<_Ref&&, _Value&> && ::std::common_reference_with<_Ref&&, _RRef&&> + && ::std::common_reference_with<_RRef&&, const _Value&>, + "an iterator with the selected value and reference types cannot model indirectly_readable"); + // clang-format on + + static_assert(_Has_real_pointers<_Alloc>, "generator allocators must use true pointers"); + + friend _Gen_promise_base<_Gen_yield_t<_Ref>>; + + public: + struct EMPTY_BASES promise_type : _Promise_allocator<_Alloc>, + _Gen_promise_base<_Gen_yield_t<_Ref>> { + [[nodiscard]] generator get_return_object() noexcept { + return generator{ + _Gen_secret_tag{}, ::std::coroutine_handle::from_promise(*this)}; + } + }; + static_assert(::std::is_standard_layout_v); +#ifdef __cpp_lib_is_pointer_interconvertible + static_assert(::std::is_pointer_interconvertible_base_of_v<_Gen_promise_base<_Gen_yield_t<_Ref>>, + promise_type>); +#endif // __cpp_lib_is_pointer_interconvertible + + generator(generator&& _That) noexcept : _Coro(::std::exchange(_That._Coro, {})) {} + + ~generator() { + if (_Coro) { + _Coro.destroy(); + } + } + + generator& operator=(generator _That) noexcept { + ::std::swap(_Coro, _That._Coro); + return *this; + } + + using iterator = _Gen_iter<_Value, _Ref>; + using sentinel = ::std::default_sentinel_t; + + [[nodiscard]] iterator begin() { + // Pre: _Coro is suspended at its initial suspend point + assert(_Coro && "Can't call begin on moved-from generator"); + _Coro.resume(); + return iterator{_Gen_secret_tag{}, + ::std::coroutine_handle<_Gen_promise_base<_Gen_yield_t<_Ref>>>::from_address( + _Coro.address())}; + } + + [[nodiscard]] sentinel end() const noexcept { + return ::std::default_sentinel; + } + + private: + ::std::coroutine_handle _Coro = nullptr; + + explicit generator(_Gen_secret_tag, ::std::coroutine_handle _Coro_) noexcept + : _Coro(_Coro_) {} + }; +} // namespace DICE_TEMPLATELIBRARY_GENERATOR_NAMESPACE + +#endif // DICE_TEMPLATELIBRARY_GENERATOR_HPP diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0ba66b0..f912eb6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -52,3 +52,6 @@ custom_add_test(tests_tuple_algorithm) add_executable(tests_flex_array tests_flex_array.cpp) custom_add_test(tests_flex_array) + +add_executable(tests_generator tests_generator.cpp) +custom_add_test(tests_generator) diff --git a/tests/tests_generator.cpp b/tests/tests_generator.cpp new file mode 100644 index 0000000..6a5083a --- /dev/null +++ b/tests/tests_generator.cpp @@ -0,0 +1,379 @@ +//////////////////////////////////////////////////////////////// +// Tests adapted from reference implementation of std::generator proposal P2502R2 +// https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2502r2.pdf + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +TEST_SUITE("generator") { + using namespace DICE_TEMPLATELIBRARY_GENERATOR_NAMESPACE; + + /////////////////////// + // Simple non-nested serial generator + + generator fib(int max) { + auto a = 0, b = 1; + for (auto n = 0; n < max; n++) { + co_yield std::exchange(a, std::exchange(b, a + b)); + } + } + + generator other_generator(int i, int j) { + while (i != j) { + co_yield i++; + } + } + + ///////////////////// + // Demonstrate ability to yield nested sequences + // + // Need to use std::ranges::elements_of() to trigger yielding elements of + // nested sequence. + // + // Supports yielding same generator type (with efficient resumption for + // recursive cases) + // + // Also supports yielding any other range whose elements are convertible to + // the current generator's elements. + + generator nested_sequences_example() { + std::printf("yielding elements_of std::array\n"); +#if defined(__GNUC__) && !defined(__clang__) + co_yield ranges::elements_of(std::array{2, 4, 6, 8, 10}, {}); +#else + co_yield ranges::elements_of{std::array{2, 4, 6, 8, 10}}; +#endif + std::printf("yielding elements_of nested std::generator\n"); + co_yield ranges::elements_of{fib(10)}; + + std::printf("yielding elements_of other kind of generator\n"); + co_yield ranges::elements_of{other_generator(5, 8)}; + } + + ////////////////////////////////////// + // Following examples show difference between: + // + // If I co_yield a... + // X / X&& | X& | const X& + // ------------+------------+----------- + // - generator (same as generator) + // - generator ref | ref | ref + // - generator ref | ill-formed | ill-formed + // - generator ill-formed | ref | ill-formed + + struct X { + int id; + X(int id) : id(id) { + std::printf("X::X(%i)\n", id); + } + X(const X& x) : id(x.id) { + std::printf("X::X(copy %i)\n", id); + } + X(X&& x) : id(std::exchange(x.id, -1)) { + std::printf("X::X(move %i)\n", id); + } + ~X() { + std::printf("X::~X(%i)\n", id); + } + }; + + generator always_ref_example() { + co_yield X{1}; + { + X x{2}; + co_yield x; + assert(x.id == 2); + } + { + const X x{3}; + co_yield x; + assert(x.id == 3); + } + { + X x{4}; + co_yield std::move(x); + } + } + + generator xvalue_example() { + co_yield X{1}; + X x{2}; + co_yield x; // well-formed: generated element is copy of lvalue + assert(x.id == 2); + co_yield std::move(x); + } + + generator const_lvalue_example() { + co_yield X{1}; // OK + const X x{2}; + co_yield x; // OK + co_yield std::move(x); // OK: same as above + } + + generator lvalue_example() { + // co_yield X{1}; // ill-formed: prvalue -> non-const lvalue + X x{2}; + co_yield x; // OK + // co_yield std::move(x); // ill-formed: xvalue -> non-const lvalue + } + + /////////////////////////////////// + // These examples show different usages of reference/value_type + // template parameters + + // value_type = std::unique_ptr + // reference = std::unique_ptr&& + generator&&> unique_ints(const int high) { + for (auto i = 0; i < high; ++i) { + co_yield std::make_unique(i); + } + } + + // value_type = std::string_view + // reference = std::string_view&& + generator string_views() { + co_yield "foo"; + co_yield "bar"; + } + + // value_type = std::string + // reference = std::string_view + template + generator strings(std::allocator_arg_t, Allocator) { + co_yield {}; + co_yield "start"; + for (auto sv : string_views()) { + co_yield std::string{sv} + '!'; + } + co_yield "end"; + } + + // Resulting vector is deduced using ::value_type. + template + std::vector> to_vector(R&& r) { + std::vector> v; + for (auto&& x : r) { + v.emplace_back(static_cast(x)); + } + return v; + } + + // zip() algorithm produces a generator of tuples where + // the reference type is a tuple of references and + // the value type is a tuple of values. + template + generator...>, + std::tuple...>> + zip_impl(std::index_sequence, Rs... rs) { + std::tuple...> its{std::ranges::begin(rs)...}; + std::tuple...> itEnds{std::ranges::end(rs)...}; + while (((std::get(its) != std::get(itEnds)) && ...)) { + co_yield {*std::get(its)...}; + (++std::get(its), ...); + } + } + + template + generator...>, + std::tuple...>> + zip(Rs&&... rs) { + return zip_impl(std::index_sequence_for{}, std::views::all(std::forward(rs))...); + } + + template + struct stateful_allocator { + using value_type = T; + + int id; + + explicit stateful_allocator(int id) noexcept : id(id) {} + + template + stateful_allocator(const stateful_allocator& x) : id(x.id) {} + + T* allocate(std::size_t count) { + std::printf("stateful_allocator(%i).allocate(%zu)\n", id, count); + return std::allocator().allocate(count); + } + + void deallocate(T* ptr, std::size_t count) noexcept { + std::printf("stateful_allocator(%i).deallocate(%zu)\n", id, count); + std::allocator().deallocate(ptr, count); + } + + template + bool operator==(const stateful_allocator& x) const { + return this->id == x.id; + } + }; + + generator> stateless_example() { + co_yield 42; + } + + generator> stateless_example( + std::allocator_arg_t, std::allocator) { + co_yield 42; + } + + template + generator stateful_alloc_example(std::allocator_arg_t, Allocator) { + co_yield 42; + } + + struct member_coro { + generator f() const { + co_yield 42; + } + }; + + TEST_CASE("nested_sequences") { + std::vector const expected{2, 4, 6, 8, 10, 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 5, 6, 7}; + std::vector actual; + + for (auto x : nested_sequences_example()) { + actual.push_back(x); + } + + CHECK_EQ(actual, expected); + } + + TEST_CASE("by_value") { + SUBCASE("fib") { + std::vector const expected{0, 1, 1, 2, 3}; + std::vector actual; + + for (auto x : fib(5)) { + actual.push_back(x); + } + + CHECK_EQ(actual, expected); + } + + SUBCASE("seq") { + std::vector const expected{1, 2, 3, 4}; + std::vector actual; + + for (auto x : other_generator(1, 5)) { + actual.push_back(x); + } + + CHECK_EQ(actual, expected); + } + } + + TEST_CASE("always_ref") { + std::vector const expected{1, 2, 3, 4}; + std::vector actual; + + for (auto&& x : always_ref_example()) { + actual.push_back(x.id); + } + + CHECK_EQ(actual, expected); + } + + TEST_CASE("by_rvalue_ref") { + std::vector const expected{1, 2, 2}; + std::vector actual; + + for (auto&& x : xvalue_example()) { + static_assert(std::is_rvalue_reference_v); + static_assert(!std::is_const_v>); + actual.push_back(x.id); + } + + CHECK_EQ(actual, expected); + } + + TEST_CASE("by_const_ref") { + std::vector const expected{1, 2, 2}; + std::vector actual; + + for (auto&& x : const_lvalue_example()) { + static_assert(std::is_lvalue_reference_v); + static_assert(std::is_const_v>); + actual.push_back(x.id); + } + + CHECK_EQ(actual, expected); + } + + TEST_CASE("by_lvalue_ref") { + std::vector const expected{2}; + std::vector actual; + + for (auto&& x : lvalue_example()) { + static_assert(std::is_lvalue_reference_v); + static_assert(!std::is_const_v>); + actual.push_back(x.id); + } + + CHECK_EQ(actual, expected); + } + + TEST_CASE("value_type") { + SUBCASE("string_views") { + std::vector const s1_expected{"foo", "bar"}; + std::vector const s1_actual = to_vector(string_views()); + CHECK_EQ(s1_actual, s1_expected); + } + + SUBCASE("strings") { + std::vector const s2_expected{"", "start", "foo!", "bar!", "end"}; + std::vector const s2_actual = to_vector(strings(std::allocator_arg, std::allocator{})); + CHECK_EQ(s2_actual, s2_expected); + } + + SUBCASE("string tuples") { + std::vector> const s3_expected{{"", ""}, {"start", "start"}, {"foo!", "foo!"}, {"bar!", "bar!"}, {"end", "end"}}; + std::vector> const s3_actual = + to_vector(zip(strings(std::allocator_arg, std::allocator{}), strings(std::allocator_arg, std::allocator{}))); + + CHECK_EQ(s3_actual, s3_expected); + } + } + + TEST_CASE("move_only") { + std::vector const expected{0, 1, 2, 3, 4}; + std::vector actual; + + for (std::unique_ptr ptr : unique_ints(5)) { + actual.push_back(*ptr); + } + + CHECK_EQ(actual, expected); + } + + TEST_CASE("stateless_alloc") { + SUBCASE("a") { + auto g = stateless_example(); + CHECK_EQ(*g.begin(), 42); + } + + SUBCASE("b") { + auto g = stateless_example(std::allocator_arg, std::allocator{}); + CHECK_EQ(*g.begin(), 42); + } + } + + TEST_CASE("stateful_alloc") { + auto g = stateful_alloc_example(std::allocator_arg, stateful_allocator{42}); + CHECK_EQ(*g.begin(), 42); + } + + TEST_CASE("member_coro") { + member_coro m; + CHECK_EQ(*m.f().begin(), 42); + } +}