How it works...

Currently, our custom container is capable of being constructed, added to, iterated over, and erased. The container does not, however, support the ability to directly access the container or support simple operations, such as a std::move() or comparison. To address these issues, let's start by adding the operator=() overloads that are missing:

    constexpr container &operator=(const container &other)
{
m_v = other.m_v;
return *this;
}

constexpr container &operator=(container &&other) noexcept
{
m_v = std::move(other.m_v);
return *this;
}

The first operator=() overload provides support for a copy assignment, while the second overload provides support for a move assignment. Since we only have a single private member variable that already provides proper copy and move semantics, we do not need to worry about self assignment (or moving), as the std::vector function's implementation of copy and move will handle this for us.

If your own custom containers have additional private elements, self-assignment checks are likely needed. For example, consider the following code:

    constexpr container &operator=(container &&other) noexcept
{
if (&other == this) {
return *this;
}

m_v = std::move(other.m_v);
m_something = other.m_something;

return *this;
}

The remaining operator=() overload takes an initializer list, shown as follows:

    constexpr container &operator=(std::initializer_list<T> list)
{
m_v = list;
std::sort(m_v.begin(), m_v.end(), compare_type());

return *this;
}

As shown in the preceding code snippet, like the initializer list constructor, we must reorder the std::vector after the assignment, as the initializer list could be provided in any order.

The next member functions to implement are the assign() functions. The following code snippet shows this:

    constexpr void assign(size_type count, const T &value)
{
m_v.assign(count, value);
}

template <typename Iter>
constexpr void assign(Iter first, Iter last)
{
m_v.assign(first, last);
std::sort(m_v.begin(), m_v.end(), compare_type());
}

constexpr void assign(std::initializer_list<T> list)
{
m_v.assign(list);
std::sort(m_v.begin(), m_v.end(), compare_type());
}

These functions are similar to the operator=() overloads, but do not provide return values or support additional functionality. Let's see how:

  • The first assign() function fills the std::vector with a specific value count number of times. Since the value never changes, the std::vector will always be in sorted order, in which case there is no need to sort the list.
  • The second assign() function takes an iterator range similar to the constructor version of this function. Like that function, the iterator that is passed to this function could come from a raw std::vector or another instance of our custom container, but with a different sort order. For this reason, we must sort the std::vector after assignment.
  • Finally, the assign() function also provides an initializer list version that is the same as our operator=() overload.

It should also be noted that we have added constexpr to each of our functions. This is because most of the functions in our custom container do nothing more than forward a call from the custom container to the std::vector, and, in some cases, make a call to std::sort(). The addition of constexpr tells the compiler to treat the code as a compile-time expression, enabling it to optimize out the additional function call when optimizations are enabled (if possible), ensuring that our custom wrapper has the smallest possible overhead.

In the past, this type of optimization was performed using the inline keyword. The constexpr, which was added in C++11, is not only capable of providing inline hints to the compiler, but it also tells the compiler that this function can be used at compile time instead of runtime (meaning that the compiler can execute the function while the code is being compiled to perform custom compile-time logic). In our example here, however, the runtime use of a std::vector is not possible as allocations are needed. As a result, the use of constexpr is simply for optimizations, and, on most compilers, the inline keyword would provide a similar benefit.

There are a number of additional functions that the std::vector also supports, such as get_allocator(), empty(), size(), and max_size(), all of which are just direct forwards. Let's focus on the accessors that, until now, have been missing from our custom container:

    constexpr const_reference at(size_type pos) const
{
return m_v.at(pos);
}

The first function that we provide to access the std::vector directly is the at() function. As with most of our member functions, this is a direct forward. Unlike a std::vector, however, we have no plans to add the operator[]() overloads that a std::vector provides. The difference between the at() function and the operator[]() overload is that the operator[]() does not check to ensure that the index that is provided is in bounds (that is, that it does not access elements outside the bounds of the std::vector).

The operator[]() overload is designed to function similarly to a standard C array. The problem with this operator (called the subscript operator) is that the lack of a bounds check opens the door for reliability and security bugs to make their way into your program. For this reason, the C++ core guidelines discourage the use of the subscript operator or any other forms of pointer arithmetic (anything that attempts to calculate the position of data through the use of a pointer without an explicit bounds check).

To prevent the use of the operator[]() overload from being used, we do not include it. 

Like std::vector, we can also add the front() and back() accessors as follows:

    constexpr const_reference front() const
{
return m_v.front();
}

constexpr const_reference back() const
{
return m_v.back();
}

The preceding additional accessors provide support for getting the first and last elements in our std::vector. As with the at() function, we only support the use of the const_reference versions of these functions that the std::vector already provides.

Let's now see the code snippet data() function:

    constexpr const T* data() const noexcept
{
return m_v.data();
}

The same goes for the data() function. We can only support the const versions of these member functions, as providing the non-const versions of these functions would provide the user direct access to the std::vector, allowing them to insert unordered data without the container having the ability to reorder as needed.

Let's now focus on the comparison operators. We start by defining the comparison operator's prototypes as friends of our container. This is needed as the comparison operators are typically implemented as non-member functions, and, as a result, will need private access to the container to compare the instances of std::vector that they contain.

For example, consider the following code snippet:

    template <typename O, typename Alloc>
friend constexpr bool operator==(const container<O, Alloc> &lhs,
const container<O, Alloc> &rhs);

template <typename O, typename Alloc>
friend constexpr bool operator!=(const container<O, Alloc> &lhs,
const container<O, Alloc> &rhs);

template <typename O, typename Alloc>
friend constexpr bool operator<(const container<O, Alloc> &lhs,
const container<O, Alloc> &rhs);

template <typename O, typename Alloc>
friend constexpr bool operator<=(const container<O, Alloc> &lhs,
const container<O, Alloc> &rhs);

template <typename O, typename Alloc>
friend constexpr bool operator>(const container<O, Alloc> &lhs,
const container<O, Alloc> &rhs);

template <typename O, typename Alloc>
friend constexpr bool operator>=(const container<O, Alloc> &lhs,
const container<O, Alloc> &rhs);

Finally, we implement the comparison operators as follows:

template <typename O, typename Alloc>
bool constexpr operator==(const container<O, Alloc> &lhs,
const container<O, Alloc> &rhs)
{
return lhs.m_v == rhs.m_v;
}

template <typename O, typename Alloc>
bool constexpr operator!=(const container<O, Alloc> &lhs,
const container<O, Alloc> &rhs)
{
return lhs.m_v != rhs.m_v;
}

As with the member functions, we only need to forward the calls to the std::vector, as there is no need to implement custom logic. The same applies to the remaining comparison operators.

For example, we can implement the >, <, >=, and <= comparison operators as follows:

template <typename O, typename Alloc>
bool constexpr operator<(const container<O, Alloc> &lhs,
const container<O, Alloc> &rhs)
{
return lhs.m_v < rhs.m_v;
}

template <typename O, typename Alloc>
bool constexpr operator<=(const container<O, Alloc> &lhs,
const container<O, Alloc> &rhs)
{
return lhs.m_v <= rhs.m_v;
}

template <typename O, typename Alloc>
bool constexpr operator>(const container<O, Alloc> &lhs,
const container<O, Alloc> &rhs)
{
return lhs.m_v > rhs.m_v;
}

template <typename O, typename Alloc>
bool constexpr operator>=(const container<O, Alloc> &lhs,
const container<O, Alloc> &rhs)
{
return lhs.m_v >= rhs.m_v;
}

That is it! That is how you implement your own container by leveraging an existing container.

As we saw, in most cases, there is no need to implement a container from scratch unless the container you need cannot be implemented using one of the containers that the C++ Standard Template Library already provides.

Using this approach, it is possible to not only create your own containers, but, more importantly, it is possible to encapsulate functionality that is duplicated throughout your code into a single container that can be independently tested and verified. This not only improves the reliability of your applications, but it makes them easier to read and maintain as well.

In the next chapter, we will explore how to use smart pointers in C++.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset