2
\$\begingroup\$

I'm trying to replicate std::array but with modifications that I should put the private data member to the public so the class template fixed_array will be a structural type that will be qualified as a non-type template parameter.

My goal here is to use the string literals and brace-init list as non-type template parameters where the ISO C++ standard may forbid it, so I created a wrapper for them to call the constructor that has corresponding types.

From class template fixed_string, I inherited the base class with more specialized member functions.

I applied some C++ 20 features which include string literal operator template, relaxed non-type template parameters, requires clause, spaceship operator, and ranges.

Note: From the definition of UDL operator""fs, I intended to use this even though ISO warns to declare with no starting underscores because, on my IDE (Code::Blocks), the syntax highlighting will disable the string literal color once I put an underscore.

// headers needed
#include <iterator>
#include <algorithm> // copy_n, fill_n
#include <type_traits>
#include <stdexcept> // out_of_range
#include <tuple>
#include <iostream>
#include <ranges>
#include <numeric>

// class template
template <typename T, size_t N>
struct fixed_array {
    // member type alias
    using value_type             = T;
    using size_type              = size_t;
    using difference_type        = ptrdiff_t;
    using pointer                = value_type*;
    using const_pointer          = const value_type*;
    using reference              = value_type&;
    using const_reference        = const value_type&;
    using iterator               = const value_type*;
    using const_iterator         = const value_type*;
    using reverse_iterator       = std::reverse_iterator<iterator>;
    using const_reverse_iterator = std::reverse_iterator<const_iterator>;

    // make underlying data public
    value_type data[N];

    // constructors
    constexpr fixed_array() noexcept {
        std::fill_n(data, N, 0);
    }

    constexpr fixed_array(const value_type& fill_arg) noexcept {
        std::fill_n(data, N, fill_arg);
    }

    constexpr fixed_array(const value_type(&arg)[N]) {
        std::copy_n(arg, N, data);
    }

    constexpr fixed_array(const std::initializer_list<value_type>& args) noexcept {
        std::copy_n(args.begin(), N, data);
    }

    constexpr fixed_array(const fixed_array& other) : fixed_array(other.data) {}
    constexpr fixed_array(fixed_array&& other) : fixed_array(std::move(other.data)) {}

    // destructors
    constexpr ~fixed_array() {}

    // assignment
    constexpr fixed_array& operator=(const fixed_array& other) {
        std::copy_n(other.data, N, data);
        return *this;
    }

    constexpr fixed_array& operator=(fixed_array&& other) {
        std::copy_n(std::move(other).data, N, data);
        return *this;
    }

    constexpr fixed_array& operator=(const value_type(&arg)[N]) {
        std::copy_n(arg, N, data);
        return *this;
    }

    constexpr fixed_array& operator=(const std::initializer_list<value_type> args) {
        std::copy_n(args.begin(), N, data);
        return *this;
    }

    // iterator access
    constexpr auto begin() const noexcept { return iterator{data}; }
    constexpr auto end() const noexcept { return iterator{data + N}; }
    constexpr auto cbegin() const noexcept { return const_iterator{data}; }
    constexpr auto cend() const noexcept { return const_iterator{data + N}; }
    constexpr auto rbegin() noexcept { return reverse_iterator{data}; }
    constexpr auto rend() noexcept { return reverse_iterator{data + N}; }
    constexpr auto crbegin() const noexcept { return const_reverse_iterator{data}; }
    constexpr auto crend() const noexcept { return const_reverse_iterator{data + N}; }

    // capacity
    constexpr size_type size() const noexcept { return N; }
    constexpr size_type max_size() const noexcept { return N; }

    // element access
    constexpr reference operator[](size_type pos) { return data[pos]; }
    constexpr const_reference operator[](size_type pos) const { return data[pos]; }
    constexpr reference at(size_type pos) {
        if (!(pos < N)) {
            throw std::out_of_range("indexing request is out of range...");
        } else {
            return data[pos];
        }
    }
    constexpr const_reference at(size_type pos) const {
        if (!(pos < N)) {
            throw std::out_of_range("indexing request is out of range...");
        } else {
            return data[pos];
        }
    }
    constexpr reference front() { return data[0]; }
    constexpr const_reference front() const { return data[0]; }
    constexpr reference back() { return data[N - 1]; }
    constexpr const_reference back() const { return data[N - 1]; }
    [[nodiscard]] constexpr bool empty() const { return N == 0; }

    // equality
    constexpr bool operator==(const fixed_array& other) const {
        return std::equal(begin(), end(), other.begin());
    }

