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.
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.
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.
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
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).
The source of the error is not always clear.
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 :)
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).
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)
Variadic templates and, in particular, fold expressions enable it to write concise expressions for repeated operations. Learn more about it in my next post.
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.
So after diving into template metaprogramming and trying out things, I came across some solutions (all with their own little problems).
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:
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 };
Pros:
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 };
Cons:
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);
// 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.
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With