Skip to content

Conversation

@20162026
Copy link
Collaborator

@20162026 20162026 commented May 22, 2025

Alternative of #77 using separate namespace for freestanding implementation.

The idea is to have two publicly facing variants of inplace_vector: regular and freestanding-delete.
All mandatory functions should be moved into an internal-use-only parent struct inplace_vector_base, from which the public-facing structs are derived.
The regular inplace_vector will have all the freestanding-delete functions implemented.
The freestanding inplace_vector will have freestanding-delete functions marked as = delete.

moved to inplace_vector_base

[inplace.vector.cons], construct/copy/destroy

  • constexpr inplace_vector() noexcept;
  • constexpr inplace_vector(const inplace_vector&);
  • constexpr inplace_vector(inplace_vector&&);
  • constexpr ~inplace_vector();
  • constexpr inplace_vector& operator=(const inplace_vector& other);
  • constexpr inplace_vector& operator=(inplace_vector&& other);

iterators

  • constexpr iterator begin() noexcept;
  • constexpr const_iterator begin() const noexcept;
  • constexpr iterator end() noexcept;
  • constexpr const_iterator end() const noexcept;
  • constexpr reverse_iterator rbegin() noexcept;
  • constexpr const_reverse_iterator rbegin() const noexcept;
  • constexpr reverse_iterator rend() noexcept;
  • constexpr const_reverse_iterator rend() const noexcept;
  • constexpr const_iterator cbegin() const noexcept;
  • constexpr const_iterator cend() const noexcept;
  • constexpr const_reverse_iterator crbegin() const noexcept;
  • constexpr const_reverse_iterator crend() const noexcept;

[inplace.vector.capacity], size/capacity

  • constexpr bool empty() const noexcept;
  • constexpr size_type size() const noexcept;
  • static constexpr size_type max_size() noexcept;
  • static constexpr size_type capacity() noexcept;
  • static constexpr void shrink_to_fit() noexcept;

element access

  • constexpr reference operator[](size_type n);
  • constexpr const_reference operator[](size_type n) const;
  • constexpr reference front();
  • constexpr const_reference front() const;
  • constexpr reference back();
  • constexpr const_reference back() const;

[inplace.vector.data], data access

  • constexpr T* data() noexcept;
  • constexpr const T* data() const noexcept;

[inplace.vector.modifiers], modifiers

  • constexpr void pop_back();
  • constexpr pointer try_emplace_back(Args&&... args);
  • constexpr pointer try_push_back(const T& x);
  • constexpr pointer try_push_back(T&& x);
  • constexpr ranges::borrowed_iterator_t try_append_range(R&& rg);
  • constexpr reference unchecked_emplace_back(Args&&... args);
  • constexpr reference unchecked_push_back(const T& x);
  • constexpr reference unchecked_push_back(T&& x);
  • constexpr iterator erase(const_iterator position);
  • constexpr iterator erase(const_iterator first, const_iterator last);
  • constexpr void swap(inplace_vector& x)
  • constexpr void clear() noexcept;
  • constexpr friend bool operator==(const inplace_vector& x, const inplace_vector& y);
  • constexpr friend auto operator<=>(const inplace_vector& x, const inplace_vector& y);
  • constexpr friend void swap(inplace_vector& x, inplace_vector& y);

freestanding-deleted

[inplace.vector.cons], construct/copy/destroy

  • constexpr explicit inplace_vector(size_type n);
  • constexpr inplace_vector(size_type n, const T& value);
  • constexpr inplace_vector(InputIterator first, InputIterator last);
  • constexpr inplace_vector(from_range_t, R&& rg);
  • constexpr inplace_vector(initializer_list il);
  • constexpr inplace_vector& operator=(initializer_list);
  • constexpr void assign(InputIterator first, InputIterator last);
  • constexpr void assign_range(R&& rg);
  • constexpr void assign(size_type n, const T& u);
  • constexpr void assign(initializer_list il);

