C ++ 20: N-мерный минимальный класс Matrix

Я написал класс N-dim matrix (тензор), основанный на моей предыдущей реализации 2D-матрицы (2D-матрица в C ++ 20 и алгоритм Штрассена), приняв здесь множество полезных обзоров.

MatrixBase.h

#ifndef FROZENCA_MATRIXBASE_H
#define FROZENCA_MATRIXBASE_H

#include <algorithm>
#include <array>
#include <cassert>
#include <cstddef>
#include <concepts>
#include <functional>
#include <initializer_list>
#include <iostream>
#include <numeric>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include "MatrixUtils.h"
#include "MatrixInitializer.h"

namespace frozenca {

template <typename T, std::regular R, std::size_t N>
class MatrixBase {
public:
    static constexpr std::size_t ndim = N;

private:
    std::size_t size_;
    std::array<std::size_t, N> dims_;
    std::array<std::size_t, N> strides_;

protected:
    MatrixBase() = delete;
    virtual ~MatrixBase() = default;

    template <typename... Dims>
    explicit MatrixBase(Dims... dims);

    MatrixBase(std::size_t size,
               const std::array<std::size_t, N> dims,
               const std::array<std::size_t, N> strides) : size_ {size}, dims_ {dims}, strides_ {strides} {}

public:
    using value_type = R;
    using reference = R&;
    using const_reference = const R&;
    using pointer = R*;

    friend void swap(MatrixBase& a, MatrixBase& b) noexcept {
        std::swap(a.size_, b.size_);
        std::swap(a.dims_, b.dims_);
        std::swap(a.strides_, b.strides_);
    }

    auto begin() { return static_cast<T&>(*this).begin(); }
    auto cbegin() const { return static_cast<const T&>(*this).cbegin(); }
    auto end() { return static_cast<T&>(*this).end(); }
    auto cend() const { return static_cast<const T&>(*this).cend(); }
    auto rbegin() { return static_cast<T&>(*this).rbegin(); }
    auto crbegin() const { return static_cast<const T&>(*this).crbegin(); }
    auto rend() { return static_cast<T&>(*this).rend(); }
    auto crend() const { return static_cast<const T&>(*this).crend(); }

    template <RequestingElement... Args>
    reference operator()(Args... args);

    template <RequestingElement... Args>
    const_reference operator()(Args... args) const;

    reference operator[](const std::array<std::size_t, N>& pos);
    const_reference operator[](const std::array<std::size_t, N>& pos) const;

    template <std::regular U>
    MatrixBase(std::initializer_list<U>) = delete;

    template <std::regular U>
    MatrixBase& operator=(std::initializer_list<U>) = delete;

    MatrixBase(typename MatrixInitializer<R, N>::type init);

    [[nodiscard]] std::size_t size() const { return size_;}

    [[nodiscard]] const std::array<std::size_t, N>& dims() const {
        return dims_;
    }

    [[nodiscard]] std::size_t dims(std::size_t n) const {
        if (n >= N) {
            throw std::out_of_range("Out of range in dims");
        }
        return dims_[n];
    }

    [[nodiscard]] const std::array<std::size_t, N>& strides() const {
        return strides_;
    }

