Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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 :)

like image 214
Plebshot Avatar asked Jan 16 '18 17:01

Plebshot


People also ask

What are fold expressions in C++17?

C++17 brought fold expressions to the language. This interesting feature allows to write expressive code, that almost seems magical. Here is a two-posts recap on how fold expressions work (this post) and how they can improve your code (the next post).

What is C++17 template deduction?

C++17 filled a gap in the deduction rules for templates. Now the template deduction can happen for standard class templates and not just for functions. For instance, the following code is (and was) legal: Because std::make_pair is a template function (so we can perform template deduction). But the following wasn’t (before C++17)

What are variadic templates and fold expressions?

Variadic templates and, in particular, fold expressions enable it to write concise expressions for repeated operations. Learn more about it in my next post.

Do we need to write type explicitly in C++17?

with C++17 it’s a bit simpler: So no need to write Type explicitly. As one of the advanced uses a lot of papers/blogs/talks point to an example of Heterogeneous compile time list: Before C++17 it was not possible to declare such list directly, some wrapper class would have to be provided first.


2 Answers

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.

like image 160
Plebshot Avatar answered Oct 16 '22 19:10

Plebshot


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).

like image 42
Jarod42 Avatar answered Oct 16 '22 20:10

Jarod42