[inplace.vector.capacity], size/capacity

  • constexpr void resize(size_type sz);
  • constexpr void resize(size_type sz, const T& c);
  • static constexpr void reserve(size_type n);

element access

  • constexpr reference at(size_type n);
  • constexpr const_reference at(size_type n) const;

[inplace.vector.modifiers], modifiers

  • constexpr reference emplace_back(Args&&... args);
  • constexpr reference push_back(const T& x);
  • constexpr reference push_back(T&& x);
  • constexpr void append_range(R&& rg);
  • constexpr iterator emplace(const_iterator position, Args&&... args);
  • constexpr iterator insert(const_iterator position, const T& x);
  • constexpr iterator insert(const_iterator position, T&& x);
  • constexpr iterator insert(const_iterator position, size_type n, const T& x);
  • constexpr iterator insert(const_iterator position, InputIterator first, InputIterator last);
  • constexpr iterator insert_range(const_iterator position, R&& rg);
  • constexpr iterator insert(const_iterator position, initializer_list il);

@wusatosi
Copy link
Member

btw, current single-header-ness is not intentional, feel free to break the current header into multiple header files.

@20162026
Copy link
Collaborator Author

btw, current single-header-ness is not intentional, feel free to break the current header into multiple header files.

that would open up a whole new discussion on how we would want to split it :D and I would rather leave it for a different (refactoring) PR

As for this PR, I'm also tempted to add some convenience functions for the freestanding implementation (e.g., initializer_list constructor and from/to non-freestanding assignment operators). We could also add them under a CMake option that is off by default. But first, I would like to hear some feedback on whether this approach is suitable, or if there are any edge cases I haven't thought through...

@wusatosi
Copy link
Member

wusatosi commented May 22, 2025

As for this PR, I'm also tempted to add some convenience functions for the freestanding implementation (e.g., initializer_list constructor and from/to non-freestanding assignment operators). We could also add them under a CMake option that is off by default. But first, I would like to hear some feedback on whether this approach is suitable, or if there are any edge cases I haven't thought through...

Yeah, also there's no none-freestanding-delete insert operation...
We should definitively keep one in.

Edit: That will have to be throwing.

I say we should get the first minimal implementation started, we can then gradually decide which function our implementation want to keep, we definitely want to at least have "no freestanding-delete functions", "some freestanding-delete functions", "all freestanding-delete functions" as I think current "no freestanding-delete functions" are non-ergonomic.

@wusatosi
Copy link
Member

The problem is if we want to do this gradually, we might might-as-well consider continuing with the approach in #77 . So I think it will be helpful for us to scratch out the approach.

@20162026
Copy link
Collaborator Author

The problem is if we want to do this gradually, we might might-as-well consider continuing with the approach in #77 . So I think it will be helpful for us to scratch out the approach.

With the previous approach, having to globally choose either a freestanding or non-freestanding implementation is a dealbreaker for me. Of course, I could just fork and do whatever I want, but I would rather avoid it, especially with such a big changes that would be hard to sync.

I say we should get the first minimal implementation started, we can then gradually decide which function our implementation want to keep, we definitely want to at least have "no freestanding-delete functions", "some freestanding-delete functions", "all freestanding-delete functions" as I think current "no freestanding-delete functions" are non-ergonomic.

I guess it depends on what the end goal of a freestanding implementation is. For me, it's about getting compile time warnings when trying to use any potentially unsafe functions, but in theory there's more to freestanding than just that...

@wusatosi
Copy link
Member

wusatosi commented May 22, 2025

I mean if your intention is to have an inplace vector that does not have any actively throwing functions, you'll need to delete any functions that increase the size of the vector without checking the capacity in advance, as they're required to throw. Along with at.

Which I think is the current list of freestanding-deleted functions.

@wusatosi
Copy link
Member

If no throwing function is the intention, then having the standalone struct is actually a better idea. But I think if we are trying to play the line of "ah but this function has great utility, we should leave it in", we will have throwing functions otherwise the implementation will be non-compliance.

