4
\$\begingroup\$

I just learned Adapter Pattern, and I have used STL for some time. I know that the std::stack is a adapter for sequence container (say, vector, list, etc,). So I try to implement a naive stack alike class. Here is the code snippet below.

How to make it better? I have three main concerns.

For

template <typename T, class Container=std::vector<T>>  
void test_mulit_lower_container()
  1. How to limit T to numeric type and the Container to sequence container?
  2. how to make the type of Container is depend on the type of T?

About unique pointer

  1. Comparing use an instance of container(say, make the said instance as a member variable of stack, but it seems impossible because the stack have many different kinds of ctors, so the lower container should be created dynamically), is it the right choice to use the unique_ptr?
#include <vector>
#include <memory>
#include <iostream>
#include <list>
#include <deque>

template<class T, class Container=std::vector<T>> 
class AdapterForStack
{
    public:
        using value_type = T;

        AdapterForStack()
        {
            m_continer_ptr = std::unique_ptr<Container>(new Container());
        }

        bool empty()
        {
            return m_continer_ptr->empty();
        }

        size_t size()
        {
            return m_continer_ptr->size();
        }

        void push( const value_type& value )
        {
            m_continer_ptr->insert(m_continer_ptr->begin(), value);
            return;
        }

        void push(value_type&& value )
        {
            m_continer_ptr->push_front(std::move(value));
            return;
        }

        const value_type& top()
        {
            return m_continer_ptr->front();
        }

        void pop()
        {
            m_continer_ptr->erase(m_continer_ptr->begin());
            return;
        }

    private:
        std::unique_ptr<Container> m_continer_ptr;
};

template <typename T, class Container=std::vector<T>>  //How to make it better?
void test_mulit_lower_container() //1. limit T to numberic type and the cont to seq cont
{                           //    2. how to make the type of Container is depend on the type of T?
    {
        AdapterForStack<T, std::list<T>> stack{};

        T default_val{};
        for(int i=0; i<9; i++, default_val++)
        {
            stack.push(default_val);
        }
        
        while(!stack.empty())
        {
            std::cout << stack.top() << std::endl;
            stack.pop();
        } 
    }
}

int main()
{
    #if 1
        std::cout << "underline type is vector" << std::endl;
        test_mulit_lower_container<int, std::vector<int>>();

        std::cout << "underline type is list" << std::endl;
        test_mulit_lower_container<int, std::list<int>>();

        std::cout << "underline type is deque" << std::endl;
        test_mulit_lower_container<int, std::deque<int>>();
    #else
    std::cout << "lower type is vector" << std::endl;
    {
        AdapterForStack<int> stack{};

        for(int i=0; i<9; i++)
        {
            stack.push(i);
        }
        
        while(!stack.empty())
        {
            std::cout << stack.top() << std::endl;
            stack.pop();
        }
    }

    std::cout << "lower type is list" << std::endl;
    {
        AdapterForStack<int, std::list<int>> stack{};

        for(int i=0; i<9; i++)
        {
            stack.push(i);
        }
        
        while(!stack.empty())
        {
            std::cout << stack.top() << std::endl;
            stack.pop();
        } 
    }

    std::cout << "lower type is deque" << std::endl;
    {
        AdapterForStack<int, std::deque<int>> stack{};

        for(int i=0; i<9; i++)
        {
            stack.push(i);
        }
        
        while(!stack.empty())
        {
            std::cout << stack.top() << std::endl;
            stack.pop();
        } 
    }
    #endif
}
\$\endgroup\$
0

2 Answers 2

9
\$\begingroup\$

Design review

So, basically, you would like to remake std::stack. That’s cool, it’s not a bad learning project at all. It is actually possible to make a much better version of std::stack, for a number of reasons (mostly because std::stack is so old).

The std::stack interface could be improved

You have gone with a very minimal stack interface, which is fine. It’s pretty close to what std::stack has, too. But do keep in mind that this is a very restrictive interface. For example, you have made it possible to put non-copyable, move-only types into the stack… but there is no way to get them back out. You can view them (with top()). And you can remove them (with pop()). But you can’t retrieve them.

