Shapes and extents¶
A tensor's shape says how many elements it has along each axis. In strata a shape is a type, Shape<...>, and each axis is either static, its extent written into the type, or dynamic, its extent known only at run time and marked Dyn.
Why a type¶
Putting an extent in the type lets the compiler know it. A static extent costs no memory, can be checked at compile time, and lets small tensors live inside the object with no allocation. A dynamic extent costs one std::size_t and is checked when the program runs. Mixing the two is the point: Shape<3, Dyn> is a matrix with 3 rows and however many columns you decide later.
| Shape | Rank | Dynamic axes | Size |
|---|---|---|---|
Shape<3, 4> | 2 | 0 | 12, known at compile time |
Shape<3, Dyn> | 2 | 1 | 3 times the second extent |
Shape<Dyn, Dyn> | 2 | 2 | the product of both, at run time |
Shape<> | 0 | 0 | 1: a scalar has exactly one element |
Making a shape¶
A constructor takes exactly the dynamic extents, in axis order, as integers of any type other than bool. The static ones are already in the type.
constexpr Shape<3, 4> fixed{}; // nothing to give
const Shape<3, Dyn, 5> mixed{4}; // (1)!
const Shape<Dyn, Dyn> dynamic{rows, columns}; // one per dynamic axis
- The middle axis is the only dynamic one, so
{4}is its extent: the shape is(3, 4, 5).
Giving the wrong number of extents does not compile: Shape<3, Dyn, Dyn>{4} and Shape<3, 4>{4} are both errors.
A default-constructed shape has every dynamic extent equal to 0, as std::extents does.
When you have every extent in hand, static ones included, use from_extents. The static ones must agree with the type:
Asking a shape¶
constexpr auto shape = Shape<3, Dyn, 5>{4};
static_assert(shape.rank() == 3);
static_assert(shape.dyn_rank() == 1); // dynamic axes
static_assert(shape.extent(1) == 4); // static or dynamic, the number
static_assert(shape.size() == 60); // 3 * 4 * 5
static_assert(Shape<3, Dyn>::static_extent(0) == 3); // what the type fixes, if anything
static_assert(!Shape<3, Dyn>::static_extent(1).has_value());
static_assert(Shape<3, Dyn>::is_dynamic(1));
rank, dyn_rank, static_extent and is_dynamic are static: they are facts about the type, and need no object. extent and size read the shape itself. All of them work in constant expressions.
An extent of 0 is allowed and gives a size of 0: Shape<3, 0>{}.size() == 0.
What is checked¶
Invalid extents are caught as early as they can be.
A static extent must be a non-negative integer, not bool, and below the maximum of std::size_t, which strata keeps as the marker of a dynamic extent. The product of all static extents must also fit std::size_t. Violating any of these fails to compile.
A dynamic extent must be non-negative, and the size of the whole shape must not overflow std::size_t. from_extents additionally requires the static extents to match the type.
extent(axis) and static_extent(axis) need axis < rank().
Violations are not exceptions
A failed check is a precondition violation. In constant evaluation it is a compile error. At run time it prints the message and the call site, then aborts, and only in builds without NDEBUG. Do not rely on catching it, and do not rely on it in release builds.
Converting between shapes¶
A shape converts to one that fixes less: a static extent may become dynamic, never the other way round.
Shape<Dyn, Dyn> a = Shape<3, 4>{}; // fine: both become dynamic
Shape<Dyn, 4> b = Shape<3, 4>{}; // fine: the second stays static
// Shape<3, 4> c = Shape<Dyn, 4>{}; // error: a dynamic extent may not become static
// Shape<4, Dyn> d = Shape<3, 4>{}; // error: 3 and 4 are not the same
The same rule governs tensors and views, because they hold a shape. A view over a Shape<3, 4> tensor converts to a view over Shape<Dyn, Dyn>, so a function can take the general one and accept both.
Shapes of expressions¶
An expression such as a + b needs a shape for its result. strata takes the most specific one both operands allow: static wherever either operand fixes the extent.
const Matrix<float, 2, 3> a = /* ... */;
const Matrix<float, Dyn, Dyn> b{Shape<Dyn, Dyn>{2, 3}, 0.5F};
Matrix c = a + b; // Tensor<float, Shape<2, 3>>: the static shape wins
Operands whose shapes can never agree, because their ranks differ or two static extents on the same axis differ, do not compile. When they agree on static extents but a dynamic one hides the difference, the mismatch is found at run time.
Printing a shape¶
A shape formats with std::format:
| Spec | Output of Shape<3, Dyn, 5>{4} | Meaning |
|---|---|---|
{} | (3, 4, 5) | the extents, as NumPy prints a shape |
{:?} | Shape<3, Dyn, 5>(3, 4, 5) | the type's static extents first, Dyn where dynamic |
Any other spec is a compile error.
Under the hood¶
How a shape stores its extents
Each template argument is turned into an extent: a number becomes a static one, Dyn a dynamic one. They are two distinct types rather than a sentinel value, so a dynamic axis reads as such in compiler diagnostics and debuggers instead of as a huge number.
Only the dynamic extents occupy memory. Shape<3, 4> is an empty class, and Shape<3, Dyn, Dyn> is the size of two std::size_t. Internally the static values and the marker for "stored" live in one list, which is what SemiStaticValues is: std::extents without its interface. Strides of a layout are made of the same thing.
See also¶
- Shapes and extents in the API reference.
- Order and layout: how the axes of a shape are laid out in memory.
- Views and expressions:
slice,permuteandblockchange a tensor's shape without copying.