    constexpr auto operator<=>(const fixed_array& other) const = default;

    // operations
    constexpr void fill(const value_type& value) {
        std::fill_n(data, N, value);
    }

    constexpr void swap(fixed_array& other) noexcept(std::is_nothrow_swappable_v<value_type>) {
        std::swap_ranges(begin(), end(), other.begin());
    }

    template <size_t I>
    constexpr value_type& get() & noexcept {
        return data[I];
    }

    template <size_t I>
    constexpr value_type&& get() && noexcept {
        return std::move(data[I]);
    }

    template <size_t I>
    constexpr const value_type& get() const& noexcept {
        return data[I];
    }

    template <size_t I>
    constexpr const value_type&& get() const&& noexcept {
        return std::move(data[I]);
    }
};

// deduction guides for fixed_array:
template <typename T, size_t N> fixed_array(const T(&)[N]) -> fixed_array<T, N>;
template <typename... Ts> fixed_array(Ts...) -> fixed_array<std::tuple_element_t<0, std::tuple<Ts...>>, sizeof...(Ts)>;

// specializing std::get, std::tuple_size, and std::tuple_element with fixed_array:
namespace std {
    template <typename T, size_t N>
    struct tuple_size<fixed_array<T, N>> : integral_constant<size_t, N> {};

    template <size_t I, typename T, size_t N>
    struct tuple_element<I, fixed_array<T, N>> : type_identity<T> {};

    using std::get;

    template <size_t I, typename T, size_t N>
    constexpr T& get(fixed_array<T, N>& arr) noexcept {
        return arr[I];
    }

    template <size_t I, typename T, size_t N>
    constexpr T&& get(fixed_array<T, N>&& arr) noexcept {
        return std::move(arr[I]);
    }

    template <size_t I, typename T, size_t N>
    constexpr const T& get(const fixed_array<T, N>& arr) noexcept {
        return arr[I];
    }

    template <size_t I, typename T, size_t N>
    constexpr const T&& get(const fixed_array<T, N>&& arr) noexcept {
        return std::move(arr[I]);
    }

    namespace ranges {
        template <typename T, size_t N>
        inline constexpr bool enable_view<fixed_array<T, N>> = true;
    }
}

// maybe static polymorphism:
template <size_t N>
struct fixed_string : fixed_array<char, N + 1> {
    using fixed_array<char, N + 1>::data;

    constexpr fixed_string(const char(&str)[N + 1]) noexcept
    requires (std::is_array_v<decltype(str)>) {
        std::copy_n(str, N + 1, data);
    }

    constexpr fixed_string() noexcept {
        std::fill_n(data, N + 1, 0);
    }

    constexpr fixed_string(const char* str) noexcept {
        std::copy_n(str, N + 1, data);
    }

    std::string to_string() const {
        return data;
    }

    // operations
    constexpr fixed_string& to_lower() {
        std::transform(data, data + N, data, ::tolower);
        return *this;
    }

    constexpr fixed_string& to_upper() {
        std::transform(data, data + N, data, ::toupper);
        return *this;
    }

    // stream insert
    friend std::ostream& operator<<(std::ostream& os, fixed_string<N> str) {
        os << str.data;
        return os;
    }
};

// deduction guide for fixed_string
template <size_t N> fixed_string(const char(&)[N]) -> fixed_string<N - 1>;

// UDL
#pragma GCC diagnostic ignored "-Wliteral-suffix"
template <fixed_string FS>
constexpr auto operator""fs() {
    return fixed_string{FS};
}
#pragma GCC diagnostic warning "-Wliteral-suffix"

// function tests
template <fixed_array Arr>
constexpr auto sum() -> decltype(Arr)::value_type {
    return std::accumulate(Arr.begin(), Arr.end(), 0);
}

template <fixed_string S>
void fixed_print() {
    for (const auto& i : S) {
        std::cout << i << ' ';
    }   std::cout << '\n';
}

template <fixed_array Arr1, fixed_array Arr2>
constexpr bool is_equal() requires (Arr1.size() == Arr2.size()) {
    return Arr1 == Arr2;
}

template <fixed_array Arr1, fixed_array Arr2>
constexpr bool is_not_equal() requires (Arr1.size() == Arr2.size()) {
    return Arr1 != Arr2;
}

template <fixed_array Arr1, fixed_array Arr2>
constexpr bool is_less_than() requires (Arr1.size() == Arr2.size()) {
    return Arr1 < Arr2;
}