    [[nodiscard]] std::size_t strides(std::size_t n) const {
        if (n >= N) {
            throw std::out_of_range("Out of range in strides");
        }
        return strides_[n];
    }

};

template <typename T, std::regular R, std::size_t N>
template <typename... Dims>
MatrixBase<T, R, N>::MatrixBase(Dims... dims) : size_{(... * static_cast<std::size_t>(dims))},
    dims_{static_cast<std::size_t>(dims)...} {
    static_assert(sizeof...(Dims) == N);
    if (std::ranges::find(dims_, 0lu) != std::end(dims_)) {
        throw std::invalid_argument("Zero dimension not allowed");
    }
    strides_ = computeStrides(dims_);
}

template <typename T, std::regular R, std::size_t N>
MatrixBase<T, R, N>::MatrixBase(typename MatrixInitializer<R, N>::type init) {
    dims_ = deriveDims<N>(init);
    strides_ = computeStrides(dims_);
    size_ = strides_[0] * dims_[0];
}

template <typename T, std::regular R, std::size_t N>
template <RequestingElement... Args>
typename MatrixBase<T, R, N>::reference MatrixBase<T, R, N>::operator()(Args... args) {
    return const_cast<typename MatrixBase<T, R, N>::reference>(std::as_const(*this).operator()(args...));
}

template <typename T, std::regular R, std::size_t N>
template <RequestingElement... Args>
typename MatrixBase<T, R, N>::const_reference MatrixBase<T, R, N>::operator()(Args... args) const {
    static_assert(sizeof...(args) == N);
    std::array<std::size_t, N> pos {std::size_t(args)...};
    return this->operator[](pos);
}

template <typename T, std::regular R, std::size_t N>
typename MatrixBase<T, R, N>::reference MatrixBase<T, R, N>::operator[](const std::array<std::size_t, N>& pos) {
    return const_cast<typename MatrixBase<T, R, N>::reference>(std::as_const(*this).operator[](pos));
}

template <typename T, std::regular R, std::size_t N>
typename MatrixBase<T, R, N>::const_reference MatrixBase<T, R, N>::operator[](const std::array<std::size_t, N>& pos) const {
    if (!std::equal(std::cbegin(pos), std::cend(pos), std::cbegin(dims_), std::less<>{})) {
        throw std::out_of_range("Out of range in element access");
    }
    return *(cbegin() + std::inner_product(std::cbegin(pos), std::cend(pos), std::cbegin(strides_), 0lu));
}

} // namespace frozenca

#endif //FROZENCA_MATRIXBASE_H

Matrix.h

#ifndef FROZENCA_MATRIX_H
#define FROZENCA_MATRIX_H

#include <array>
#include <iterator>
#include <memory>
#include "MatrixBase.h"
#include "MatrixView.h"
#include "MatrixInitializer.h"

namespace frozenca {

template <std::regular T, std::size_t N>
class Matrix final : public MatrixBase<Matrix<T, N>, T, N> {
private:
    std::unique_ptr<T[]> data_;

public:
    using Base = MatrixBase<Matrix<T, N>, T, N>;
    using Base::size;
    using Base::dims;
    using Base::strides;

    template <typename... Dims>
    explicit Matrix(Dims... dims);

    ~Matrix() override = default;

    friend void swap(Matrix& a, Matrix& b) noexcept {
        swap(static_cast<Base&>(a), static_cast<Base&>(b));
        std::swap(a.data_, b.data_);
    }

    using iterator = T*;
    using const_iterator = const T*;
    using reverse_iterator = std::reverse_iterator<iterator>;
    using const_reverse_iterator = std::reverse_iterator<const_iterator>;

    iterator begin() { return &data_[0];}
    const_iterator cbegin() const { return &data_[0]; }
    iterator end() { return &data_[size()];}
    const_iterator cend() const { return &data_[size()];}
    reverse_iterator rbegin() { return std::make_reverse_iterator(end());}
    const_reverse_iterator crbegin() const { return std::make_reverse_iterator(cend());}
    reverse_iterator rend() { return std::make_reverse_iterator(begin());}
    const_reverse_iterator crend() const { return std::make_reverse_iterator(cbegin());}

    template <typename M, std::regular U>
    Matrix(const MatrixBase<M, U, N>&);

    template <typename M, std::regular U>
    Matrix& operator=(const MatrixBase<M, U, N>&);

    Matrix(typename MatrixInitializer<T, N>::type init);
    Matrix& operator=(typename MatrixInitializer<T, N>::type init);

    MatrixView<T, N> submatrix(const std::array<std::size_t, N>& pos_begin, const std::array<std::size_t, N>& pos_end = dims()) {
        if (!std::equal(std::cbegin(pos_begin), std::cend(pos_begin), std::cbegin(pos_end), std::less<>{})) {
            throw std::out_of_range("submatrix begin/end position error");
        }
        std::array<std::size_t, N> view_dims;
        std::transform(std::cbegin(pos_end), std::cend(pos_end), std::cbegin(pos_begin), std::begin(view_dims), std::minus<>{});
        std::size_t view_size = std::accumulate(std::cbegin(view_dims), std::cend(view_dims), 1lu, std::multiplies<>{});
        MatrixView<T, N> view (view_size, view_dims, strides(), &this->operator[](pos_begin));
        return view;
    }
};

template <std::regular T, std::size_t N>
template <typename... Dims>
Matrix<T, N>::Matrix(Dims... dims) : Base(dims...), data_(std::make_unique<T[]>(size())) {
}

template <std::regular T, std::size_t N>
Matrix<T, N>::Matrix(typename MatrixInitializer<T, N>::type init) : Base(init),
        data_(std::make_unique<T[]>(size())) {
    insertFlat(data_, init);
}

template <std::regular T, std::size_t N>
Matrix<T, N>& Matrix<T, N>::operator=(typename MatrixInitializer<T, N>::type init) {
    Matrix<T, N> mat(init);
    swap(*this, mat);
    return *this;
}

} // namespace frozenca

