Date: Thu, 20 Apr 2023 21:42:47 +0200

I took the liberty of revisiting the source code of your dr_tensor

container and implementing a draft of the tensor container, making some

design changes to make it more flexible and lightweight.

Among other things, I believe that the implementation of the tensor

container should take inspiration from the specifications of some STL

containers, such as std::vector, and from the specification of std::mdspan

because they represent the real updates that the C++ language has had in

recent years. While the role of the tensor container should be to extend

the functionality of std::valarray by providing an interface for operating

on multidimensional arrays, the existence of the tensor container should

represent an evolution of std::valarray, an evolution that it was never

carried forward because the class stopped being actively supported from the

beginning. In this sense, if we succeed in carrying out the proposal and it

is approved by the committee, I hope that the tensor container will be more

successful than std::valarray and can be updated over time.

The class definition may be the following:

template <class T, class Extents, class LayoutPolicy = std::layout_right,

class ContainerPolicy = /* see-below */, class Alloc = std::allocator<T>>

class tensor;

The type T must meet the requirements of NumericType.

The requirements that are imposed on the elements depend on the actual

operations performed on the container. Generally, it is required that

element type meets the requirements of Erasable, but many member functions

impose stricter requirements. This container (but not its members) can be

instantiated with an incomplete element type if the allocator satisfies the

allocator completeness requirements.

The type Extents specifies number of dimensions, their sizes, and which are

known at compile time. Must be a specialization of std::extents.

The type LayoutPolicy specifies how to convert multidimensional index to

underlying 1D index (column-major 3D array, symmetric triangular 2D matrix,

etc) .

The type ContainerPolicy specifies how the elements in the container must

be accessed.

The type Alloc must meet the requirements of Allocator. The program is

ill-formed if Allocator::value_type is not the same as T.

using value_type = T;

using reference = value_type&;

using const_reference = const value_type&;

using pointer = std::allocator_traits<Alloc>::pointer;

using const_pointer = std::allocator_traits<Alloc>::const_pointer

using size_type = [ Unsigned integer type (usually std::size_t) ];

using difference_type = [ Signed integer type (usually std::ptrdiff_t) ];

using iterator = [ LegacyRandomAccessIterator, contiguous_iterator, and

ConstexprIterator to value_type ];

using const_iterator = [ LegacyRandomAccessIterator, contiguous_iterator,

and ConstexprIterator to const value_type ];

using reverse_iterator = reverse_iterator<iterator>;

using const_reverse_iterator = reverse_iterator<const_iterator>;

using extents_type = Extents;

using layout_type = LayoutPolicy;

using accessor_type = ContainerPolicy;

using allocator_type = Alloc;

constexpr tensor() noexcept(std::is_default_constructible_v<Alloc>);

