diff --git a/include/beman/inplace_vector/inplace_vector.hpp b/include/beman/inplace_vector/inplace_vector.hpp index 70fbf8b..6e912cf 100644 --- a/include/beman/inplace_vector/inplace_vector.hpp +++ b/include/beman/inplace_vector/inplace_vector.hpp @@ -1,7 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception #include +#include #include +#include #include #include #include @@ -54,36 +56,68 @@ using inplace_vector_internal_size_type = If::max(), uint32_t, uint64_t>>>; +// array based storage is used so that we can satisfy constexpr requirement +// +// Selecting this storage type implies: std::is_trivial_v or Capacity = 0 +template +struct inplace_vector_type_based_storage { + using array_type = If, std::array, + std::array, Capacity>>; + array_type elems; + + constexpr T *begin() { return elems.data(); } + constexpr const T *begin() const { return elems.data(); } +}; + +// byte based storage is used for non-constexpr environment. Objects of type T +// are only constructed when needed and they don't even have to be default +// constructible. +// +// Selecting this storage type implies: !std::is_trivial_v and Capacity != 0 +template +struct inplace_vector_bytes_based_storage { + alignas(T) std::array elems; + + T *begin() { return std::launder(reinterpret_cast(elems.data())); } + const T *begin() const { + return std::launder(reinterpret_cast(elems.data())); + } +}; + // Base class for inplace_vector template struct inplace_vector_destruct_base { using size_type = std::size_t; using internal_size_type = inplace_vector_internal_size_type; + using internal_storage_type = + std::conditional_t or Capacity == 0, + inplace_vector_type_based_storage, + inplace_vector_bytes_based_storage>; - alignas(T) unsigned char elems[Capacity * sizeof(T)] = {}; + internal_storage_type elems; internal_size_type size_{0}; // [containers.sequences.inplace.vector.cons], construct/copy/destroy constexpr inplace_vector_destruct_base() = default; - inplace_vector_destruct_base( + constexpr inplace_vector_destruct_base( const inplace_vector_destruct_base &other) noexcept(std::is_nothrow_copy_constructible_v) : elems(), size_(other.size_) {} - inplace_vector_destruct_base( + constexpr inplace_vector_destruct_base( const inplace_vector_destruct_base &&other) noexcept(std::is_nothrow_move_constructible_v) - : elems(), size_(other.size()) {} + : elems(), size_(other.size_) {} - inplace_vector_destruct_base & + constexpr inplace_vector_destruct_base & operator=(const inplace_vector_destruct_base &other) noexcept( std::is_nothrow_copy_constructible_v && std::is_nothrow_copy_assignable_v) { size_ = other.size_; } - inplace_vector_destruct_base & + constexpr inplace_vector_destruct_base & operator=(const inplace_vector_destruct_base &&other) noexcept( std::is_nothrow_move_constructible_v && std::is_nothrow_move_assignable_v) { @@ -108,19 +142,18 @@ struct inplace_vector_base : public inplace_vector_destruct_base { // [containers.sequences.inplace.vector.cons], construct/copy/destroy constexpr inplace_vector_base() = default; - inplace_vector_base(const inplace_vector_base &other) noexcept( + constexpr inplace_vector_base(const inplace_vector_base &other) noexcept( std::is_nothrow_copy_constructible_v) - : inplace_vector_destruct_base(other.size) { + : inplace_vector_destruct_base(other.size_) { std::copy(other.begin(), other.end(), begin()); } - inplace_vector_base(inplace_vector_base &&other) noexcept( + constexpr inplace_vector_base(inplace_vector_base &&other) noexcept( Capacity == 0 || std::is_nothrow_move_constructible_v) : inplace_vector_destruct_base(other.size_) { std::copy(other.begin(), other.end(), begin()); - std::destroy(other.begin(), other.end()); - other.size_ = 0; } - inplace_vector_base &operator=(const inplace_vector_base &other) noexcept( + constexpr inplace_vector_base & + operator=(const inplace_vector_base &other) noexcept( std::is_nothrow_copy_constructible_v && std::is_nothrow_copy_assignable_v) { const auto diff = static_cast(other.size() - size()); @@ -141,7 +174,8 @@ struct inplace_vector_base : public inplace_vector_destruct_base { return *this; } - inplace_vector_base &operator=(inplace_vector_base &&other) noexcept( + constexpr inplace_vector_base & + operator=(inplace_vector_base &&other) noexcept( Capacity == 0 || (std::is_nothrow_move_constructible_v && std::is_nothrow_move_assignable_v)) { const auto diff = static_cast(other.size() - size()); @@ -151,13 +185,10 @@ struct inplace_vector_base : public inplace_vector_destruct_base { std::destroy(new_end, end()); // other size is grater than size } else { - std::move(other, other.begin(), other.begin() + size(), begin()); + std::move(other.begin(), other.begin() + size(), begin()); std::move(other.begin() + size(), other.end(), end()); } this->size_ = other.size(); - std::destroy(other.begin(), other.end()); - // reset size to zero - other.change_size(-static_cast(other.size())); return *this; } constexpr inplace_vector_base(const size_type size) @@ -173,28 +204,27 @@ struct inplace_vector_base : public inplace_vector_destruct_base { constexpr bool empty() const noexcept { return this->size_ == 0; } // [containers.sequences.inplace.vector.data], data access - constexpr T *data() noexcept { - return std::launder(reinterpret_cast(this->elems)); - } - constexpr const T *data() const noexcept { - return std::launder(reinterpret_cast(this->elems)); - } + constexpr T *data() noexcept { return this->elems.begin(); } + constexpr const T *data() const noexcept { return this->elems.begin(); } // [containers.sequences.inplace.vector.iterators] iterators - iterator begin() noexcept { - return std::launder(reinterpret_cast(this->elems)); + constexpr iterator begin() noexcept { + if constexpr (Capacity == 0) + return nullptr; + else + return this->elems.begin(); } - const_iterator begin() const noexcept { - return std::launder(reinterpret_cast(this->elems)); + constexpr const_iterator begin() const noexcept { + if constexpr (Capacity == 0) + return nullptr; + else + return this->elems.begin(); } - iterator end() noexcept { - return std::launder(reinterpret_cast(this->elems) + this->size()); - } + constexpr iterator end() noexcept { return begin() + this->size(); } - const_iterator end() const noexcept { - return std::launder(reinterpret_cast(this->elems) + - this->size()); + constexpr const_iterator end() const noexcept { + return begin() + this->size(); } // [containers.sequences.inplace.vector.modifiers], modifiers @@ -576,7 +606,8 @@ class inplace_vector : public inplace_vector_base { #else // Note: placement-new may not be constexpr friendly // Avoiding placement-new may allow inplace_vector to be constexpr friendly - auto final = ::new (end()) T(std::forward(args)...); + // cast to void* here to adapt to a const T + auto final = ::new ((void *)end()) T(std::forward(args)...); #endif this->change_size(1); return *final; diff --git a/tests/beman/inplace_vector/CMakeLists.txt b/tests/beman/inplace_vector/CMakeLists.txt index 0dc7351..223b230 100644 --- a/tests/beman/inplace_vector/CMakeLists.txt +++ b/tests/beman/inplace_vector/CMakeLists.txt @@ -9,3 +9,18 @@ add_executable(beman.inplace_vector.test inplace_vector.test.cpp) target_link_libraries(beman.inplace_vector.test PRIVATE beman.inplace_vector) add_test(NAME beman.inplace_vector.test COMMAND beman.inplace_vector.test) + +if(CMAKE_CXX_STANDARD GREATER_EQUAL 20) + # constexpr test + add_executable(beman.inplace_vector.constexpr_test constexpr.test.cpp) + target_link_libraries( + beman.inplace_vector.constexpr_test + PRIVATE beman.inplace_vector + ) + add_test( + NAME beman.inplace_vector.constexpr_test + COMMAND beman.inplace_vector.constexpr_test + ) +else() + message(WARNING "C++20 or later is not enabled, skipping constexpr test.") +endif() diff --git a/tests/beman/inplace_vector/constexpr.test.cpp b/tests/beman/inplace_vector/constexpr.test.cpp new file mode 100644 index 0000000..75e6830 --- /dev/null +++ b/tests/beman/inplace_vector/constexpr.test.cpp @@ -0,0 +1,278 @@ +#include +#include +#include +#include + +using namespace beman::inplace_vector; + +#define S_ASSERT(EXP) \ + do { \ + if (!(EXP)) { \ + return false; \ + } \ + } while (0) + +struct NonTrivial { + int z = 0; +}; +static_assert(!std::is_trivial_v); + +template constexpr bool test_empty_vec(T &vec) { + + // sizes + S_ASSERT(vec.max_size() == 0); + S_ASSERT(vec.capacity() == 0); + S_ASSERT(vec.size() == 0); + S_ASSERT(vec.empty()); + + // itr + S_ASSERT(vec.begin() == vec.end()); + S_ASSERT(vec.cbegin() == vec.cend()); + S_ASSERT(vec.rbegin() == vec.rend()); + S_ASSERT(vec.crbegin() == vec.crend()); + + // push_back + S_ASSERT(vec.try_push_back({}) == nullptr); + S_ASSERT(vec.try_emplace_back() == nullptr); + + return true; +} + +static_assert(std::invoke([]() { + inplace_vector vec; + test_empty_vec(vec); + return true; + }), + "0 capacity Trivial type"); + +static_assert(std::invoke([]() { + inplace_vector vec; + test_empty_vec(vec); + return true; + }), + "0 capacity Trivial const type"); + +static_assert(std::invoke([]() { + inplace_vector vec; + test_empty_vec(vec); + return true; + }), + "0 capacity Non-trivial type"); + +static_assert(std::invoke([]() { + inplace_vector vec; + test_empty_vec(vec); + return true; + }), + "0 capacity Non-trivial const type"); + +static_assert(std::invoke([]() { + // sizes + { + inplace_vector vec; + vec.push_back(1); + + S_ASSERT(vec.max_size() == 1); + S_ASSERT(vec.capacity() == 1); + S_ASSERT(vec.size() == 1); + S_ASSERT(!vec.empty()); + } + + // Access + { + + inplace_vector vec; + vec.push_back(1); + + S_ASSERT(vec[0] == 1); + S_ASSERT(vec.front() == 1); + S_ASSERT(vec.back() == 1); + } + + // forward itr + { + inplace_vector vec; + vec.push_back(1); + + auto itr = vec.begin(); + S_ASSERT(*itr == 1); + itr += 1; + S_ASSERT(itr == vec.end()); + } + + // backward itr + { + inplace_vector vec; + vec.push_back(1); + + auto itr = vec.rbegin(); + S_ASSERT(*itr == 1); + itr += 1; + S_ASSERT(itr == vec.rend()); + } + + // try variant + { + inplace_vector vec; + S_ASSERT(*vec.try_push_back(1) == 1); + S_ASSERT(vec.try_push_back(2) == nullptr); + } + + return true; + }), + "Single push_back"); + +static_assert(std::invoke([]() { + // push pop back + { + inplace_vector vec; + vec.push_back(1); + vec.pop_back(); + + S_ASSERT(vec.empty()); + } + + // resize + { + inplace_vector vec; + vec.push_back(1); + vec.push_back(2); + + S_ASSERT(vec.size() == 2); + + vec.resize(1); + S_ASSERT(vec.size() == 1); + S_ASSERT(vec.back() == 1); + } + + return true; + }), + "Basic mutation"); + +// [container.reqmts] General container requirements +using X = inplace_vector; + +constexpr bool reqmts_default() { + { + X u; + S_ASSERT(u.empty()); + } + { + X u = X(); + S_ASSERT(u.empty()); + } + + return true; +} + +static_assert(reqmts_default()); + +constexpr bool reqmts_copy() { + constexpr X exp{1, 2, 3}; + + X a{1, 2, 3}; + + { + X u(a); + S_ASSERT(std::ranges::equal(exp, u)); + S_ASSERT(std::ranges::equal(exp, a)); + } + { + X u = a; + S_ASSERT(std::ranges::equal(exp, u)); + S_ASSERT(std::ranges::equal(exp, a)); + } + + return true; +} + +static_assert(reqmts_copy()); + +constexpr bool reqmts_move() { + /* + * TODO: Need to keep in check with revision + * + * Move semantics + * A moved-from inplace_vector is left in a valid but unspecified state + * (option 3 below) unless T is trivially-copyable, in which case the size of + * the inplace_vector does not change (array semantics, option 2 below). That + * is: + * + * inplace_vector a(10); + * inplace_vector b(std::move(a)); + * assert(a.size() == 10); // MAY FAIL + * + * moves a's elements element-wise into b, and afterwards the size of the + * moved-from inplace_vector may have changed. + * + * This prevents code from relying on the size staying the same (and therefore + * being incompatible with changing an inplace_vector type back to vector) + * without incuring the cost of having to clear the inplace_vector. + * + * When T is trivially-copyable, array semantics are used to provide trivial + * move operations. + */ + + { + constexpr X exp{1, 2, 3}; + X mov_from(exp); + X u(std::move(mov_from)); + S_ASSERT(std::ranges::equal(exp, u)); + + static_assert(std::is_trivially_copyable_v); + S_ASSERT(mov_from.size() == exp.size()); + } + { + // Note(river): for later non-trivial type implementation, verify: + // Effects: All existing elements of a are either move assigned to or + // destroyed. + constexpr X origin{1, 2, 3}; + constexpr X exp{1, 2}; + + X a(origin); + X mov_from(exp); + a = std::move(mov_from); + + S_ASSERT(std::ranges::equal(exp, a)); + static_assert(std::is_trivially_copyable_v); + S_ASSERT(mov_from.size() == exp.size()); + } + + return true; +} + +static_assert(reqmts_move()); + +// destructor implicilty tested + +constexpr bool reqmts_itr() { + constexpr X exp{1, 2, 3}; + + { + X b = exp; + auto beg = b.begin(); + auto end = b.end(); + end--; + + S_ASSERT(*b.begin() == 1); + S_ASSERT(*end == 3); + } + + { + X b = exp; + auto beg = b.cbegin(); + auto end = b.cend(); + end--; + + S_ASSERT(*b.begin() == 1); + S_ASSERT(*end == 3); + } + + return true; +} + +static_assert(reqmts_itr()); + +int main() { + // compile means pass +} diff --git a/tests/beman/inplace_vector/inplace_vector.test.cpp b/tests/beman/inplace_vector/inplace_vector.test.cpp index dd09737..f6d2e17 100644 --- a/tests/beman/inplace_vector/inplace_vector.test.cpp +++ b/tests/beman/inplace_vector/inplace_vector.test.cpp @@ -1,8 +1,16 @@ // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception #include +#include using namespace beman::inplace_vector; +struct NonTrivial { + int z = 5; + + bool operator==(NonTrivial const &other) const { return this->z == other.z; } +}; +static_assert(!std::is_trivial_v); + template constexpr void test() { using vec = inplace_vector; vec range{T(1), T(1337), T(42), T(12), T(0), T(-1)}; @@ -82,8 +90,19 @@ void test_exceptions() { } } } + +template void test_const() { + inplace_vector vec; + vec.push_back({50}); + const T &first = vec.front(); + assert(first == T{50}); +} + int main() { test(); test_exceptions(); + + test_const(); + test_const(); return 0; }