#endif //FROZENCA_MATRIX_H

MatrixView.h

#ifndef FROZENCA_MATRIXVIEW_H
#define FROZENCA_MATRIXVIEW_H

#include <compare>
#include "MatrixBase.h"

namespace frozenca {

template <std::regular T, std::size_t N>
class MatrixView final : public MatrixBase<MatrixView<T, N>, T, N> {
private:
    T* data_view_;
    std::array<std::size_t, N> view_strides_;

public:
    using Base = MatrixBase<MatrixView<T, N>, T, N>;
    using Base::size;
    using Base::dims;
    using Base::strides;

    explicit MatrixView(std::size_t size,
                        const std::array<std::size_t, N>& dims,
                        const std::array<std::size_t, N>& strides,
                        T* data_view) : Base(size, dims, strides), data_view_ {data_view} {
        view_strides_ = computeStrides(dims);
    }

    ~MatrixView() override = default;

    friend void swap(MatrixView& a, MatrixView& b) noexcept {
        swap(static_cast<Base&>(a), static_cast<Base&>(b));
        std::swap(a.data_view_, b.data_view_);
        std::swap(a.view_strides_, b.view_strides_);
    }

    template <typename T_>
    struct MVIterator {
        MatrixView* ptr_ = nullptr;
        std::array<std::size_t, N> pos_ = {0};
        std::size_t offset_ = 0;
        std::size_t index_ = 0;

        using difference_type = std::ptrdiff_t;
        using value_type = T_;
        using pointer = T_*;
        using reference = T_&;
        using iterator_category = std::random_access_iterator_tag;

        MVIterator(MatrixView* ptr, std::array<std::size_t, N> pos = {0}) : ptr_ {ptr}, pos_ {pos} {
            ValidateOffset();
        }

        reference operator*() const {
            return ptr_->data_view_[offset_];
        }

        pointer operator->() const {
            return ptr_->data_view_ + offset_;
        }

        void ValidateOffset() {
            offset_ = std::inner_product(std::cbegin(pos_), std::cend(pos_), std::cbegin(ptr_->strides()), 0lu);
            index_ = std::inner_product(std::cbegin(pos_), std::cend(pos_), std::cbegin(ptr_->view_strides_), 0lu);
            if (index_ > ptr_->size()) {
                throw std::out_of_range("MatrixView iterator out of range");
            }
        }

        void Increment() {
            for (std::size_t i = N - 1; i < N; --i) {
                ++pos_[i];
                if (pos_[i] != ptr_->dims(i) || i == 0) {
                    break;
                } else {
                    pos_[i] = 0;
                }
            }
            ValidateOffset();
        }

        void Increment(std::ptrdiff_t n) {
            if (n < 0) {
                Decrement(-n);
                return;
            }
            auto carry = static_cast<std::size_t>(n);
            for (std::size_t i = N - 1; i < N; --i) {
                std::size_t curr_dim = ptr_->dims(i);
                pos_[i] += carry;
                if (pos_[i] < curr_dim || i == 0) {
                    break;
                } else {
                    carry = pos_[i] / curr_dim;
                    pos_[i] %= curr_dim;
                }
            }
            ValidateOffset();
        }

        void Decrement() {
            for (std::size_t i = N - 1; i < N; --i) {
                --pos_[i];
                if (pos_[i] != static_cast<std::size_t>(-1) || i == 0) {
                    break;
                } else {
                    pos_[i] = ptr_->dims(i) - 1;
                }
            }
            ValidateOffset();
        }

        void Decrement(std::ptrdiff_t n) {
            if (n < 0) {
                Increment(-n);
                return;
            }
            auto carry = static_cast<std::size_t>(n);
            for (std::size_t i = N - 1; i < N; --i) {
                std::size_t curr_dim = ptr_->dims(i);
                pos_[i] -= carry;
                if (pos_[i] < curr_dim || i == 0) {
                    break;
                } else {
                    carry = static_cast<std::size_t>(-quot(static_cast<long>(pos_[i]), static_cast<long>(curr_dim)));
                    pos_[i] = mod(static_cast<long>(pos_[i]), static_cast<long>(curr_dim));
                }
            }
            ValidateOffset();
        }