template <fixed_array Arr1, fixed_array Arr2>
constexpr bool is_less_than_or_equal() requires (Arr1.size() == Arr2.size()) {
    return Arr1 <= Arr2;
}

template <fixed_array Arr1, fixed_array Arr2>
constexpr bool is_greater_than() requires (Arr1.size() == Arr2.size()) {
    return Arr1 > Arr2;
}

template <fixed_array Arr1, fixed_array Arr2>
constexpr bool is_greater_than_or_equal() requires (Arr1.size() == Arr2.size()) {
    return Arr1 >= Arr2;
}

Main Function:

int main() {

    // test fixed_string:
    std::cout << "Chapter 1: Preliminary" << '\n';
    fixed_print<"Desmond Gold">();
    std::cout << "Sum: " << sum<{1, 2, 3, 4, 5}>() << '\n';

    static_assert(sum<{1, 2, 3, 4, 5}>() == 15);

    // prelim
    int c_arr_1[] = {8, 4, 3};
    auto print = [](auto x){ std::cout << x << ' '; };

    // testing 1: constructors

    [[maybe_unused]] fixed_array arr_1 {1, 2, 3, 4}; // #4
    [[maybe_unused]] fixed_array arr_2 = {9, 4, 2, 1, 7}; // #5
    [[maybe_unused]] fixed_array arr_3 { arr_1 }; // #4
    [[maybe_unused]] fixed_array arr_4 { std::move(arr_2) }; // #5
    [[maybe_unused]] fixed_array arr_5 = c_arr_1; // #3

    // testing 2: assignments
    arr_1 = arr_1;
    arr_2 = std::move(arr_2);
    arr_3 = {0, 1, 2, 8, 9}; // same length
    arr_5 = c_arr_1;

    // testing 3: iterators
    std::cout << "Chapter 2: Iterators" << '\n';
    std::for_each(arr_1.begin(), arr_1.end(), print); std::cout << '\n';
    std::for_each(arr_2.cbegin(), arr_2.cend(), print); std::cout << '\n';

    // testing 4: element access
    std::cout << "Chapter 3: Element Access" << '\n';
    for (decltype(arr_5)::size_type i = 0; i < arr_5.size(); i++) {
        std::cout << arr_5[i] << ' ';
    }   std::cout << '\n';

    std::cout << "Front: " << arr_1.front() << '\n'
              << "Back:  " << arr_1.back()  << '\n';

    // testing 5: operations
    std::cout << "Chapter 4: Fill Operation" << '\n';
    arr_5.fill(99);

    for (const auto& i : arr_5) {
        std::cout << i << ' ';
    }   std::cout << '\n';

    // testing 6: range views
    std::cout << "Chapter 5: Range Views" << '\n';
    auto reverse_and_filter_array = std::views::reverse | std::views::filter([](auto x){ return x > 1; });
    auto transform_array = std::views::transform([](auto x){ return x * x; });

    for (const auto& elem : arr_1
                          | reverse_and_filter_array
                          | transform_array) {

        std::cout << elem << ' ';
    }   std::cout << '\n';

    // testing 7: structured binding
    std::cout << "Chapter 6: Structured Binding" << '\n';
    [[maybe_unused]] const auto& [x_1, x_2, x_3] = arr_5;

    std::cout
    << "x_1: " << x_1 << '\n'
    << "x_2: " << x_2 << '\n'
    << "x_3: " << x_3 << '\n';

    std::cout << "Tuple: "
    << std::get<0>(arr_1) << ' '
    << std::get<1>(arr_1) << ' '
    << std::get<2>(arr_1) << ' '
    << std::get<3>(arr_1) << '\n';

    // testing 8: equality and lexicographical comparison (must be in the same length)
    std::cout << std::boolalpha << "Chapter 7: Comparisons" << '\n';

    std::cout
    << "Is equal? [1, 2, 3] and [1, 2, 3]: " << is_equal<{1, 2, 3}, {1, 2, 3}>() << '\n'
    << "Is not equal? [5, 5, 5] and [5, 4, 5]: " << is_not_equal<{5, 5, 5}, {5, 4, 5}>()   << '\n'
    << "Is less than? [5, 4, 4] and [4, 4, 4]: " << is_less_than<{5, 4, 4}, {4, 4, 4}>() << '\n'
    << "Is less than or equal? [7, 6, 3] and [7, 7, 7]: " << is_less_than_or_equal<{7, 6, 3}, {7, 7, 7}>() << '\n'
    << "Is greater than? [6, 5, 1] and [1, 5, 6]: " << is_greater_than<{6, 5, 1}, {1, 5, 6}>() << '\n'
    << "Is greater than or equal? [19, 10, 17] and [14, 10, 13]: " << is_greater_than_or_equal<{19, 10, 17}, {14, 10, 13}>() << '\n';

    // testing 9: fixed strings
    auto string_1 = "First String"fs;
    std::cout << "Chapter 8: Fixed Strings" << '\n'
              << string_1 << '\n';

    static_assert("Hello"fs == "Hello"fs);


    return 0;
}

