Array based queue implementation with C++20 I implemented a queue that holds elements on an array as underlying data type:
#include &lt;cstddef&gt;
#include &lt;cassert&gt;
#include &lt;concepts&gt;
#include &lt;stdexcept&gt;
#include &lt;type_traits&gt;
#include &lt;iostream&gt;

namespace mine {
template &lt;std::semiregular T, std::size_t N&gt; class queue {
public:
  using value_type = std::remove_cv_t&lt;T&gt;;
  using const_value_type = std::add_const_t&lt;value_type&gt;;

  using reference_type = std::add_lvalue_reference_t&lt;value_type&gt;;
  using const_reference_type = std::add_const_t&lt;reference_type&gt;;

  using size_type = std::size_t;
  using const_size_type = std::add_const_t&lt;size_type&gt;;

  void enqueue(const value_type val) {
    if (current_size == N) {
      throw std::overflow_error(&quot;queue overflowed.&quot;);
    }

    if (current_size == 0) {
      m_head = m_tail = 0;
    }

    m_tail = current_size;
    m_data[current_size++] = val;
  }

  value_type dequeue() {
    if (current_size == 0) {
      m_head = m_tail = 0;
      throw std::underflow_error(&quot;queue underflowed.&quot;);
    }

    --current_size;

    auto ret = m_data[m_head];
    m_data[m_head] = T{};
    ++m_head;
    return ret;
  }

  [[nodiscard]] reference_type head() { return m_data[m_head]; }
  [[nodiscard]] const_reference_type head() const { return m_data[m_head]; }

  [[nodiscard]] reference_type tail() { return m_data[m_tail]; }
  [[nodiscard]] const_reference_type tail() const { return m_data[m_tail]; }

  [[nodiscard]] bool empty() noexcept { return current_size == 0; }
  [[nodiscard]] const bool empty() const noexcept { return current_size == 0; }

  [[nodiscard]] size_type size() noexcept { return current_size; }
  [[nodiscard]] const size_type size() const noexcept { return current_size; }

private:
  size_type m_head = 0;
  size_type m_tail = 0;
  int current_size = 0;
  value_type m_data[N]{};
};

}

And I wonder about the possible blunders, what is missing and how to make it better in general. Thanks a lot.
Avoid unnecessary copies
enqueue() takes its parameter by value, which means the caller has to make a copy. Then it has to store that value in the array, making another copy. You can avoid copies by replacing the function that takes a value with two overloads, one for const references and one for rvalue references:
void enqueue(const_reference_type val) {
    // same as before
    ...
}

void enqueue(value_type &amp;&amp;val) {
    // mostly the same
    ...
    // but now we move:
    data[current_size++] = std::move(val);
}

The first version will make just one copy, the second one will use move assignment, which might avoid an expensive copy.
Similarly, dequeue() makes a copy. You can avoid this by splitting this function in two; one that returns a reference to the head element so the caller can read it without having to copy it, and another function that just removes the head element. You already have a head(), so then just modify dequeue() to be:
void dequeue() {
    // mostly the same as before, except without returning anything
    ...
}

This is also why std::queue provides a separate front() and pop() function. Also, doing it this way makes the requirements for T less strict; it now only requires it to be std::default_constructible and std::movable.
Make it look like std::queue
Programming is much easier if everything has a similar interface. Consider renaming your member functions to match that of std::queue:

enqueue() -&gt; push()
dequeue() -&gt; pop()
head() -&gt; front()
tail() -&gt; back()

You could also add emplace() and swap() to make it almost a drop-in replacement.