        MVIterator& operator++() {
            Increment();
            return *this;
        }

        MVIterator operator++(int) {
            MVIterator temp = *this;
            Increment();
            return temp;
        }

        MVIterator& operator--() {
            Decrement();
            return *this;
        }

        MVIterator operator--(int) {
            MVIterator temp = *this;
            Decrement();
            return temp;
        }

        MVIterator operator+(difference_type n) const {
            MVIterator temp = *this;
            temp.Increment(n);
            return temp;
        }

        MVIterator& operator+=(difference_type n) {
            Increment(n);
            return *this;
        }

        MVIterator operator-(difference_type n) const {
            MVIterator temp = *this;
            temp.Decrement(n);
            return temp;
        }

        MVIterator& operator-=(difference_type n) {
            Decrement(n);
            return *this;
        }

        reference operator[](difference_type n) const {
            return *(this + n);
        }

        template <typename T2>
        std::enable_if_t<std::is_same_v<std::remove_cv_t<T_>, std::remove_cv_t<T2>>, difference_type>
                operator-(const MVIterator<T2>& other) const {
            return offset_ - other.offset_;
        }

        // oh no.. *why* defining operator<=> doesn't work to automatically define these in gcc?
        template <typename T1, typename T2>
        friend std::enable_if_t<std::is_same_v<std::remove_cv_t<T1>, std::remove_cv_t<T2>>,
                bool>
        operator==(const MVIterator<T1>& it1, const MVIterator<T2>& it2) {
            return it1.offset_ == it2.offset_;
        }

        template <typename T1, typename T2>
        friend std::enable_if_t<std::is_same_v<std::remove_cv_t<T1>, std::remove_cv_t<T2>>,
                bool>
        operator!=(const MVIterator<T1>& it1, const MVIterator<T2>& it2) {
            return !(it1 == it2);
        }

        template <typename T1, typename T2>
        friend std::enable_if_t<std::is_same_v<std::remove_cv_t<T1>, std::remove_cv_t<T2>>,
        std::compare_three_way_result_t<std::size_t, std::size_t>>
        operator<=>(const MVIterator<T1>& it1, const MVIterator<T2>& it2) {
            return it1.offset_ <=> it2.offset_;
        }
    };

    using iterator = MVIterator<T>;
    using const_iterator = MVIterator<const T>;
    using reverse_iterator = std::reverse_iterator<iterator>;
    using const_reverse_iterator = std::reverse_iterator<const_iterator>;

    iterator begin() { return iterator(this);}
    const_iterator cbegin() const { return const_iterator(this); }
    iterator end() { return iterator(this, {dims(0), });}
    const_iterator cend() const { return iterator(this, {dims(0), });}
    reverse_iterator rbegin() { return std::make_reverse_iterator(end());}
    const_reverse_iterator crbegin() const { return std::make_reverse_iterator(cend());}
    reverse_iterator rend() { return std::make_reverse_iterator(begin());}
    const_reverse_iterator crend() const { return std::make_reverse_iterator(cbegin());}

};

} // namespace frozenca

#endif //FROZENCA_MATRIXVIEW_H

MatrixInitializer.h

#ifndef FROZENCA_MATRIXINITIALIZER_H
#define FROZENCA_MATRIXINITIALIZER_H

#include <concepts>
#include <initializer_list>

namespace frozenca {

template <std::regular T, std::size_t N>
struct MatrixInitializer {
    using type = std::initializer_list<typename MatrixInitializer<T, N - 1>::type>;
};

template <std::regular T>
struct MatrixInitializer<T, 1> {
    using type = std::initializer_list<T>;
};

template <std::regular T>
struct MatrixInitializer<T, 0>;

} // namespace frozenca

#endif //FROZENCA_MATRIXINITIALIZER_H

MatrixUtils.h

#ifndef FROZENCA_MATRIXUTILS_H
#define FROZENCA_MATRIXUTILS_H

#include <array>
#include <cassert>
#include <cstddef>
#include <iostream>
#include <iterator>
#include <memory>
#include <type_traits>