See for yourself. We’ll use std::unique_ptr, because that is a standard move-only type:

// First we create a unique pointer.
auto p = std::unique_ptr<std::string>(new std::string{"some text"});

// Now we create the stack:
auto stack = AdapterForStack<std::unique_ptr<std::string>>{};

// Now we put the pointer in the stack (this technically won't work for other
// reasons I'll get to shortly, but it *should* work):
stack.push(std::move(p));

// We can *view* the pointer:
std::cout << *(stack.top()) << '\n';

// But there is no possible way to *retrieve* the pointer:
auto q = stack.top(); // won't work, non-copyable
auto q = std::move(stack.top());    // won't work, can't move from const

With std::stack, it is possible to retrieve the pointer because std::stack has two overloads of top()… one that returns const&, and one that just returns &. If you get the non-const reference, you can move from that. So it “works”, but leaves the top element of the stack in an indeterminate, moved-from state… which is not good.

If std::stack were designed today, it might have a different interface. Instead of pop() returning void, it would probably pop the top object and return it. This just makes sense:

// the obvious way to push something onto the stack
stack.push(something);

// the obvious way to pop something off the stack
auto something = stack.pop();

(It is also how the proposed concurrent queue would work.)

Anywho, if all you’re aiming to do is replicate the std::stack interface, that’s fine. But do keep in mind that the std::stack interface is not great. It is as ancient as the STL (which predates the standard library). It was good back then… not so much now.

Which end of the stack?

Your stack works by appending to elements to the front of the stack. That is… peculiar. Most stacks work by appending to the end of the stack. This includes std::stack.

And, in fact, you have a bug. You didn’t fully test your stack. Or rather, your testing code is broken.

To illustrate the bug, let’s first fix the broken testing code. In test_mulit_lower_container(), you take the container type as a template parameter Container. However… look at how you actually construct the stack:

        AdapterForStack<T, std::list<T>> stack{};

You see? You don’t use Container. You have hard-coded std::list<T>.

So first we have to fix that to:

        AdapterForStack<T, Container> stack{};

Now your testing code is actually testing different container types. You can compile and run it to make sure everything is working.

To illustrate the bug, just change the push line from:

            stack.push(default_val);

to:

            stack.push(std::move(default_val));

If you try to compile the code now, it will fail.

What went wrong? Well, when we were using stack.push(default_val), we were calling the lvalue overload of push():

        void push( const value_type& value )
        {
            m_continer_ptr->insert(m_continer_ptr->begin(), value);
            return;
        }

But when we switched to stack.push(std::move(default_val)), we are now calling the rvalue overload of push():

        void push(value_type&& value )
        {
            m_continer_ptr->push_front(std::move(value));
            return;
        }

See the difference? The lvalue overload calls .insert(). The rvalue overload calls push_front().

The problem is… std::vector does not have .push_front(). Thus, compile error.

This is (part of) why std::stack puts elements at the end of the container. That way it can use .push_back() and .pop_back(), which are supported by std::vector, std::deque, and std::list.

If you are going to push elements to the front, that’s okay (although, it will be very slow for std::vector, and it makes many other operations more complicated). However, you can’t use .push_front() and .pop_front(), because only std::deque and std::list support those… not std::vector.

You could use .insert() and .erase(), as you do.

So, you need to make a decision. Either:

  1. Change to pushing/popping from the back. That way you can use .push_back() and .pop_back(), and get maximum performance when using std::vector as the underlying container; … or…
  2. Don’t use .push_front(), and only use .insert() and .erase(), and accept that performance with a std::vector will be terrible.

Questions

How to limit T to numeric type and the Container to sequence container?

So that’s two questions. Let’s tackle them one at a time.

First, what do you mean by “numeric type”? I mean, I assume you mean stuff like int, double, and other built-in “math” types. You probably also mean types that do modular arithmetic, like unsigned int. But do you also mean std::complex? Do you also mean third-party “bignum” types?

Let’s start simple. If you want to constrain to integers, there is the concept std::integral. For floating point numbers, there is std::floating_point. So, a first attempt would be:

template <typename T>
concept number = std::integral<T> or std::floating_point<T>;

Except there’s a catch, because std::integral also includes things that are not numbers, like bool, and character types.

So we want something more like this:

template <typename T>
concept character =
    std::same_as<T, char>
    or std::same_as<T, char8_t>
    or std::same_as<T, char16_t>
    or std::same_as<T, char32_t>
    or std::same_as<T, wchar_t>
;

template <typename T>
concept number =
    (
        std::integral<T>
        and (not std::same_as<T, bool>)
        and (not character<T>)
    )
    or std::floating_point<T>
;

If you want to extend that to support other numeric types like std::complex or “bignum” types, it’s not hard, but I’ll leave that as an exercise for the reader.

With that concept, you’d just constrain your template like this:

template <number T, class Container=std::vector<T>> 
class AdapterForStack

Now, for the second part of the question. This is going to be even trickier than the first part. As before, there is no single concept that defines containers. A container is a range… but not all ranges are containers. Nevertheless, a range is a good start:

template <typename T>
concept container = std::ranges::range<T>;

Now, a container is something more than a “pure”, abstract range… especially a container that you can use as a stack. Looking at the container hierarchy, we need at least to be able to read and write (so at least a mutable input iterator), but we need multiple passes (it won’t do if we can only read what we put in the stack once… what would happen if you call top() twice?), so we need at least a mutable forward iterator.

So instead of a plain range, we can require at least a forward range:

template <typename T>
concept container = std::ranges::forward_range<T>;

(As of C++23, you could also test not std::ranges::constant_range<T> to ensure the range is mutable.)

Now, a container, by definition, owns its elements. That’s what differentiates a container from a view. Because it owns its elements, it can’t also be borrowing them. Therefore we can check not std::ranges::borrowed_range<T>. While we’re at it, we could also check that it’s not a view:

template <typename T>
concept container =
    std::ranges::forward_range<T>
    and not std::ranges::borrowed_range<T>
    and not std::ranges::view<T>
;

Finally, if you are going to be requiring that the container supports .insert() and .erase() (or .push_front() or .push_back() or whatever you are going to be using), you can check for that, too:

template <typename T>
concept container =
    std::ranges::forward_range<T>
    and not std::ranges::borrowed_range<T>
    and not std::ranges::view<T>
    and requires(
        T& container,
        std::ranges::iterator_t<T> i,
        std::ranges::range_value_t<T> const& lv,
        std::ranges::range_value_t<T>&& rv
    )
    {
        container.insert(i, lv);
        container.insert(i, rv);
        container.erase(i);
    }
;

Something like that.

And with that, you’d do:

template <
    number T,
    container Container = std::vector<T>
>
class AdapterForStack

Note though, while we have pretty much confirmed it’s a container, we never actually checked whether it’s a sequence container. I’m not sure how to go about that off the top of my head. That’s something you can look into on your own.

how to make the type of Container is depend on the type of T?

Now, if you ask this question to a large enough pool of C++ experts, you’ll probably get advice about using template templates (that is not a typo, “template templates” are a thing.) This is bad advice. It will “work” for most containers, but it is a hack.

The correct way to do this constraint is to use something like:

std::same_as<T, std::ranges::range_value_t<Container>>

Which you would use like:

template <
    number T,
    container Container = std::vector<T>
>
requires std::same_as<T, std::ranges::range_value_t<Container>>
class AdapterForStack

Easy peasy.

About unique pointer

Yes, about unique pointer. I have to admit, I don’t understand what your concern is with your question:

Comparing use an instance of an instance of container(say, make the said instance as an object, but it seems impossible because the stack have many different kinds of ctors, so the lower container should be created dynamically), is it the right choice to use the unique_ptr?

This reads like nonsense to me. “Impossible because the stack may have many different kinds of constructors”? What does that even mean? The stack can have as many constructors as it pleases with no bearing on the on the underlying container:

template<class T, class Container = std::vector<T>> 
class AdapterForStack
{
    Container _container;

public:
    // a default constructor:
    AdapterForStack()
        // just default construct the underlying container
        : _container{}
    {}

    // an iterator pair constructor:
    template <std::input_iterator I, std::sentinel_for<I> S>
    AdapterForStack(I first, S last)
        // either: if the underlying container supports iterator pair construction:
        : _container(first, last)
    {}
        // or: if it doesn't:
        : _container{}
    {
        std::ranges::copy(first, last, std::back_inserter(_container));
    }

    // a range constructor:
    template <std::ranges::input_range R>
    AdapterForStack(R&& rng)
    {
        std::ranges::copy(std::forward<R>(rng), std::back_inserter(_container));
    }

    // and just for fun, a constructor from 3 T values:
    AdapterForStack(T a, T b, T c)
    {
        _container.push_back(std::move(a));
        _container.push_back(std::move(b));
        _container.push_back(std::move(c));
    }

    // how about a constructor that fills the stack with n copies of v?
    AdapterForStack(std::size_t n, T v)
    {
        std::ranges::fill_n(std::back_inserter(_container), n, v);
    }

    // and so on...

What is impossible here?

You could even automatically use the underlying container constructors if supported, and fall back on a default if not:

    // container can be constructed from iterator pair
    template <std::input_iterator I, std::sentinel_for<I> S>
    requires std::constructible_from<Container, I, S>
    AdapterForStack(I first, S last)
        : _container(first, last)
    {}

    // container can *not* be constructed from iterator pair
    template <std::input_iterator I, std::sentinel_for<I> S>
    AdapterForStack(I first, S last)
        : _container{}
    {
        std::ranges::copy(first, last, std::back_inserter(_container));
    }

There is no need for std::unique_ptr or for any kind of dynamic allocation. Just try it and see.

Code review

#include <vector>
#include <memory>
#include <iostream>
#include <list>
#include <deque>

It is good practice to put includes in a logical order; alphabetical is usually a good idea.

template<class T, class Container=std::vector<T>> 
class AdapterForStack

“Adapter for stack” is a bit of a clunky name. “Stack adapter” is nicer, but even just “stack” gets the message across. “Adapter” is the design pattern the class uses, but that information is not really useful for the person using it. All they care about is that it’s a stack.

Put another way, suppose I see auto x = AdapterForStack<int>{};. What do I care about here? What do I need to know to use x? Do I care that this is an adapter? No. Do I care that it is a stack? Yes. That tells me that x is a container that I can probably push and pop to. That’s useful information.

Also, you should always put stuff in a namespace; it is bad form to pollute the global namespace.

template<class T, class Container=std::vector<T>> 
class AdapterForStack
{
    public:
        using value_type = T;

        AdapterForStack()
        {
            m_continer_ptr = std::unique_ptr<Container>(new Container());
        }

There is just too much indentation going on here. It makes sense to indent the contents of a class, so you can see what stuff is in the class and what stuff is not. But there is no point in double indenting every method, member, and type. Something more like:

template<class T, class Container=std::vector<T>> 
class AdapterForStack
{
public:
    using value_type = T;

    AdapterForStack()
    {
        m_continer_ptr = std::unique_ptr<Container>(new Container());
    }

wastes a lot less space, and loses no information (because the public: and private: must always be in a class, so there is no need to signal that they are in a class by indenting them).

        AdapterForStack()
        {
            m_continer_ptr = std::unique_ptr<Container>(new Container());
        }

The less code you write, and the more you leave to be auto-generated by the compiler, the better.

The only reason you need this default constructor is because you are (unnecessarily) using a pointer to hold a dynamically-allocated instance of the container. If you got rid of the dynamic allocation and just used the container directly as a member, then you don’t need this constructor. You could just write:

    AdapterForStack() = default;

You could also add constexpr and conditional noexcept with:

    constexpr AdapterForStack() noexcept(std::is_nothrow_default_constructible_v<Container>) = default;

Technically, you could write nothing at all, and get the default constructor automatically. However, you probably want other constructors, in which case you’ll need the above.

Speaking of, you really do need some more constructors. Even if you want to ignore allocators, which is fine, you at least want to be able to construct a stack with some pre-existing content, either using an iterator pair, an initializer list, or a range (or all of the above).

It’s also worth mentioning that with your current design, using a std::unique_ptr, your stack is non-copyable. But there is no reason that need be the case. If you use the container directly as a data member, the stack will be copyable if the underlying container is copyable. (And the same for movability.)

        bool empty()
        {
            return m_continer_ptr->empty();
        }

A couple things here.

First, there is no reason this function should ever fail. Either the stack is empty or it is not. There are no other possible logical options. So this function should be marked noexcept.

It should also be marked const, because asking if the stack is empty should never change the stack.

And of course, everything should be constexpr by default.

So:

constexpr auto empty() const noexcept -> bool
{
    return m_continer_ptr->empty();
}

However, it’s worth noting that if you are going to use a pointer, you should be more responsible, and check the pointer before dereferencing it. You don’t need to; the only way you could ever get a null pointer is if you move the stack:

auto s1 = AdapterForStack<int>{};
// s1.m_continer_ptr is always valid here

auto s2 = std::move(s1);
// s1.m_continer_ptr is no longer valid

s1.empty(); // NULL POINTER DEREFERENCE!!!

So you could just say it’s an error to do anything with a moved-from stack. That’s how pretty much everything in the standard library works. It would make your stack more dangerous, but more efficient. So you need to weigh the consequences.

This problem goes away if you just don’t use dynamic allocation, and instead put the underlying container directly in the stack.

        size_t size()
        {
            return m_continer_ptr->size();
        }

Everything for empty() applies here, too. But also, it should be std::size_t. size_t works for historical reasons… but it will not work in future C++ code. So get in the habit of doing std::size_t (and including one of the required headers).

But there is one more complication: the size_type of the underlying container might not be std::size_t.

The correct thing to do is:

template<
    class T,
    class Container = std::vector<T>
>
class AdapterForStack
{
public:
    using value_type = T; // could also do std::ranges::range_value_t<Container>;
    using size_type = std::ranges::range_size_t<Container>;

    // ...

    constexpr auto size() const noexcept -> size_type
    {
        return m_continer_ptr->size();
    }

OR, just let everything be deduced:

template<
    class T,
    class Container = std::vector<T>
>
class AdapterForStack
{
public:
    using value_type = T;

    // ...

    constexpr auto size() const noexcept
    {
        return m_continer_ptr->size();
    }

Either way is fine.

        void push( const value_type& value )
        {
            m_continer_ptr->insert(m_continer_ptr->begin(), value);
            return;
        }

        void push(value_type&& value )
        {
            m_continer_ptr->push_front(std::move(value));
            return;
        }

You don’t need the return; statements here. They add nothing to the code but clutter.

As mentioned in the design section, you have a bug here. In the rvalue overload of push(), you use push_front()… but std::vector has no push_front(). So you should do this:

    constexpr auto push(value_type const& value)
    {
        m_continer_ptr->insert(m_continer_ptr->begin(), value);
    }

    constexpr auto push(value_type&& value)
    {
        m_continer_ptr->insert(m_continer_ptr->begin(), std::move(value));
    }

However… do you really want to insert new stack elements to the front of the underlying container?

Consider that there are basically 3 main underlying container types you want to support: std::vector, std::deque, and std::list. Only std::deque and std::list support push_front() and pop_front(). But all three support push_back() and pop_back(). (See the function table for the standard containers.)

There’s more. While you can push to the front of a vector (which you do with m_continer_ptr->insert(m_continer_ptr->begin(), value)), doing so can be incredibly expensive. To push to the front of a vector requires moving every single element in the vector, every time. Pushing to the back of a vector is very fast, though. (Pushing to the front or back of a std::deque or std::list is fast. std::vector is the only one the three where there is a difference.)

There’s still more. Eventually, you’ll probably want to be able to support filling a stack from a sequence, either an iterator pair or a range, with something like: auto stack = AdapterForStack<int>{0, 1, 2, 3, 4, 5, 6, 7, 8};. If you put each element at the front of the underyling container, then you would have to write:

    constexpr AdapterForStack(std::initializer_list<T> il)
    {
        std::ranges::copy(il | std::views::reverse, std::inserter(_container, std::ranges::begin(_container)));
    }

    // or:

    constexpr AdapterForStack(std::initializer_list<T> il)
    {
        std::ranges::for_each(std::views::all(il), [this] (auto&& v) { this->push(v); });
    }

In other words, if you want the last element of the input range to be the first element of the underlying container—which would be the top of the stack—you need to copy the input range into the container backwards. So when the input range is {0, 1, 2, 3, 4, 5, 6, 7, 8}, the container contents will be {8, 7, 6, 5, 4, 3, 2, 1, 0}.

This is… not great. Copying backwards is a lot of work.

If, instead, the top of the stack was at the back of the underlying container, then you could just do a straight-up copy:

    constexpr AdapterForStack(std::initializer_list<T> il)
    {
        std::ranges::copy(std::views::all(il), std::inserter(_container, std::ranges::end(_container)));
    }

    // or possibly even just:

    constexpr AdapterForStack(std::initializer_list<T> il)
        : _container(il)
    {}

This could be much faster, because it might just be a big-ass block transfer (aka memcpy()).

So there are a lot of very good reasons to have the stack grow to the back of the underlying container, rather than at the front.

        std::unique_ptr<Container> m_continer_ptr;

I think you’ve misspelled “container”.

Extension

Constraining for sequence containers

To borrow from that classic “Jurassic Park” quote, before we become too preoccupied with whether we can constrain for sequence containers, we should stop to think if we should.

For the underlying container of a stack, we don’t actually need a sequence container. “Sequence container” is both over- and under-constraining for what we need. All we need is something that has a way to “put stuff in” and “get stuff back” in a determined order. A sequence container needs a way to not only do that, but also access the stuff in order. We don’t need that; we are only interested in the “front” or “back” element… not anything else. We don’t need iterators, for example. Just the ability to put elements into the “front”/“back”, and get them back in the same (or rather, reversed) order that we put them there. So “sequence container” is too much for what we need.

At the same time, just because something is a sequence container, that doesn’t mean it can work as the underlying container for a stack. std::array is a sequence container, but it won’t do. It’s size is fixed, and we need the ability to grow and shrink as we push and pop elements. So “sequence container” is not enough for what we need.

But let’s put that aside for the moment and solve the problem we originally thought we had: How do we constrain for a sequence container?

So, my solution for just “container”—not yet “sequence container”—was this:

template <typename T>
concept container =
    std::ranges::forward_range<T>
    and not std::ranges::borrowed_range<T>
    and not std::ranges::view<T>
;

Basically:

  1. a forward range (because a container can’t sensibly be just single-pass readable)
  2. that is not borrowing its elements (because a container should own its elements); and
  3. is not a view (ranges are basically either containers or views, so if it’s a range but not a view, it’s (probably) a container).

If we look at the requirements for a standard-compatible sequence container, we neither need nor want to constrain on all of those operations. Doing so would not only be absurdly overwrought, it would be wrong. For example std::array is a sequence container, but it has neither insert() nor erase() operations.

What we need to do is suss which operations are fundamental to being a sequence container: which operations a sequence container must have, and which only a sequence container should have.

I propose for that we should use the assign() operations.

Why assign()? Because if something is a container that holds a bunch of stuff in order, it logically needs a way to put stuff into it in order. Even if a container has a fixed size, like std::array, it still needs a way to fill it.

insert() also serves that purpose, but it requires that the container is resizable… which is why it won’t work for std::array. Plus, associative containers also use insert(), so it won’t help specify sequence containers.

And because we are using assign(), we no longer need to worry about constraining against views or borrowed ranges, because you can’t assign to either of those.

So the concept could be:

template <typename T>
concept sequence_container =
    std::ranges::forward_range<T>
    and requires(
        T& container,
        std::ranges::range_value_t<T> const& v,
        std::ranges::range_size_t<T> const& n,
        std::initializer_list<std::range_value_t<T>> il
    )
    {
        container.assign(il);
        container.assign(n, v);
    }
;

Something like that should be all we need to accurately constrain on any sequence container, and only sequence containers.

But now back to the first point: while that constraint may work for detecting sequence containers… it won’t actually work for the underlying type of a stack. For example, it will accept std::array, but we can’t use that.

So we don’t really need to constrain for sequence containers. Well then, what do we need to constrain for?

Well, the underlying type for a stack needs to be a container with just the specific operations for putting elements at the front or back, and removing elements from the front or back. Assuming you’re going to use the back, then that’s just:

template <typename T>
concept stack_capable_container =
    std::ranges::forward_range<T>
    and requires(
        T& container,
        std::ranges::range_value_t<T> const& lv,
        std::ranges::range_value_t<T>&& rv
    )
    {
        container.push_back(lv);
        container.push_back(rv);
        container.pop_back();
    }
;

You could also check for .back(), .empty(), and .size() (or use std::ranges::sized_range). But that’s basically it.

I would go a step further, and add the additional constraint:

template <typename Container, typename T>
concept stack_capable_container_for =
    stack_capable_container<T>
    and std::same_as<std::ranges::range_value_t<T>, T>
;

So now you can just write:

template <
    number T,
    stack_capable_container_for<T> Container = std::vector<T>
>
class AdapterForStack

Boom goes the dynamite. That should do the job.

\$\endgroup\$
3
  • \$\begingroup\$ I just wanted to add one more thing: OP was asking about sequence containers, and cppreference actually has a page dedicated to just that: en.cppreference.com/w/cpp/named_req/SequenceContainer. I don't know if your concept definitions already handle that, but the requirements listed on that page are quite expansive. It might be easier to just list the current known sequence containers in the library and call it a day \$\endgroup\$
    – smac89
    Commented May 4 at 7:04
  • \$\begingroup\$ @indi Thank you so much. I am really amazed at your answer! Than you again. I have been reading your answer and modifying my implementation for almost two hours. I really can't imagine how long it takes to write this detailed answer. You taught a lot, not only about C++, but also kindness. I will modify the code with C++17 and prior firstly since it's a pity that I don't know any C++20 feature yet. \$\endgroup\$
    – John
    Commented May 4 at 8:24
  • \$\begingroup\$ I was thinking about how to constrain for sequence containers, and I don’t think it is as difficult as anyone thought, myself included. I’ll extend the answer with suggestions for a better concept. \$\endgroup\$
    – indi
    Commented May 4 at 20:53
3
\$\begingroup\$

Thanks to @indi. I have been reading @indi's answer and modifying my implementation for almost two hours. I really can't imagine how long it takes to write this detailed answer. I modify the code with C++17 and prior firstly since it's a pity that I don't know any C++20 feature yet. Here is the updated version.

#include <deque>
#include <iostream>
#include <list>
#include <map>
#include <memory>
#include <stack>
#include <string>
#include <type_traits>
#include <vector>

namespace Naive
{
    template<class T, class Container=std::vector<T>> 
    class Stack
    {
    public:
        using value_type = T;
        using size_type = typename Container::size_type;

        Stack(){}

        Stack(size_type size) : m_container(size){}

        Stack(size_type size, const value_type& val) : m_container(size, val){}
        