I think I will reverse my previous stance, I am for a separate struct that is in the freestanding namespace, and I am for the struct to delete all freestanding-delete functions without leaving in convenience functions.

@20162026
Copy link
Collaborator Author

I am for a separate struct that is in the freestanding namespace, and I am for the struct to delete all freestanding-delete functions without leaving in convenience functions

I'm also fine with deleting all of them. Convenience functions are definitely not mandatory.

@wusatosi
Copy link
Member

I'm also fine with deleting all of them. Convenience functions are definitely not mandatory.

Let's do that, if someone wants non-throwing convenience function in a restricted context they can disable exception.

@20162026 20162026 mentioned this pull request May 27, 2025
66 tasks
@20162026 20162026 marked this pull request as ready for review May 27, 2025 19:17
@20162026 20162026 requested review from a team and RaduNichita May 27, 2025 19:17
@wusatosi
Copy link
Member

Actually, I will just add testing here.

Copy link
Member

@JeffGarland JeffGarland left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to approve this PR as it looks like it's basically ready. The one big gaping omission here is an update to readme regarding the freestanding option. Of course that can be done in a separate PR so I won't hold this up for that.

I'm going to continue to point out that calling the freestanding implementation freestanding-deleted is backwards. The implementation is a freestanding implementation, not freestanding-deleted implementation. Yes, the net effect of a freestanding-implementation is to delete functions but the only time the standard applies 'freestanding-deleted' is in the context of a single function.

@wusatosi
Copy link
Member

I'm going to continue to point out that calling the freestanding implementation freestanding-deleted is backwards. The implementation is a freestanding implementation, not freestanding-deleted implementation. Yes, the net effect of a freestanding-implementation is to delete functions but the only time the standard applies 'freestanding-deleted' is in the context of a single function.

This is a valid point, what would you recommend us naming this?

Copy link
Member

@wusatosi wusatosi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you so much for you contribution.
This is fantastic.

Looking at the implementation, I don't think we need to test both structs.

The separation between freestanding hosted and freestanding deleted test suites would be helpful for us to make sure those extra function throws as expected, but not really useful within the context of this PR.

It will be great if you can add some docs work, but we can do that in another PR.

Let me know if you want me to just merge this @20162026 .

@wusatosi
Copy link
Member

Actually, I have a question, do the inplace_vector and freestanding::inplace_vector work with each other? e.g. on assignment, copy constructor, etc... I am more than happy to leave this behavior undefined, I just wonder if we need to guard against this at all.

@wusatosi wusatosi mentioned this pull request May 29, 2025
@20162026
Copy link
Collaborator Author

20162026 commented May 29, 2025

Actually, I have a question, do the inplace_vector and freestanding::inplace_vector work with each other? e.g. on assignment, copy constructor, etc... I am more than happy to leave this behavior undefined, I just wonder if we need to guard against this at all.

technically we could even allow conversion from one to another as there should be no problem as long as T and N are the same. Or just add some tests to ensue none of the tested compilers do something unexpected, e.g:

TYPED_TEST(Constructors, freestandingConversion) {
  using T = TestFixture::T;

  using IV = beman::inplace_vector<T, 5>;
  using FS = beman::freestanding::inplace_vector<T, 5>;

  static_assert(std::is_constructible_v<FS, FS>);
  static_assert(std::is_constructible_v<IV, IV>);

  static_assert(!std::is_constructible_v<IV, FS>);
  static_assert(!std::is_constructible_v<FS, IV>);
}

@wusatosi
Copy link
Member

Thanks for the doc work. I don't know if we want this, the fact that someone is mixing the two should be a code smell.

Copy link
Member

@wusatosi wusatosi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great job!

@20162026
Copy link
Collaborator Author

@wusatosi can we merge or should we wait for another review?

@wusatosi
Copy link
Member

@wusatosi can we merge or should we wait for another review?

Let's ship it

@wusatosi wusatosi merged commit b93683d into bemanproject:main May 31, 2025
29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants