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