        template< class InputIt >
        Stack( InputIt first, InputIt last) : m_container(first, last){}

        Stack(const Stack&) = default;

        Stack(Stack&&) = default;

        Stack& operator=(const Stack&) = default;

        Stack& operator=(Stack&&) = default;
 

        bool empty() const noexcept
        {
            return m_container.empty();
        }

        size_type size() const noexcept
        {
            return m_container.size();
        }

        void push( const value_type& value )
        {
            m_container.push_back(value);
        }

        void push(value_type&& value )
        {
            m_container.push_back(std::move(value));
        }

        const value_type& top() const
        {
            return m_container.back();
        }

        value_type& top()
        {
            return m_container.back();
        }

        void pop()
        {
            m_container.pop_back();
        }

    private:
        Container m_container;
    };
};


// Base template for checking type, defaults to false
template<typename T>
struct is_vector_list_deque : std::false_type {};

// Specialization for std::vector
template<typename T, typename Alloc>
struct is_vector_list_deque<std::vector<T, Alloc>> : std::true_type {};

// Specialization for std::list
template<typename T, typename Alloc>
struct is_vector_list_deque<std::list<T, Alloc>> : std::true_type {};


// Specialization for std::list
template<typename T, typename Alloc>
struct is_vector_list_deque<std::deque<T, Alloc>> : std::true_type {};

template <typename T, class Container=std::vector<T>, typename std::enable_if<std::is_integral<T>::value && !std::is_same<T, bool>::value && is_vector_list_deque<Container>::value && std::is_same<T, typename Container::value_type>::value, T>::type* = nullptr>  
void test_mulit_lower_container() 
{                           
    {

        Naive::Stack<T, Container> stack{};

        T default_val{};
        for(int i=0; i<9; i++, default_val++)
        {
            stack.push(default_val);
        }
        
        while(!stack.empty())
        {
            std::cout << stack.top() << std::endl;
            stack.pop();
        } 
    }
}


int main()
{
    //testcase 1
    {
        // First we create a unique pointer.
        auto p = std::unique_ptr<std::string>(new std::string{"some text"});

        // Now we create the stack:
        auto stack = Naive::Stack<std::unique_ptr<std::string>>{};

        // Now we put the pointer in the stack 
        stack.push(std::move(p));

        // We can *view* the pointer:
        std::cout << *(stack.top()) << '\n';

        // But there is no possible way to *retrieve* the pointer:
        auto q = std::move(stack.top()); // won't work, non-copyable
    }

    //testcase 2
    {
        std::cout << "underline type is vector" << std::endl;
        test_mulit_lower_container<int, std::vector<int>>();

        std::cout << "underline type is list" << std::endl;
        test_mulit_lower_container<int, std::list<int>>();

        std::cout << "underline type is deque" << std::endl;
        test_mulit_lower_container<int, std::deque<int>>();
    }

    //testcase 3
    {
        using uniq_str_ptr = std::unique_ptr<std::string>;
        Naive::Stack<uniq_str_ptr, std::vector<uniq_str_ptr>> stack(3);

        while(!stack.empty())
        {
            auto& str_ptr = stack.top();
            if(str_ptr)
            {
                std::cout << *str_ptr << std::endl;
            }
        }
    }

    //not compile as expected, but seems still there is some problem with is_vector_list_deque<Container>::value && std::is_same<T, typename Container::value_type>::value
    {
        //test_mulit_lower_container<int, std::stack<int>>();
        //test_mulit_lower_container<int, std::map<int, int>>();

        //test_mulit_lower_container<int, int>();
    }
}
```
\$\endgroup\$
1
  • \$\begingroup\$ If you’re stuck with C++17 (and keep in mind that’s two versions back now; the current version is C++23, and C++26 features are already being rolled out), you can still use static_assert() and type traits to get something kinda like C++20 concept checking. It’s clunky, but if that’s all you got, at least it’s something. \$\endgroup\$
    – indi
    Commented May 4 at 20:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.