Date: Fri, 21 Feb 2025 10:32:29 +0000
On Fri, 21 Feb 2025 at 01:01, Jeremy Rifkin via Std-Proposals <
std-proposals_at_[hidden]> wrote:
> Hi,
> I can't speak to the original design of std::stack and I also don't see
> prior discussion of this right off hand. But I have some thoughts:
>
> > Is there a reason std::stack does not provide a standard iterator
>
> I presume for the same reason std::stack does not provide access to
> anything other than the top element. As a data structure, a stack by design
> only provides access and operations on the top element. If you need access
> to anything else, what you want isn't a stack.
>
Right. std::stack is a *very* simple adaptor, which takes a sequence
container and then adapts it to provide a very simple push/pop/top API. 90%
of the std::stack interface is constructor overloads. The actual LIFO logic
is trivial, because the API is so simple.
If you want a different API, it would be trivial to define that and use
that API instead of std::stack. Don't complicate std::stack.
>
> > and for that matter, an *input iterator* (or what would have been a
> legacy input iterator)? Is it because it would be too confusing?
> > ...
> > *An input iterator does not guarantee multiple passes*, so that should
> avoid surprises—the stack is empty after iterating from beginning to end
>
> I for one would be quite confused by this, at least if this were provided
> as .begin()/.end()/std::begin/std::end and stack<T,
> C>::iterator. std::find(pi.begin(), pi.end(), 3); is a great example of
> something that would be extraordinarily confusing. I can't think of any
> other container where iterating mutates. If there were some separate
> std::stack_input_iterator, that would be more reasonable, but then the
> question just comes down to usefulness.
>
Right, it shouldn't be a container, it should be a model of
std::ranges::input_range, with an input iterator. It's fine for an input
range to be single pass such that elements are discarded once they've been
iterated past.
But then it's not really a stack, is it? It's just an input range. And
almost any range can be treated as an input range. To be a stack, you need
to be able to push more items onto the stack, and the iterator would only
support popping not pushing. Unless it was also an output iterator, and
assigning through the iterator would push a new element, rather than modify
the current element, which seems like it could also be confusing. I think I
would want to use the stack object itself for pushing, not do that via the
iterator.
But if what you want is an input iterator that pops from a stack as you
iterate, maybe you just want something like:
#include <stack>
#include <ranges>
template<typename Stack>
requires requires (Stack& s) {
{ s.top() } -> std::same_as<typename Stack::reference>;
{ s.pop() } ;
{ s.empty() } -> std::convertible_to<bool>;
{ s.size() } -> std::same_as<typename Stack::size_type>;
}
struct stack_iterator
{
using iterator_concept = std::input_iterator_tag;
using value_type = typename Stack::value_type;
using difference_type = std::ptrdiff_t;
explicit
stack_iterator(Stack& s) : s(std::addressof(s)) { }
auto&& operator*() const noexcept { return s->top(); }
stack_iterator& operator++() noexcept { s->pop(); return *this; }
void operator++(int) noexcept { s->pop(); }
bool operator==(std::default_sentinel_t) const noexcept { return
s->empty(); }
friend difference_type operator-(std::default_sentinel_t, stack_iterator
i)
{ return i.s->size(); }
friend difference_type operator-(stack_iterator i,
std::default_sentinel_t s)
{ return -(s - i); }
private:
Stack* s;
};
template<typename Stack>
using lifo_range
= std::ranges::subrange<stack_iterator<Stack>, std::default_sentinel_t>;
static_assert( std::ranges::input_range<lifo_range<std::stack<int>>> );
static_assert( std::ranges::sized_range<lifo_range<std::stack<int>>> );
Is this worth adding to the standard? Probably not IMHO. It's not difficult
to write, and it's probably not useful to a large enough number of people
to be worth standardizing.
std-proposals_at_[hidden]> wrote:
> Hi,
> I can't speak to the original design of std::stack and I also don't see
> prior discussion of this right off hand. But I have some thoughts:
>
> > Is there a reason std::stack does not provide a standard iterator
>
> I presume for the same reason std::stack does not provide access to
> anything other than the top element. As a data structure, a stack by design
> only provides access and operations on the top element. If you need access
> to anything else, what you want isn't a stack.
>
Right. std::stack is a *very* simple adaptor, which takes a sequence
container and then adapts it to provide a very simple push/pop/top API. 90%
of the std::stack interface is constructor overloads. The actual LIFO logic
is trivial, because the API is so simple.
If you want a different API, it would be trivial to define that and use
that API instead of std::stack. Don't complicate std::stack.
>
> > and for that matter, an *input iterator* (or what would have been a
> legacy input iterator)? Is it because it would be too confusing?
> > ...
> > *An input iterator does not guarantee multiple passes*, so that should
> avoid surprises—the stack is empty after iterating from beginning to end
>
> I for one would be quite confused by this, at least if this were provided
> as .begin()/.end()/std::begin/std::end and stack<T,
> C>::iterator. std::find(pi.begin(), pi.end(), 3); is a great example of
> something that would be extraordinarily confusing. I can't think of any
> other container where iterating mutates. If there were some separate
> std::stack_input_iterator, that would be more reasonable, but then the
> question just comes down to usefulness.
>
Right, it shouldn't be a container, it should be a model of
std::ranges::input_range, with an input iterator. It's fine for an input
range to be single pass such that elements are discarded once they've been
iterated past.
But then it's not really a stack, is it? It's just an input range. And
almost any range can be treated as an input range. To be a stack, you need
to be able to push more items onto the stack, and the iterator would only
support popping not pushing. Unless it was also an output iterator, and
assigning through the iterator would push a new element, rather than modify
the current element, which seems like it could also be confusing. I think I
would want to use the stack object itself for pushing, not do that via the
iterator.
But if what you want is an input iterator that pops from a stack as you
iterate, maybe you just want something like:
#include <stack>
#include <ranges>
template<typename Stack>
requires requires (Stack& s) {
{ s.top() } -> std::same_as<typename Stack::reference>;
{ s.pop() } ;
{ s.empty() } -> std::convertible_to<bool>;
{ s.size() } -> std::same_as<typename Stack::size_type>;
}
struct stack_iterator
{
using iterator_concept = std::input_iterator_tag;
using value_type = typename Stack::value_type;
using difference_type = std::ptrdiff_t;
explicit
stack_iterator(Stack& s) : s(std::addressof(s)) { }
auto&& operator*() const noexcept { return s->top(); }
stack_iterator& operator++() noexcept { s->pop(); return *this; }
void operator++(int) noexcept { s->pop(); }
bool operator==(std::default_sentinel_t) const noexcept { return
s->empty(); }
friend difference_type operator-(std::default_sentinel_t, stack_iterator
i)
{ return i.s->size(); }
friend difference_type operator-(stack_iterator i,
std::default_sentinel_t s)
{ return -(s - i); }
private:
Stack* s;
};
template<typename Stack>
using lifo_range
= std::ranges::subrange<stack_iterator<Stack>, std::default_sentinel_t>;
static_assert( std::ranges::input_range<lifo_range<std::stack<int>>> );
static_assert( std::ranges::sized_range<lifo_range<std::stack<int>>> );
Is this worth adding to the standard? Probably not IMHO. It's not difficult
to write, and it's probably not useful to a large enough number of people
to be worth standardizing.
Received on 2025-02-21 10:32:46