Output:

Chapter 1: Preliminary
D e s m o n d   G o l d
Sum: 15
Chapter 2: Iterators
1 2 3 4
9 4 2 1 7
Chapter 3: Element Access
8 4 3
Front: 1
Back:  4
Chapter 4: Fill Operation
99 99 99
Chapter 5: Range Views
16 9 4
Chapter 6: Structured Binding
x_1: 99
x_2: 99
x_3: 99
Tuple: 1 2 3 4
Chapter 7: Comparisons
Is equal? [1, 2, 3] and [1, 2, 3]: true
Is not equal? [5, 5, 5] and [5, 4, 5]: true
Is less than? [5, 4, 4] and [4, 4, 4]: false
Is less than or equal? [7, 6, 3] and [7, 7, 7]: true
Is greater than? [6, 5, 1] and [1, 5, 6]: true
Is greater than or equal? [19, 10, 17] and [14, 10, 13]: true
Chapter 8: Fixed Strings
First String

So far, there are exactly 0 compilation errors (only in GCC, except for Clang (because it would reject my code using no-underscore UDL, brace-init-list from non-type template parameter, and some ranges-related errors. So it may lead to non-portable code), and no runtime errors with passing static assertions.

Is there anything I could improve?

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Mainly this is pretty nice :)


constexpr fixed_array(const std::initializer_list<value_type>& args) noexcept {
    std::copy_n(args.begin(), N, data);
}

We definitely need to check that the initializer_list is the correct size, and throw if it isn't.

Note that initializer lists are lightweight (probably just a pair of pointers, or a pointer and a size), so can be passed by value, instead of const&.


constexpr fixed_array(const fixed_array& other) : fixed_array(other.data) {}
constexpr fixed_array(fixed_array&& other) : fixed_array(std::move(other.data)) {}

// destructors
constexpr ~fixed_array() {}

These guys can all be = default;

constexpr fixed_array& operator=(const fixed_array& other) {
    std::copy_n(other.data, N, data);
    return *this;
}

constexpr fixed_array& operator=(fixed_array&& other) {
    std::copy_n(std::move(other).data, N, data);
    return *this;
}

As can these.


constexpr bool operator==(const fixed_array& other) const {
    return std::equal(begin(), end(), other.begin());
}

I believe the spaceship operator will define this for us.


namespace std {
    ...
}

I think that specializing std::tuple_size and std::tuple_element in the std namespace is ok, but specializing std::get is technically not ok.

For std::get, we should define the get functions in the same namespace as the fixed_array class, so that they work correctly with ADL. i.e. the user can do:

using std::get;
get<0>(a); // will find and use the `get` we defined in the same namespace if `a` is our fixed_array type, or std::get if `a` is a std type

constexpr fixed_string(const char(&str)[N + 1]) noexcept
requires (std::is_array_v<decltype(str)>) {
    std::copy_n(str, N + 1, data);
}

Hmm... I'm guessing that the N + 1 size of fixed_string is so that we can always have a terminating null character. Here we're copying from a char array of N + 1 chars, with no guarantee that the last char is null.

Perhaps we should only accept a size N array? (And manually add the null character).

It might make sense to accept smaller array sizes too? (And fill the rest of the array with nulls).

We probably need to think about other assignment operations here too, and make sure that we have a trailing null.

Consider adding a .c_str() member too.

I guess exactly what you do here depends on the intent. Is the class only for strings of size N exactly, or should it be for any string up to size N?


constexpr fixed_string(const char* str) noexcept {
    std::copy_n(str, N + 1, data);
}

Eeep! We have no guarantee that str is long enough. We need to check and throw, or use something like strncpy.


constexpr fixed_string& to_lower() {
    std::transform(data, data + N, data, ::tolower);
    return *this;
}

Technically, we have to cast our chars to unsigned char when using tolower or toupper.

Also, technically, these functions are in the std:: namespace.


friend std::ostream& operator<<(std::ostream& os, fixed_string<N> str) {
    os << str.data;
    return os;
}

We could pass the fixed_string by const& instead of by value.

\$\endgroup\$

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.