std::array support

Issue #253 resolved
Matthias Moulin created an issue

All dense vector classes offer a constructor for an initialization with a dynamic or static array. Is it also possible to offer an additional templated constructor for an initialization (constexpr for StaticVector) with a std::array? The latter is ideal for storage purposes and more flexible than a static array due to its additional STL functionality and the possibility to use it as a base class (to extend it with proper constructors).

#include <array>
#include <blaze/math/StaticVector.h>
#include <iostream>

int main() {
    constexpr std::array< float, 4u > a = { 0.0f, 1.0f, 2.0f, 3.0f };
    constexpr float b[] = { 0.0f, 1.0f, 2.0f, 3.0f };

    blaze::StaticVector< float, 4u, blaze::rowVector > va(a);
    blaze::StaticVector< float, 4u, blaze::rowVector > vb(b);

    std::cout << va << std::endl;
    std::cout << vb << std::endl;

    return 0;
}

Furthermore, I also wonder why

explicit inline constexpr StaticVector( initializer_list<Type> list );

template< typename Other, size_t Dim >
explicit inline StaticVector( const Other (&array)[Dim] );

both are declared explicit?

Comments (13)

  1. Klaus Iglberger

    Hi Matthias!

    Thanks a lot for creating this proposal. You are correct, it would be convenient to have a constructor that enables you to pass a std::array or also std::vector directly instead of passing its size and data:

    #include <array>
    #include <blaze/math/StaticVector.h>
    #include <iostream>
    
    int main()
    {
        constexpr std::array< float, 4u > a{ 0.0f, 1.0f, 2.0f, 3.0f };
        constexpr float b[] = { 0.0f, 1.0f, 2.0f, 3.0f };
        std::vector< float > c{ 0.0f, 1.0f, 2.0f, 3.0f };
    
        blaze::StaticVector< float, 4u, blaze::rowVector > va(a.size(), a.data());
        blaze::StaticVector< float, 4u, blaze::rowVector > vb(b);
        blaze::StaticVector< float, 4u, blaze::rowVector > vc(c.size(), c.data());
    
        std::cout << va << std::endl;
        std::cout << vb << std::endl;
        std::cout << vc << std::endl;
    
        return 0;
    }
    

    However, there shouldn't be a special purpose constructor for every possible container, but a constructor based on std::span. Since std::span is proposed for C++20, we are considering to alternatively provide a stand-in until std::span becomes available.

    The two constructors are marked explicit as the C++ Core Guideline C.46 suggests that all single argument constructors should be explicit. We believe that in both cases an explicit constructor call is reasonable since it involves a copy operation (i.e. runtime overhead). This shouldn't go unnoticed in an implicit conversion.

    Thanks again for creating the proposal,

    Best regards,

    Klaus!

  2. Matthias Moulin reporter

    Hi Klaus,

    Thanks for the answers! You're right about the C++20 std::span generalization for general contiguous sequences. (In the meantime, you can consider using gsl::spanas that was the original std::span proposal.) Apparently, std::spanhas constexprconstructors which still enable the compile-time construction of blaze::StaticVectorfrom an std::array.

    What I now also wonder about, asstd::spanis going to be the preferred way to construct Blaze vectors from STL kind of containers, is whether there is a preferred way (e.g., explicit cast operators) of going back to STL kind of containers without using a free function or relying on the container itself to do the conversion. My problem with e.g., DirectXMath are explicit load/store barriers to/from the SIMD registers as there is still a need of non-SIMD storage containers of which the size is exactly the same across all platforms for storage and GPU packing purposes:

    // Pseudo C++:
    
    StorageVectorType stored = ... ;
    ComputeVectorType computed = Load(stored);
    
    // do all computations
    
    stored = Store(computed);
    
  3. Matthias Moulin reporter

    Note that std::span could not be used to go back to STL containers via a cast operator as std::span merely implements a (mutable) view to a contiguous sequence.

  4. Russell Hewett

    Hi Klaus and Matthias,

    Preface: list-initialization is a huge reason I am exploring Blaze over Eigen. I had a longer discussion that was more technical than I am comfortable with, regarding internals of C++ and the STL, so I'll leave this as a use-case consideration and not a technical suggestion.

    The issue with explicit came directly to me today as I started exploring Blaze, so I am glad I found this discussion. I’m developing a library which avoid exposing explicitly the underlying storage types. This model is already working well as most changes from Eigen to Blaze were quite simple.

    Consider an example API in which it does not matter to the end user what the underlying storage looks like.

    class Thing {
       typedef Vector_t = ... // std::array<int, 3> or blaze::StaticVector<int, 3>
       void process(const Vector_t& external) {
           ...
       }
    };
    
    // Valid for std::array but not for blaze
    Thing::process({1,2,3});
    
    // Valid for both, but unnecessary extra work for the user
    Thing::Vector_t v{1,2,3};
    Thing::process(v);
    
    // Valid for std::array but not blaze, still unnecessary extra work for the user
    Thing::Vector_t v = {1,2,3};
    Thing::process(v);
    

    If I had only the second/third use-case above, there would be no issue, as they are essentially the same from a user perspective, but the first case is a motivator for me.

    To make the first case work, I have to overload process to take a specific STL container, then use the StaticVector constructor on size and data to create a vector which can be passed to the cannonical processfunction. ( A similar hack works in Eigen, but still requires an additional copy, using Eigen::Map.) I will proceed this way, but it would be really nice to essentially treat StaticVector as a mathematically enhanced drop-in replacement for std::array.

    Related, speaking with respect to copies and overhead, why is the argument here not a const reference?

    explicit inline constexpr StaticVector( initializer_list<Type> list );

  5. Klaus Iglberger

    To answer your question: std::initializer_list contains a pointer and a size (see cppreference for more details). That is why at cppreference or also in the C++ Core Guidelines you will always find examples where an std::initializer_list is passed by value.

    I understand your example as a request to rethink the explicit specifier on the std::initializer_list constructors. That is an interesting point, especially since the standard library itself uses non-explicit constructors. We will consider the this change.

    Best regards,

    Klaus!

  6. Matthias Moulin reporter

    Normally, I would prefer the explicit keyword for pretty much all my C++ constructors (to avoid chains of implicit type conversions), but for mathematical concepts such as vectors, matrices and tensors it feels more convenient and familiar to have implicit constructors to write statements such as MyVector v = { 1, 2, 3 };

    std::array seems to me like the most portable standard C++ adapter to transfer data between libraries in both ways (the Vc std proposal seems to use it as well). Though, one probably will construct a wrapper anyway, because std::array has no constructors and uses aggregate initialization (which starts to look different and odd for initializing aggregates of aggregates). std::span can be used as well to transfer data to a library, as it provides an implicit constructor accepting a std::array, but not always to transfer data from a library (e.g., implicit/explicit cast operator), as it is merely a view.

    In order to avoid adding Blaze dependencies to my core library while still having std::array that can be constructed from Blaze static vectors and matrices. I created a class that inherits from std::array and adds constructors (instead of using aggregate initialization). Apart from the usual default, copy and move constructor, I only added one variadic template argument constructor which can be evaluated at compile-time and roughly acts as follows:

    • For 1D arrays:

      • Flatten (up to one level for tuple-like arguments) and concatenate the given flattened arguments to obtain the array to construct (padded with zeros if needed).
    • For nD > 1D arrays:

      • If there is only one argument of the same dimension, and smaller or equal size as the array to construct, convert the argument to the array to construct (padded with zeros if needed). This assumes the argument is tuple-like.
      • If there are multiple arguments of smaller dimension, and smaller or equal size of the element of the array to construct, convert each argument to the element of the array to construct (padded with zeros if needed) and combine all of these elements into the array to construct (padded with zeros if needed). This basically relies on recursion.

    The essence consists of using something like the following to decide on “tuple-like”:

    template< typename T, typename = void >
    struct is_tuple : std::false_type 
    {};
    
    template< typename T >
    struct is_tuple< T,
        std::conditional_t< false,
                            decltype(std::tuple_size< std::decay_t< T > >::value),
                            void > > : std::true_type
    {};
    
    template< typename T >
    constexpr bool is_tuple_v = is_tuple< T >::value;
    

    This allows me to use std::array, std::pair, std::tuple and basically anything supporting structure binding as arguments.

    Note that Blaze already kept 1D arrays (vectors) and 2D arrays (matrices separate).

  7. Klaus Iglberger

    In commit b786653 we have removed the explicit specifier from all vector and matrix constructors taking an initializer_list. You should now be able to use the desired syntax.

  8. Matthias Moulin reporter

    Really nice feature which is consistent with the STD containers with initializer_list (non-explicit) constructors.

  9. Klaus Iglberger

    Summary

    The feature has been implemented, tested, optimized, and documented as required. It is immediately available via cloning the Blaze repository and will be officially released in Blaze 3.7.

    Array Construction

    All dense vector classes offer a constructor for an initialization with a dynamic or static array, or with a std::array. If the vector is initialized from a dynamic array, the constructor expects the actual size of the array as first argument, the array as second argument. In case of a static array or std::array, the fixed size of the array is used:

    const unique_ptr<double[]> array1( new double[2] );
    // ... Initialization of the dynamic array
    blaze::StaticVector<double,2UL> v13( 2UL, array1.get() );
    
    const int array2[4] = { 4, -5, -6, 7 };
    blaze::StaticVector<int,4UL> v14( array2 );
    
    const std::array<float,3UL> array3{ 1.1F, 2.2F, 3.3F };
    blaze::StaticVector<float,3UL> v15( array3 );
    

    Array Assignment

    Dense vectors can also be assigned a static array or std::array:

    blaze::StaticVector<float,2UL> v1;
    blaze::DynamicVector<double,rowVector> v2;
    
    const float array1[2] = { 1.0F, 2.0F };
    const std::array<double,5UL> array2{ 2.1, 4.0, -1.7, 8.6, -7.2 };
    
    v1 = array1;
    v2 = array2;
    
  10. Matthias Moulin reporter

    This is great! Adding C++20’s std::span eventually will now be trivial in the future as the most important constructors are already explicitly present now. Also, switching back and forth between my own array-like container types is very convenient and transparant now: (i) the structure binding support allows me to construct instances of these types from the Blaze vectors without having any Blaze dependencies, and (ii) the std::array constructor allows me to construct Blaze vectors from instances of these types without Blaze having any dependencies to my code. So the only common glue is std::tuple/std::array. 🙂

    One odd thing imho, are the constructors being explicit, while having implicit assignment operators for std::array. Assuming the latter is the desired behavior, the constructor should be implicit as well for consistency. Furthermore, the overloaded std::array assignment operator can be removed in which case the implicit constructor will be called to make a temporary vector which will then be assigned using the move assignment operator.

    There is, however, one caveat, I got to think of, std::array can be initialized with fewer elements than the size. On the other hand, there is the std::initializer_list constructor as well, so that one will be called in case of brace initialization.

  11. Klaus Iglberger

    Hi Matthias!

    You have convinced me (again) to remove the explicit keyword from the new dense vector constructors accepting a std::array (see commit fcb74d2). Thanks for pointing out this inconsistency,

    Best regards,

    Klaus!

  12. Log in to comment