Related
I'm making a tensor class that can make tensors of any order. For example a third order 3 by 2 by 4 float tensor is made using Tensor<float, 3, 2, 4> a. The elements are recursively stored as arrays of lower order tensors, until the 1st order, at which point they're stored as an array of the given type.
template <typename T, int m, int ...n>
class Tensor {
public:
std::array<std::conditional<sizeof...(n) == 0, T, Tensor<T, n...>>, m> a;
};
I have a main loop to test it out.
#include <iostream>
#include <typeinfo>
int main() {
Tensor<float, 3, 2, 4> a;
// Tensor<float, 3> b;
std::cout << typeid(a.a[0]).name() << '\n';
return 0;
}
For some reason, constructing b fails but a succeeds. Also a.a[0] should simply be of type Tensor<float, 2, 4>, but instead it's some conditional type, at runtime. How is this even possible? Don't understand what's going on here, both why b fails with a succeeding (seems impossible) and why the types are conditionals at runtime.
Instantiating std::conditional<sizeof...(n) == 0, T, Tensor<T, n...>> instantiates Tensor<T, n...> even when the condition is true, meaning you give the template one argument instead of the 2+ it requires. Lazy arguments don't exist in C++ for general use (including standard library templates). You need to delay the instantiation of the problematic template so that you instantiate it only when applicable. I find the easiest way to do that is with if constexpr:
template<typename T>
static T type();
static auto determine_element_type() {
if constexpr (sizeof...(n) == 0) {
return type<T>();
} else {
return type<Tensor<T, n...>>();
}
}
std::array<decltype(determine_element_type()), m> a;
Your type of a was off because you used the std::conditional type itself instead of std::conditional_t or typename std::conditional<...>::type to get the resulting type.
std::conditional is a type trait. The way a type trait works, by convention, is that the result of the type-level computation is a member type alias of the std::conditional class, which otherwise is useless. I.e. std::conditional is basically defined like this:
template<bool, typename, typename>
struct conditional;
template<typename T, typename F>
struct conditional<true, T, F> { using type = T; };
template<typename T, typename F>
struct conditional<false, T, F> { using type = F; };
There are also template aliases to reduce typing:
template<bool B, typename T, typename F>
using conditional_t = typename std::conditional<B, T, F>::type;
And you're meant to use it like this
template <typename T, int m, int ...n>
struct Tensor {
std::array<std::conditional_t<sizeof...(n) == 0, T, Tensor<T, n...>>, m> a;
};
This doesn't actually work: if you try to instantiate Tensor<float, 3>, then that tries to instantiate Tensor<float>, and that fails, since Tensor must have at least one int. It doesn't matter that the use of Tensor<float> is under std::conditional_t. std::conditional_t is not like a ? : conditional. It is like a function
Type conditional_t(bool b, Type t, Type f) { return b ? t : f; }
The "arguments" are evaluated before the conditional switches between them. If one of the arguments is ill-formed, the conditional will not save you.
A better design would be specialization of the whole class.
template<typename T, int... ns>
struct tensor;
// a rank-0 tensor is a scalar
template<typename T>
struct tensor<T> {
T value;
};
// a rank-(n+1) tensor is some number of rank-n tensors
template<typename T, int n, int... ns>
struct tensor<T, n, ns...> {
std::array<tensor<T, ns...>, n> value;
};
Not sure what you meant by Tensor<float, 3, 2, 4> working in your version; didn't work for me!
Here's a neat, complete example
You can define the tensor as nested arrays like this:
#include <array>
#include <type_traits>
namespace detail {
template <class T, std::size_t n, std::size_t... ns>
auto helper() {
if constexpr (sizeof...(ns) == 0)
return std::array<T, n>();
else
return std::array<decltype(helper<T, ns...>()), n>();
}
}
template <class T, std::size_t n, std::size_t... ns>
using Tensor = decltype(detail::helper<T, n, ns...>());
int main()
{
Tensor<float, 2, 3, 4> b;
b[0][1][2] = 1;
}
That is, given a constexpr std::array<int,2>{1,2} pass it to some function or helper class that would spit out the type std::integer_sequence<int, 1, 2>?
It seems easy to jump from the type world to the "constexpr value" world (e.g., to do the reverse conversion), but hard or impossible to do the reverse.
It seems like you can do it in C++17, at the cost of introducing a lambda at the call site:
template <size_t N, typename F, size_t... indexes>
constexpr auto make_seq_helper(F f, std::index_sequence<indexes...> is) {
return std::integer_sequence<int, std::get<indexes>(f())...>{};
}
template <typename F>
constexpr auto make_seq(F f) {
constexpr size_t N = f().size();
using indexes = std::make_index_sequence<N>;
return make_seq_helper<N>(f, indexes{});
};
Calling make_seq like this:
constexpr std::array a{7, 15, 28};
auto x = make_seq([](){ return a; });
Results in an x with type std::integer_sequence<int, 7, 15, 28>. I'm not sure if the lambda use can be removed.
If array have external linkage, you might do something like:
template <auto& Arr, size_t... Is>
constexpr auto make_seq_impl(std::index_sequence<Is...>) {
using T = typename std::decay_t<decltype(Arr)>::value_type;
return std::integer_sequence<T, Arr[Is]...>{};
}
template <auto& Arr>
constexpr auto make_seq() {
return make_seq_impl<Arr>(std::make_index_sequence<Arr.size()>());
}
constexpr std::array a{7, 15, 28};
int main()
{
[[maybe_unused]]auto x = make_seq<a>();
static_assert(std::is_same<std::integer_sequence<int, 7, 15, 28>, decltype(x)>::value, "!");
}
Demo
Or, with structure way, you might do:
template <const auto& Arr, typename Seq = std::make_index_sequence<std::size(Arr)>>
struct make_seq;
template <typename T, std::size_t N, const std::array<T, N>& Arr, std::size_t ... Is>
struct make_seq<Arr, std::index_sequence<Is...>>
{
using type = std::integer_sequence<T, Arr[Is]...>;
};
Demo
Here's a C++14 compatible solution.
A trick to "pass" a constexpr std::array (or any other struct/class object) as a template argument is to wrap it into a type:
constexpr std::array<int,3> a{7,15,28};
struct ArrayWrapper_a {
static constexpr auto& value = a;
};
template<typename ArrayWrapper>
struct Foobar {
// do stuff with ArrayWrapper::value
}
Then you can do something similar to BeeOnRope's answer to generate sequences:
template<typename ArrayWrapper, typename Sequence>
struct array_to_sequence_impl;
template<typename ArrayWrapper, std::size_t... indices>
struct array_to_sequence_impl<ArrayWrapper,std::index_sequence<indices...>> {
using value_type = typename std::decay_t<decltype(ArrayWrapper::value)>::value_type;
using type = std::integer_sequence<value_type, std::get<indices>(ArrayWrapper::value)...>;
};
template<typename ArrayWrapper>
using array_to_sequence = typename array_to_sequence_impl<ArrayWrapper,std::make_index_sequence<ArrayWrapper::value.size()>>::type;
Usage:
constexpr std::array<int,3> a{7,15,28};
struct ArrayWrapper_a {
static constexpr auto& value = a;
};
using Sequence_a = array_to_sequence<ArrayWrapper_a>;
Live demo
I recently watched a video which inspired me to write my own neural network system, and I wanted to have the amount of nodes in the network be adjustable.
At first I achieved this at runtime by parsing an array of the numbers of nodes but I was wondering if I could do this at compile time instead. Here's an example of the kind of thing I was hoping to accomplish.
template<int FirstNodes, int SecondNodes, int... OtherNodes>
class Net
{
tuple<Eigen::Matrix<float, FirstNodes, SecondNodes>, ...> m_weights;
// More matricies with the values from the OtherNodes
};
As a more detailed example, Net<784, 16, 16, 10> n; n.m_weight should have type
tuple<Eigen::Matrix<float, 784, 16>,
Eigen::Matrix<float, 16, 16>,
Eigen::Matrix<float, 16, 10>>
From what I know about C++ and constexpr, this should be possible.
I should add that I was able to do
template<int FirstNodes, int SecondNodes, int... OtherNodes>
class Net
{
public:
Net()
{
auto nodes = {FirstNodes, SecondNodes, OtherNodes...};
auto i = nodes.begin();
do
{
// Eigen::Matrix<float, Dynamic, Dynamic>
Eigen::MatrixXf m(*(i++), *i);
} while (i+1 != nodes.end());
}
};
But then I'm just using dynamic matricies again and that isn't what I was hoping for.
Any advice or working examples would be greatly appreciated.
You want some sort of type transformation that given a list of N integers returns a tuple of N - 1 matrices. Here's a C++17 solution:
template <int A, int B, int... Is>
auto make_matrix_tuple()
{
if constexpr(sizeof...(Is) == 0)
{
return std::tuple<Eigen::Matrix<float, A, B>>{};
}
else
{
return std::tuple_cat(make_matrix_tuple<A, B>(),
make_matrix_tuple<B, Is...>());
}
}
live example on wandbox
In C++11, you can implement this type transformation recursively:
template <int... Is>
struct matrix_tuple_helper;
template <int A, int B, int... Rest>
struct matrix_tuple_helper<A, B, Rest...>
{
using curr_matrix = Eigen::Matrix<float, A, B>;
using type =
decltype(
std::tuple_cat(
std::tuple<curr_matrix>{},
typename matrix_tuple_helper<B, Rest...>::type{}
)
);
};
template <int A, int B>
struct matrix_tuple_helper<A, B>
{
using curr_matrix = Eigen::Matrix<float, A, B>;
using type = std::tuple<curr_matrix>;
};
template <int... Is>
using matrix_tuple = typename matrix_tuple_helper<Is...>::type;
C++14 approach:
struct matrix_tuple_maker
{
template <int A, int B, int C, int... Is>
static auto get()
{
return std::tuple_cat(get<A, B>(), get<B, C, Is...>());
}
template <int A, int B>
static auto get()
{
return std::tuple<Eigen::Matrix<float, A, B>>{};
}
};
static_assert(std::is_same_v<
decltype(matrix_tuple_maker::get<784, 16, 16, 10>()),
std::tuple<Eigen::Matrix<float, 784, 16>,
Eigen::Matrix<float, 16, 16>,
Eigen::Matrix<float, 16, 10>>
>);
It seems to me that you need two list of integers, out of phase of 1.
If you define a trivial template integer container (in C++14 you can use a std::integer_sequence)
template <int...>
struct iList
{ };
you can define a base class as follows (sorry: used foo instead of Eigen::Matrix)
template <typename, typename, typename = std::tuple<>>
struct NetBase;
// avoid the first couple
template <int ... Is, int J0, int ... Js>
struct NetBase<iList<0, Is...>, iList<J0, Js...>, std::tuple<>>
: NetBase<iList<Is...>, iList<Js...>, std::tuple<>>
{ };
// intermediate case
template <int I0, int ... Is, int J0, int ... Js, typename ... Ts>
struct NetBase<iList<I0, Is...>, iList<J0, Js...>, std::tuple<Ts...>>
: NetBase<iList<Is...>, iList<Js...>,
std::tuple<Ts..., foo<float, I0, J0>>>
{ };
// avoid the last couple and terminate
template <int I0, typename ... Ts>
struct NetBase<iList<I0>, iList<0>, std::tuple<Ts...>>
{ using type = std::tuple<Ts...>; };
and Net simply become (observe out of phase couple of integer lists)
template <int F, int S, int... Os>
struct Net : NetBase<iList<0, F, S, Os...>, iList<F, S, Os..., 0>>
{ };
The following is a full compiling example
#include <tuple>
template <int...>
struct iList
{ };
template <typename, int, int>
struct foo
{ };
template <typename, typename, typename = std::tuple<>>
struct NetBase;
// avoid the first couple
template <int ... Is, int J0, int ... Js>
struct NetBase<iList<0, Is...>, iList<J0, Js...>, std::tuple<>>
: NetBase<iList<Is...>, iList<Js...>, std::tuple<>>
{ };
// intermediate case
template <int I0, int ... Is, int J0, int ... Js, typename ... Ts>
struct NetBase<iList<I0, Is...>, iList<J0, Js...>, std::tuple<Ts...>>
: NetBase<iList<Is...>, iList<Js...>,
std::tuple<Ts..., foo<float, I0, J0>>>
{ };
// avoid the last couple and terminate
template <int I0, typename ... Ts>
struct NetBase<iList<I0>, iList<0>, std::tuple<Ts...>>
{ using type = std::tuple<Ts...>; };
template <int F, int S, int... Os>
struct Net : NetBase<iList<0, F, S, Os...>, iList<F, S, Os..., 0>>
{ };
int main()
{
static_assert(std::is_same<
typename Net<784, 16, 16, 10>::type,
std::tuple<foo<float, 784, 16>, foo<float, 16, 16>,
foo<float, 16, 10>>>{}, "!");
}
Here is another C++14 solution. I consider it worth posting because it is non-recursive and readable.
#include <tuple>
#include <utility>
template<class, int, int> struct Matrix {};
template<int... matsizes, std::size_t... matinds>
constexpr auto make_net(
std::integer_sequence<int, matsizes...>,
std::index_sequence<matinds...>
) {
constexpr int sizes[] = {matsizes...};
return std::tuple< Matrix<float, sizes[matinds], sizes[1+matinds]>... >{};
}
template<int... matsizes>
constexpr auto make_net(
std::integer_sequence<int, matsizes...> sizes
) {
static_assert(sizes.size() >= 2, "");
constexpr auto number_of_mats = sizes.size() - 1;
return make_net(sizes, std::make_index_sequence<number_of_mats>{});
}
int main () {
auto net = make_net(std::integer_sequence<int, 784, 16, 16, 10>{});
using Net = decltype(net);
static_assert(
std::is_same<
std::tuple<
Matrix<float, 784, 16>,
Matrix<float, 16, 16>,
Matrix<float, 16, 10>
>,
Net
>{}, ""
);
return 0;
}
I have Vector (CVector<T, std::size_t Size>), Matrix (CMatrix<T, std::size_t Height, std::size_t Width>) and Tensor (CTensor<T, std::size_t... Sizes>) classes, and I wish to be able to implicitly convert from a CTensor class to a CVector class if sizeof...(Sizes) == 1 and to a CMatrix class if sizeof...(Sizes) == 2, so I have the following conversion operators (initially I didn't have the std::enable_if template parameter hoping I could use SFINAE to prevent it from compiling):
template <typename std::enable_if<sizeof...(Sizes) == 2, int>::type = 0>
operator CMatrix<NumType, Sizes...>() const
{
static_assert(sizeof...(Sizes) == 2, "You can only convert a rank 2 tensor to a matrix");
CMatrix<NumType, Sizes...> matResult;
auto& arrThis = m_numArray;
auto& arrResult = matResult.m_numArray;
concurrency::parallel_for_each( arrResult.extent, [=, &arrThis, &arrResult]( concurrency::index<2> index ) restrict( amp ) {
arrResult[index] = arrThis[index];
} );
return matResult;
}
template <typename std::enable_if<sizeof...(Sizes) == 1, int>::type = 0>
operator CVector<NumType, Sizes...>() const
{
static_assert(sizeof...(Sizes) == 1, "You can only convert a rank 1 tensor to a vector");
CVector<NumType, Sizes...> vecResult;
auto& arrThis = m_numArray;
auto& arrResult = vecResult.m_numArray;
concurrency::parallel_for_each( arrResult.extent, [=, &arrThis, &arrResult]( concurrency::index<1> index ) restrict( amp ) {
arrResult[index] = arrThis[index];
} );
return vecResult;
}
However, if I instantiate CTensor<float, 3, 3, 3> for instance, and try to compile, I will be presented with errors declaring that there are too many template parameters for CMatrix and CVector along with errors regarding missing type for std::enable_if<false, int>. Is there a way to implement these operators without having to specialize CTensor for rank 1 and 2?
I have simplified my previous solution, details below.
SFINAE is not needed at all because you have static_assert in template method which is instantiated only upon usage.
My solution makes the conversion operator a template method with dependant argument (so that compiler does not instantiate its body, only parses signature), and adds -1 size that pretends to be missing dimension within tensor of size 1 (not to the tensor itself, but to helper class that extracts parameter pack), to allow compiler instantiate the tensor template itself, but will not allow later on to instantiate the conversion operator within tensor of invalid dimension.
Live demo link.
#include <cstddef>
template <typename T, unsigned int index, T In, T... args>
struct GetArg
{
static const T value = GetArg<T, index-1, args...>::value;
};
template <typename T, T In, T... args>
struct GetArg<T, 0, In, args...>
{
static const T value = In;
};
template <typename T, T In>
struct GetArg<T, 1, In>
{
static const T value = -1;
};
template <typename T, std::size_t Size>
struct CVector
{
};
template <typename T, std::size_t Height, std::size_t Width>
struct CMatrix
{
};
template <typename T, std::size_t... Sizes>
struct CTensor
{
template <std::size_t SZ = sizeof...(Sizes)>
operator CVector<T, GetArg<std::size_t, 0, Sizes...>::value>() const
{
static_assert(SZ == 1, "You can only convert a rank 1 tensor to a vector");
CVector<T, Sizes...> vecResult;
return vecResult;
}
template <std::size_t SZ = sizeof...(Sizes)>
operator CMatrix<T, GetArg<std::size_t, 0, Sizes...>::value, GetArg<std::size_t, 1, Sizes...>::value>() const
{
static_assert(SZ == 2, "You can only convert a rank 2 tensor to a matrix");
CMatrix<T, Sizes...> matResult;
return matResult;
}
};
int main()
{
CTensor<float, 3> tensor3;
CTensor<float, 3, 3> tensor3_3;
CTensor<float, 3, 3, 3> tensor3_3_3;
CVector<float, 3> vec(tensor3);
//CVector<float, 3> vec2(tensor3_3); // static_assert fails!
CMatrix<float, 3, 3> mat(tensor3_3);
//CMatrix<float, 3, 3> mat2(tensor3_3_3); // static_assert fails!
}
Here is how you could do it with static_assert:
template <typename NumType,size_t... Sizes>
struct CTensor {
template<size_t n,size_t m>
operator CMatrix<NumType,n,m>() const
{
static_assert(
sizeof...(Sizes)==2,
"You can only convert a rank 2 tensor to a matrix"
);
static_assert(
std::is_same<CTensor<NumType,n,m>,CTensor>::value,
"Size mismatch"
);
...
}
template<size_t n>
operator CVector<NumType,n>() const
{
static_assert(
sizeof...(Sizes)==1,
"You can only convert a rank 1 tensor to a vector"
);
static_assert(
std::is_same<CTensor<NumType,n>,CTensor>::value,
"Size mismatch"
);
...
}
};
or with SFINAE:
template <typename NumType,size_t... Sizes>
struct CTensor {
template<size_t n,size_t m,
typename =
typename std::enable_if<
std::is_same<CTensor<NumType,n,m>,CTensor>::value, int
>::type
>
operator CMatrix<NumType,n,m>() const
{
...
}
template<size_t n,
typename =
typename std::enable_if<
std::is_same<CTensor<NumType,n>,CTensor>::value, int
>::type
>
operator CVector<NumType,n>() const
{
...
}
};
And here is another approach using function overloading:
template <typename NumType,size_t... Sizes>
struct CTensor {
template<size_t n,size_t m>
CMatrix<NumType,n,m> convert() const
{
...
}
template<size_t n>
CVector<NumType,n> convert() const
{
...
}
template <typename T>
operator T() const { return convert<Sizes...>(); }
};
This is actually longer description of my comment: Why not having CTensor only and aliasing it as CVector / CMatrix? No conversion needed, they will become the same.
...it is solving the real problem in a completely different way than the title asks for. Just for the record :)
1) Hiding base implementation in namespace detail
2) Specializing what really needs to be specialized
(this can be done by some helper struct as well - specializing the struct providing the method)
3) Aliasing CVector/CMatrix as CTensor (no need for the operator then)
#include <vector>
namespace detail {
template<class T, std::size_t... Sizes>
class base;
template<class T, std::size_t Size>
class base<T, Size> {
std::vector<T> data;
public:
T& operator[](std::size_t i) {
return data[i]; }
};
template<class T, std::size_t First, std::size_t... More>
class base<T, First, More...> {
std::vector<base<T, More...>> data;
public:
// this could be done better, just an example
base<T, More...>& operator[](std::size_t i) {
return data[i]; }
};
}
template<class T, std::size_t... Sizes>
class CTensor: public detail::base<T, Sizes...> {};
//we can specialize CTensor<T, Size>
//and CTensor<T, Width, Height> here
template<class T, std::size_t Size>
using CVector = CTensor<T, Size>;
template<class T, std::size_t Width, std::size_t Height>
using CMatrix = CTensor<T, Width, Height>;
Make sizeof...(Sizes) a dependant argument, and make the type CMatrix/CVector correct (taking the correct number of template parameter).
Using:
template <std::size_t ... Is> struct index_sequence {};
template <std::size_t I, typename T> struct index_element;
template <std::size_t I, std::size_t ... Is>
struct index_element<I, index_sequence<Is...> >
{
private:
static constexpr const std::size_t a[] = {Is...};
public:
static_assert(I < sizeof...(Is), "out of bound");
static constexpr const std::size_t value = a[I];
};
then you may do:
template <
std::size_t N = sizeof...(Sizes),
typename std::enable_if<N == 1, int>::type = 0>
operator CVector<
T,
index_element<0, index_sequence<Sizes..., 0>
>::value>() const
{
// Your implementation
}
template <
std::size_t N = sizeof...(Sizes),
typename std::enable_if<N == 2, int>::type = 0>
operator CMatrix<
T,
index_element<0, index_sequence<Sizes..., 0>>::value
index_element<1, index_sequence<Sizes..., 0, 0>>::value
>() const
{
// Your implementation
}
I have a template class with the following specification:
template <typename T, size_t... Dims> class Array;
And say it can be used as follows:
// Define a 2X3X4 array of integers. Elements are uninitialized.
Array<int, 2, 3, 4> a, b;
Array<short, 2, 3, 4> c;
Array<int, 0> e1; // This line must cause a compile-time error.
How can I achieve this functionality? I thought if I could extract all the argument list I could then create the n-dimentional array as a straight forward recursive call. How can I do it now
You can create a compile-time trait that does what you want:
#include <type_traits>
template <std::size_t... Ts>
struct not_zero {};
template <std::size_t N>
struct not_zero<N> : std::integral_constant<bool, N> {};
template <std::size_t N, std::size_t... Ts>
struct not_zero<N, Ts...> : std::integral_constant<bool, N && not_zero<Ts...>::value> {};
template <typename T, std::size_t... Ts>
struct Array
{
static_assert(not_zero<Ts...>::value, "Dimension cannot be 0");
};
template struct Array<int, 3>; // OK
template struct Array<int, 3, 2, 1, 0>; // error: static assertion failed: Dimension cannot be 0
See a demo here.
Create a specialization for it:
#include <iostream>
template <typename T, std::size_t... Dims>
struct Array {};
template <typename T>
struct Array<T, 0>; // leave as an incomplete type
int main()
{
Array<int, 3> x; // OK
Array<int, 0> y; // error: aggregate ‘Array<int, 0u> y’ has incomplete type and cannot be defined
}
Something like this maybe
namespace mine {
template<typename T, size_t first, size_t... rest>
struct multi_array__ {
enum { dims = sizeof...(rest) };
static_assert(first,"dimension can not be zero!");
typedef std::array< typename multi_array__<T, rest...>::type, first > type;
};
template<typename T, size_t first>
struct multi_array__<T,first> {
enum { dims = 1 };
static_assert(first,"dimension can not be zero!");
typedef std::array<T,first> type;
};
template <typename T, std::size_t... ds>
using multi_array = typename multi_array__<T, ds ...>::type;
};
You can use it like this
mine::multi_array <int,2,3,4> arr1 = {};
// multi_array<int,2,3,0,4> arr3; // This fails
mine::multi_array <int,3,4> arr2 = {};
Assignments like this work too
arr2[0] = arr1[0][0];
Here is a simple test program.