[ Note: you cannot indicate the default constructor as noexcept (only

conditionally noexcept), because the standard does not guarantee that the

allocator's default constructor will not throw exceptions ]

constexpr explicit tensor(const allocator_type& a) noexcept;

constexpr explicit tensor(size_type n, const allocator_type& a =

allocator_type());

constexpr tensor(size_type n, const value_type& v, const allocator_type& a

= allocator_type());

constexpr tensor(const tensor& x);

constexpr tensor(const tensor& x, const allocator_type& a);

constexpr tensor(tensor&& x) noexcept;

constexpr tensor(tensor&& x, const allocator_type& a) noexcept;

[Note: it doesn't make sense to make noexcept conditional, because the

standard guarantees that the allocator's copy and move constructor doesn't

throw exceptions, and the container's move constructor doesn't have to do

any memory allocating operations.]

constexpr tensor(std::initializer_list<value_type> il, const

allocator_type& a = allocator_type());

template <class InputIter> constexpr tensor(InputIter first, InputIter

last, const allocator_type& a = allocator_type());

[ Note: this overload participates in overload resolution only if InputIt

satisfies LegacyInputIterator ]

template <class R> constexpr explicit tensor(std::from_range_t, R&& rg,

const allocator_type& a = allocator_type());

[ Note: this overload participates in overload resolution only if R

satisfies at least the std::ranges::input_range concept ]

constexpr ~tensor();

constexpr tensor& operator=(std::initializer_list<value_type> il);

constexpr tensor& operator=(const tensor& x);

constexpr tensor& operator=(tensor&& x)

noexcept(std::allocator_traits<Allocator>::propagate_on_container_move_assignment::value

|| std::allocator_traits<Allocator>::is_always_equal::value);

constexpr allocator_type get_allocator() const noexcept;

[ Note: set_allocator() was removed because changing the allocator doesn't

make much sense, also because, if

std::allocator_traits<Alloc>::is_always_equal::value is not true, it might

be necessary to deallocate already existing memory with the previous

allocator and re-allocate memory with the new allocator ]

[[nodiscard]] constexpr bool empty() const noexcept;

constexpr size_type size() const noexcept;

constexpr size_type capacity() const noexcept;

constexpr size_type max_size() const noexcept;

constexpr reference operator[](size_type ... indices);

constexpr const_reference operator[](size_type... indeces) const;

constexpr reference at(size_type ... indices);

constexpr const_reference at(size_type... indeces) const;

[ Note: if pos is not within the range of the container, an exception of

type std::out_of_range is thrown ]

constexpr swap(tensor& x)

noexcept(std::allocator_traits<Allocator>::propagate_on_container_swap::value

|| std::allocator_traits<Allocator>::is_always_equal::value);

void resize(size_type n);

void resize(size_type n, const value_type& v);

template <class T, class Extents, class LayoutPolicy, class

ContainerPolicy, class Alloc>

constexpr bool operator==(const tensor<T, Extents, LayoutPolicy,

ContainerPolicy, Alloc>& x, const tensor<T, Extents, LayoutPolicy,

ContainerPolicy, Alloc>& y);

template <class T, class Extents, class LayoutPolicy, class

ContainerPolicy, class Alloc>

constexpr auto operator<=>(const tensor<T, Extents, LayoutPolicy,

ContainerPolicy, Alloc>& x, const tensor<T, Extents, LayoutPolicy,

ContainerPolicy, Alloc>& y);

For the moment, we must work on custom constructors for std::mdspan of

others. The most important thing is creating few overloaded constructors,

that do not cause ambiguity with the already-existing overloaded

constructors. I omitted all the member overloaded operators to perform

Linear Algebra operations, since they can be easily found in the

implementation of std::valarray. Other member functions to perform more

specific operations might be introduced in the future. All non-member

overloaded operators to perform Linear Algebra operations use the member

overloaded operators under the hood.

Furthermore, we should think more about how to integrate the std::mdspan

class, taking from it only the interface to manage multidimensional arrays,

and some STL containers, taking from them the allocator-aware interface.

container and implementing a draft of the tensor container, making some

design changes to make it more flexible and lightweight.

Among other things, I believe that the implementation of the tensor

container should take inspiration from the specifications of some STL

containers, such as std::vector, and from the specification of std::mdspan

because they represent the real updates that the C++ language has had in

recent years. While the role of the tensor container should be to extend

the functionality of std::valarray by providing an interface for operating

on multidimensional arrays, the existence of the tensor container should

represent an evolution of std::valarray, an evolution that it was never

carried forward because the class stopped being actively supported from the

beginning. In this sense, if we succeed in carrying out the proposal and it

is approved by the committee, I hope that the tensor container will be more

successful than std::valarray and can be updated over time.

The class definition may be the following:

template <class T, class Extents, class LayoutPolicy = std::layout_right,

class ContainerPolicy = /* see-below */, class Alloc = std::allocator<T>>

class tensor;

The type T must meet the requirements of NumericType.

The requirements that are imposed on the elements depend on the actual

operations performed on the container. Generally, it is required that

element type meets the requirements of Erasable, but many member functions

impose stricter requirements. This container (but not its members) can be

instantiated with an incomplete element type if the allocator satisfies the

allocator completeness requirements.

The type Extents specifies number of dimensions, their sizes, and which are

known at compile time. Must be a specialization of std::extents.

The type LayoutPolicy specifies how to convert multidimensional index to

underlying 1D index (column-major 3D array, symmetric triangular 2D matrix,

etc) .

The type ContainerPolicy specifies how the elements in the container must

be accessed.

The type Alloc must meet the requirements of Allocator. The program is

ill-formed if Allocator::value_type is not the same as T.

using value_type = T;

using reference = value_type&;

using const_reference = const value_type&;

using pointer = std::allocator_traits<Alloc>::pointer;

using const_pointer = std::allocator_traits<Alloc>::const_pointer

using size_type = [ Unsigned integer type (usually std::size_t) ];

using difference_type = [ Signed integer type (usually std::ptrdiff_t) ];

using iterator = [ LegacyRandomAccessIterator, contiguous_iterator, and

ConstexprIterator to value_type ];

using const_iterator = [ LegacyRandomAccessIterator, contiguous_iterator,

and ConstexprIterator to const value_type ];

using reverse_iterator = reverse_iterator<iterator>;

using const_reverse_iterator = reverse_iterator<const_iterator>;

using extents_type = Extents;

using layout_type = LayoutPolicy;

using accessor_type = ContainerPolicy;

using allocator_type = Alloc;

constexpr tensor() noexcept(std::is_default_constructible_v<Alloc>);

[ Note: you cannot indicate the default constructor as noexcept (only

conditionally noexcept), because the standard does not guarantee that the

allocator's default constructor will not throw exceptions ]

constexpr explicit tensor(const allocator_type& a) noexcept;

constexpr explicit tensor(size_type n, const allocator_type& a =

allocator_type());

constexpr tensor(size_type n, const value_type& v, const allocator_type& a

= allocator_type());

constexpr tensor(const tensor& x);

constexpr tensor(const tensor& x, const allocator_type& a);

constexpr tensor(tensor&& x) noexcept;

constexpr tensor(tensor&& x, const allocator_type& a) noexcept;

[Note: it doesn't make sense to make noexcept conditional, because the

standard guarantees that the allocator's copy and move constructor doesn't

throw exceptions, and the container's move constructor doesn't have to do

any memory allocating operations.]

constexpr tensor(std::initializer_list<value_type> il, const

allocator_type& a = allocator_type());

template <class InputIter> constexpr tensor(InputIter first, InputIter

last, const allocator_type& a = allocator_type());

[ Note: this overload participates in overload resolution only if InputIt

satisfies LegacyInputIterator ]

template <class R> constexpr explicit tensor(std::from_range_t, R&& rg,

const allocator_type& a = allocator_type());

[ Note: this overload participates in overload resolution only if R

satisfies at least the std::ranges::input_range concept ]

constexpr ~tensor();

constexpr tensor& operator=(std::initializer_list<value_type> il);

constexpr tensor& operator=(const tensor& x);

constexpr tensor& operator=(tensor&& x)

noexcept(std::allocator_traits<Allocator>::propagate_on_container_move_assignment::value

|| std::allocator_traits<Allocator>::is_always_equal::value);

constexpr allocator_type get_allocator() const noexcept;

[ Note: set_allocator() was removed because changing the allocator doesn't

make much sense, also because, if

std::allocator_traits<Alloc>::is_always_equal::value is not true, it might

be necessary to deallocate already existing memory with the previous

allocator and re-allocate memory with the new allocator ]

[[nodiscard]] constexpr bool empty() const noexcept;

constexpr size_type size() const noexcept;

constexpr size_type capacity() const noexcept;

constexpr size_type max_size() const noexcept;

constexpr reference operator[](size_type ... indices);

constexpr const_reference operator[](size_type... indeces) const;

constexpr reference at(size_type ... indices);

constexpr const_reference at(size_type... indeces) const;

[ Note: if pos is not within the range of the container, an exception of

type std::out_of_range is thrown ]

constexpr swap(tensor& x)

noexcept(std::allocator_traits<Allocator>::propagate_on_container_swap::value

|| std::allocator_traits<Allocator>::is_always_equal::value);

void resize(size_type n);

void resize(size_type n, const value_type& v);

template <class T, class Extents, class LayoutPolicy, class

ContainerPolicy, class Alloc>

constexpr bool operator==(const tensor<T, Extents, LayoutPolicy,

ContainerPolicy, Alloc>& x, const tensor<T, Extents, LayoutPolicy,

ContainerPolicy, Alloc>& y);

template <class T, class Extents, class LayoutPolicy, class

ContainerPolicy, class Alloc>

constexpr auto operator<=>(const tensor<T, Extents, LayoutPolicy,

ContainerPolicy, Alloc>& x, const tensor<T, Extents, LayoutPolicy,

ContainerPolicy, Alloc>& y);

For the moment, we must work on custom constructors for std::mdspan of

others. The most important thing is creating few overloaded constructors,

that do not cause ambiguity with the already-existing overloaded

constructors. I omitted all the member overloaded operators to perform

Linear Algebra operations, since they can be easily found in the

implementation of std::valarray. Other member functions to perform more

specific operations might be introduced in the future. All non-member

overloaded operators to perform Linear Algebra operations use the member

overloaded operators under the hood.

Furthermore, we should think more about how to integrate the std::mdspan

class, taking from it only the interface to manage multidimensional arrays,

and some STL containers, taking from them the allocator-aware interface.

Received on 2023-04-20 19:43:00