namespace frozenca {

template <typename... Args>
inline constexpr bool All(Args... args) { return (... && args); };

template <typename... Args>
inline constexpr bool Some(Args... args) { return (... || args); };

template <typename... Args>
concept RequestingElement = All(std::is_convertible_v<Args, std::size_t>...);

template <std::size_t N>
std::array<std::size_t, N> computeStrides(const std::array<std::size_t, N>& dims) {
    std::array<std::size_t, N> strides;
    std::size_t str = 1;
    for (std::size_t i = N - 1; i < N; --i) {
        strides[i] = str;
        str *= dims[i];
    }
    return strides;
}

template <std::size_t N, typename Initializer>
bool checkNonJagged(const Initializer& init) {
    auto i = std::cbegin(init);
    for (auto j = std::next(i); j != std::cend(init); ++j) {
        if (i->size() != j->size()) {
            return false;
        }
    }
    return true;
}

template <std::size_t N, typename Iter, typename Initializer>
void addDims(Iter& first, const Initializer& init) {
    if constexpr (N > 1) {
        assert(checkNonJagged<N>(init));
    }
    *first = std::size(init);
    ++first;
    if constexpr (N > 1) {
        addDims<N - 1>(first, *std::begin(init));
    }
}

template <std::size_t N, typename Initializer>
std::array<std::size_t, N> deriveDims(const Initializer& init) {
    std::array<std::size_t, N> dims;
    auto f = std::begin(dims);
    addDims<N>(f, init);
    return dims;
}

template <std::regular T>
void addList(std::unique_ptr<T[]>& data,
             const T* first, const T* last,
             std::size_t& index) {
    for (; first != last; ++first) {
        data[index] = *first;
        ++index;
    }
}

template <std::regular T, typename I>
void addList(std::unique_ptr<T[]>& data,
             const std::initializer_list<I>* first, const std::initializer_list<I>* last,
             std::size_t& index) {
    for (; first != last; ++first) {
        addList(data, first->begin(), first->end(), index);
    }
}

template <std::regular T, typename I>
void insertFlat(std::unique_ptr<T[]>& data, std::initializer_list<I> list) {
    std::size_t index = 0;
    addList(data, std::begin(list), std::end(list), index);
}

inline long quot(long a, long b) {
    return (a % b) >= 0 ? (a / b) : (a / b) - 1;
}

inline long mod(long a, long b) {
    return (a % b + b) % b;
}

} // namespace frozenca

#endif //FROZENCA_MATRIXUTILS_H

Код теста:

#include "Matrix.h"
#include <iostream>
#include <numeric>

int main() {
    frozenca::Matrix<float, 2> m {{1, 2, 3}, {4, 5, 6}};
    m = {{4, 6}, {1, 3}};
    std::cout << m(1, 1) << 'n';
    std::cout << m[{1, 1}] << 'n';

    // 3 x 4 x 5
    frozenca::Matrix<int, 3> m2 (3, 4, 5);
    std::iota(std::begin(m2), std::end(m2), 0);

    // should output 0 ~ 59
    for (auto n : m2) {
        std::cout << n << ' ';
    }
    std::cout << 'n';

    auto sub = m2.submatrix({1, 1, 1}, {2, 3, 4});

    // 26 27 28
    // 31 32 33
    for (auto n : sub) {
        std::cout << n << ' ';
    }
    std::cout << 'n';

}

Работает как задумано.

Впереди: пользовательские распределители, срезы, числовые операции (например, умножение матриц), reshape (), специализация одномерного шаблона и т. Д.

Не стесняйтесь комментировать что угодно, как всегда!

2 ответа
2

Выглядит очень хорошо. Мне особенно нравится улучшение submatrix(). Тем не менее, есть несколько вещей, которые я упомянул в моем обзоре 2D-класса, которые я повторю здесь, а также добавлю несколько других замечаний.

Избегайте слишком сильного ограничения типов шаблонов

Я вижу вы используете std::regular сейчас вместо concept Scalar. Это по-прежнему требует, чтобы тип был сопоставимым по равенству, но от этого ничего в ваших классах не зависит. Возможно, вы могли бы использовать std::semiregular вместо.

Избегайте дублирования кода

Всегда ищите возможности уменьшить дублирование кода, даже если оно выглядит мелочью. В MatrixBase, begin() и связанные функции делают static_cast<T&>(*this). Вы можете сделать этот код лучше, написав:

template <typename T, std::semiregular R, std::size_t N>
class MatrixBase {
    T &self() { return static_cast<T&>(*this); }
    ...
public:
    auto begin() { return self().begin(); }
    ...
}

