C++17 Limit type of fold expression for initialization of a template-class
Asked Answered
H

2

7

I basically try to write my own game engine for practice and personal use (I know, it's a nearly impossible task, but as I said, it's mostly for learning new things).

Currently, I'm working on my math library (mainly vectors and matrices) and I came across an interesting, but mostly, aesthetic problem.

The following pseudo-code is given:

template <uint8 size>
struct TVector {
    float elements[size];
};

Now I want to be able to construct the struct with the required amount of floats as parameters:

TVector<3> vec0(1.0f, 2.5f, -4.0f); 
TVector<2> vec1(3.0f, -2.0f);

TVector<3> vec2(2.0f, 2.2f); // Error: arg missing 
TVector<2> vec3(1.0f, 2.0f, 3.0f) // Error: too many args

Since the size of array is given by the template parameter, I struggled with declaring a fitting constructor for the struct. My ultimate goal would be something like this:

// This is pseudo-ideal-code
TVector(size * (float value)); // Create a constructor with number of size 
                               // parameters, which are all floats

Of course, this is non-logical syntax, but the closest thing I achieved in that manner was with C++17 fold expressions:

template<typename... Args>
    TVector(Args... values) {
        static_assert(sizeof...(values) <= size, "Too many args");
        uint8 i = 0;
        (... , void(elements[i++] = values));
    }

It works perfectly fine in sense of filling the array and (I guess) is not much overhead, but it is also error-prone for the programmer who uses this struct, since it gives no direct indication of how many arguments the constructor takes in.

Furthermore, it does not specify which type of the arguments should be and this is my biggest problem here.

Why is it a problem if it works?

Imagine having the following struct, which uses the TVector struct:

template <const uint8 rows, const uint8 columns>
struct TMatrix {
    // elements[-columns-][-rows-]; 
    TVector<rows> elements[columns];
}

Given, that the constructor is similar to the fold expression of the vector struct, I want to be able to construct the matrix with the accordingly sized vectors or brace initialization.

  1. The aggregate initialization does not work.

    TVector<2> vec(1.0f, 3.0f);
    TMatrix<2, 2> mat0(vec, vec); // Works
    TMatrix<2, 2> mat1(vec, {0.2f, -4.2f}); // Error
    // Does not compile, because the Type is not clear
    
  2. It does not show an error until compilation when given the wrong parameters (like a vector with a wrong size, that would not fit as column of the matrix).

  3. The source of the error is not always clear.

TL;DR: Now finally to my real question here:

Is there a way to limit the type of a fold expression, ultimately not using templates at all and solving my 3 problems given above?

I imagine something like:

TVector(float... values) { 
// Maybe even specify the size of the pack with the size given in the struct template
    uint8 i = 0;
    (... , void(elements[i++] = values));
}

And:

TMatrix(const TVector<rows>&... values) {
    uint8 i = 0;
    (..., void(elements[i++] = values));
}

Of course, I am being very picky here and this is mostly an aesthetic problem, but I think it is an important design decision, which could really improve code usability.


Thank you for reading this and helping me with my first question here :)

Heterocyclic answered 16/1, 2018 at 17:27 Comment(1)
You could create deduction guide with enable if if all of the types are not convertible to doubles.Dome
H
4

So after diving into template metaprogramming and trying out things, I came across some solutions (all with their own little problems).

std::initializer_list:

Pros:

  • Easy to implement:

    // Constructors:
    TVector(std::initalizer_list<float> values);
    TMatrix(std::initalizer_list<TVector<rows>> values);
    
  • Brace initialization:

    TVector<3>    vec { 1.0f, 0.0f, 2.0f };
    TMatrix<3, 3> mat { vec, { 3.0f, 4.0f, 1.0f }, vec };
    

Cons:

  • Copy overhead
  • Values can't be moved
  • Does not specify the allowed number of parameters
  • I suggest Andrzej's C++ Blog on that topic: The cost of std::initializer_list

std::array

Pros:

  • Easy to implement:

    // Constructors:
    TVector(std::array<float, size>&& values);
    TMatrix(std::aray<TVector<rows>, columns>&& values);
    
  • Movable if the objects in the array are movable

Cons:

  • Brace initialization is extremely ugly

    TVector<3>    vec { { 1.0f, 0.0f, 2.0f } };
    TMatrix<3, 3> mat { vec, TVector<3>{ { 3.0f, 4.0f, 1.0f } }, vec };
    

Fold expression - My prefered solution

Pros:

  • No overhead
  • Movable
  • Uses uniform initialization

    TVector<3>    vec { 1.0f, 0.0f, 2.0f };
    TMatrix<3, 3> mat { vec, TVector<3>{ 3.0f, 4.0f, 1.0f }, vec };
    
  • Can be specified for the need of the constructor

Cons:

  • Hard to implement and to specify
  • Does not allow nested braces without specifying the type (as far as I can tell)

    // Constructors:
    template<typename... Args, std::enable_if_t<
        is_pack_convertible<float, Args...>::value && 
        is_pack_size_of<columns, Args...>::value, bool> = false >
    TVector(std::array<float, size>&& values);
    
    template<typename... Args, std::enable_if_t<
        is_pack_convertible<Vector<rows>, Args...>::value && 
        is_pack_size_of<columns, Args...>::value, bool> = false >
    TMatrix(std::aray<TVector<rows>, columns>&& values);
    

is_pack_convertible / is_pack_size_of

// Declaration - checks if all types of a pack are convertible to one type
template <typename To, typename... Pack> struct is_pack_convertible;
// End of pack case
template <typename To> struct is_pack_convertible<To> : std::true_type {};
// Recursive bool &&
template <typename To, typename From, typename... Pack>
struct is_pack_convertible<To, From, Pack...> {
    static constexpr bool value = std::is_convertible<From, To>::value
        && is_pack_convertible<To, Pack...>::value;
};

// Declaration - checks if the pack is equal to a certain size
template <size_t size, typename... Pack> struct is_pack_size_of;
// End of pack: size is equal
template <> struct is_pack_size_of<0> : std::true_type {};
// End of pack: size is not equal
template <size_t remainder> struct is_pack_size_of<remainder> : std::false_type {};
// Count down size for every element in pack
template <size_t size, typename Arg, typename... Pack> 
struct is_pack_size_of<size, Arg, Pack...> {
    static constexpr bool value = is_pack_size_of<size - 1, Pack...>::value;
};

I hope this helps other people and gives a brief overview of the options when initializing generic classes.

Heterocyclic answered 23/1, 2018 at 16:13 Comment(0)
R
3

With indirection, you may do something like:

template <typename Seq> struct TVectorImpl;

template <std::size_t, typename T> using force_type = T;

template <std::size_t ... Is>
struct TVectorImpl<std::index_sequence<Is...>>
{
    TVectorImpl(force_type<Is, float>... args) : elements{args...} {}

    float elements[sizeof...(Is)];
};

template <std::size_t size>
using TVector = TVectorImpl<decltype(std::make_index_sequence<size>())>;

That avoids to have also template method (so construct as {2.4, 5} works).

Demo

Note that it is in C++14 (and index_sequence can be done in C++11).

Refrigerator answered 16/1, 2018 at 18:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.