quot() в MatrixUtils.h можно было бы переписать как:

inline long quot(long a, long b) {
    return (a / b) - (a % b < 0);
}

Удаление дублирования снижает вероятность ошибок.

Проблемы с concept RequestingElement

Эта концепция используется в вариативных шаблонах, но сама по себе не должна быть вариативным шаблоном. Я бы переписал это так:

template <typename Arg>
concept RequestingElement = std::is_convertible_v<Arg, std::size_t>;

При этом концепции All а также Some не нужны. Однако с этой концепцией есть и другие проблемы. Например, это просто проверяет, что что-то неявно конвертируется в size_t, но float также неявно преобразовал бы в size_t. Было бы лучше ограничить это integral типы.

Наконец, название мне не очень понятно, думаю, лучше было бы переименовать его в IndexType.

Делать size_, dims_ а также strides_ const

Похоже, что размеры матрицы зафиксированы во время строительства. Поэтому вы должны сделать переменные-члены, которые представляют размер и форму матрицы const.

Обратите внимание, что вы можете и должны будете вызывать функции внутри списка инициализаторов членов, например, так:

template <typename T, std::regular R, std::size_t N>
template <typename... Dims>
MatrixBase<T, R, N>::MatrixBase(Dims... dims) :
    size_{(... * static_cast<std::size_t>(dims))},
    dims_{static_cast<std::size_t>(dims)...},
    strides_{computeStrides(dims_)},
{
    static_assert(sizeof...(Dims) == N);
}

Разрешить размеры нулевой длины

Похоже, ваш код отлично справляется с матрицами с одним из измерений, имеющих нулевую длину. Я бы удалил проверку на это в конструкторе MatrixBase.

Обратите внимание, что выделение массива нулевой длины с использованием new, а также при использовании std::make_unique<T[]>(size), допустимо в C ++.

  • 1

    Если у тебя есть const элементы данных, то вы не можете назначить объекту. Он также определяет swap.

    — JDłuosz


  • Ах, хорошее замечание. Я думаю, это зависит от того, хотите ли вы разрешить переключение между матрицами разных размеров. Он уже ограничивает замену матрицами с тем же числом измерений …

    — Г. Сон

  • Хотя построение матрицы размерности нулевой длины само по себе не является незаконным, вы не можете сделать ничего значимого с матрицей, которая содержит измерение нулевой длины (почти все операции будут вызывать исключения). Вот почему я не допускал размеров нулевой длины.

    — замороженный


  • Также я переехал submatrix() к MatrixBase, потому что мы можем сделать подматрицу и из вида матрицы. Другие операции, такие как сложение матриц и умножение, будут выполняться после MatrixBase! Прохладный!

    — замороженный


  • 1

    @frozenca «вы не можете сделать ничего значимого с матрицей, содержащей измерение нулевой длины» — вы можете посмотреть, почему Fortran (от Fortran 90 до последней версии) позволяет использовать измерения нулевой длины с очевидной семантикой и никаких исключений. Основная причина в том, что они полезный!

    — алефзеро

Начиная с C ++ 11, вы можете рассчитывать на / а также % поведение предписанным образом: вместо того, чтобы быть определяемым реализацией (что бы ни делала базовая машинная инструкция), она определяется как усечение до нуля. Итак, тебе не нужна твоя quot обертка и mod Точно так же можно предположить, что разделение уже работало таким образом.

А до C ++ 11 был std::div.


 auto f = std::begin(dims);

Вам нужно вызвать begin и некоторые другие, используя «std двухэтапный «, а не путем прямого вызова квалифицированной версии. Если тип dims не является встроенным и требует поиска, зависящего от аргументов, тогда это не удастся.

using std::begin;
auto f = begin(dims);

или используйте более новые версии в ranges пространство имен или ваши собственные оболочки.

Но вы знаете, что это тип std::array, так же, как вы это делали, когда у вас std::initializer_list, просто используйте форму функции-члена dims.begin() так что это не вызывает у читателя красных флажков.


КСТАТИ, #pragma once; работает на всех основных компиляторах.

  • 1

    quot а также mod вести себя иначе с / а также %. quot(-7, 4) = -2, mod(-7, 4) = 1, -7 / 4 = -1, -7 % 4 = -3. Также мне не нравится #pragma once, это нестандартно.

    — замороженный


Добавить комментарий

Ваш адрес email не будет опубликован.