Implementing std::array-like constructors in other classes - c++

In all the modern C++ compilers I've worked with, the following is legal:
std::array<float, 4> a = {1, 2, 3, 4};
I'm trying to make my own class that has similar construction semantics, but I'm running into an annoying problem. Consider the following attempt:
#include <array>
#include <cstddef>
template<std::size_t n>
class float_vec
{
private:
std::array<float, n> underlying_array;
public:
template<typename... Types>
float_vec(Types... args)
: underlying_array{{args...}}
{
}
};
int main()
{
float_vec<4> v = {1, 2, 3, 4}; // error here
}
When using int literals like above, the compiler complains it can't implicitly convert int to float. I think it works in the std::array example, though, because the values given are compile-time constants known to be within the domain of float. Here, on the other hand, the variadic template uses int for the parameter types and the conversion happens within the constructor's initializer list where the values aren't known at compile-time.
I don't want to do an explicit cast in the constructor since that would then allow for all numeric values even if they can't be represented by float.
The only way I can think of to get what I want is to somehow have a variable number of parameters, but of a specific type (in this case, I'd want float). I'm aware of std::initializer_list, but I'd like to be able to enforce the number of parameters at compile time as well.
Any ideas? Is what I want even possible with C++11? Anything new proposed for C++14 that will solve this?

A little trick is to use constructor inheritance. Just make your class derive from another class which has a pack of the parameters you want.
template <class T, std::size_t N, class Seq = repeat_types<N, T>>
struct _array_impl;
template <class T, std::size_t N, class... Seq>
struct _array_impl<T, N, type_sequence<Seq...>>
{
_array_impl(Seq... elements) : _data{elements...} {}
const T& operator[](std::size_t i) const { return _data[i]; }
T _data[N];
};
template <class T, std::size_t N>
struct array : _array_impl<T, N>
{
using _array_impl<T, N>::_array_impl;
};
int main() {
array<float, 4> a {1, 2, 3, 4};
for (int i = 0; i < 4; i++)
std::cout << a[i] << std::endl;
return 0;
}
Here is a sample implementation of the repeat_types utility. This sample uses logarithmic template recursion, which is a little less intuitive to implement than with linear recursion.
template <class... T>
struct type_sequence
{
static constexpr inline std::size_t size() noexcept { return sizeof...(T); }
};
template <class, class>
struct _concatenate_sequences_impl;
template <class... T, class... U>
struct _concatenate_sequences_impl<type_sequence<T...>, type_sequence<U...>>
{ using type = type_sequence<T..., U...>; };
template <class T, class U>
using concatenate_sequences = typename _concatenate_sequences_impl<T, U>::type;
template <std::size_t N, class T>
struct _repeat_sequence_impl
{ using type = concatenate_sequences<
typename _repeat_sequence_impl<N/2, T>::type,
typename _repeat_sequence_impl<N - N/2, T>::type>; };
template <class T>
struct _repeat_sequence_impl<1, T>
{ using type = T; };
template <class... T>
struct _repeat_sequence_impl<0, type_sequence<T...>>
{ using type = type_sequence<>; };
template <std::size_t N, class... T>
using repeat_types = typename _repeat_sequence_impl<N, type_sequence<T...>>::type;

First of what you are seeing is the default aggregate initialization. It has been around since the earliest K&R C. If your type is an aggregate, it supports aggregate initialization already. Also, your example will most likely compile, but the correct way to initialize it is std::array<int, 3> x ={{1, 2, 3}}; (note the double braces).
What has been added in C++11 is the initializer_list construct which requires a bit of compiler magic to be implemented.
So, what you can do now is add constructors and assignment operators that accept a value of std::initializer_list and this will offer the same syntax for your type.
Example:
#include <initializer_list>
struct X {
X(std::initializer_list<int>) {
// do stuff
}
};
int main()
{
X x = {1, 2, 3};
return 0;
}
Why does your current approach not work? Because in C++11 std::initializer_list::size is not a constexpr or part of the initializer_list type. You cannot use it as a template parameter.
A few possible hacks: make your type an aggregate.
#include <array>
template<std::size_t N>
struct X {
std::array<int, N> x;
};
int main()
{
X<3> x = {{{1, 2, 3}}}; // triple braces required
return 0;
}
Provide a make_* function to deduce the number of arguments:
#include <array>
template<std::size_t N>
struct X {
std::array<int, N> x;
};
template<typename... T>
auto make_X(T... args) -> X<sizeof...(T)>
// might want to find the common_type of the argument pack as well
{ return X<sizeof...(T)>{{{args...}}}; }
int main()
{
auto x = make_X(1, 2, 3);
return 0;
}

If you use several braces to initialize the instance, you can leverage list-init of another type to accept these conversions for compile-time constants. Here's a version that uses a raw array, so you only need parens + braces for construction:
#include <array>
#include <cstddef>
template<int... Is> struct seq {};
template<int N, int... Is> struct gen_seq : gen_seq<N-1, N-1, Is...> {};
template<int... Is> struct gen_seq<0, Is...> : seq<Is...> {};
template<std::size_t n>
class float_vec
{
private:
std::array<float, n> underlying_array;
template<int... Is>
constexpr float_vec(float const(&arr)[n], seq<Is...>)
: underlying_array{{arr[Is]...}}
{}
public:
constexpr float_vec(float const(&arr)[n])
: float_vec(arr, gen_seq<n>{})
{}
};
int main()
{
float_vec<4> v0 ({1, 2, 3, 4}); // fine
float_vec<4> v1 {{1, 2, 3, 4}}; // fine
float_vec<4> v2 = {{1, 2, 3, 4}}; // fine
}

Explicitly specify that the data type for the initialization to floating point type. You can do this by doing "1.0f" instead of putting "1". If it is a double, put "1.0d". If it is a long, put "1l" and for unsigned long put "1ul" and so on..
I've tested it here: http://coliru.stacked-crooked.com/a/396f5d418cbd3f14
and here: http://ideone.com/ZLiMhg
Your code was fine. You just initialized the class a bit incorrect.
#include <array>
#include <cstddef>
#include <iostream>
template<std::size_t n>
class float_vec
{
private:
std::array<float, n> underlying_array;
public:
template<typename... Types>
float_vec(Types... args)
: underlying_array{{args...}}
{
}
float get(int index) {return underlying_array[index];}
};
int main()
{
float_vec<4> v = {1.5f, 2.0f, 3.0f, 4.5f}; //works fine now..
for (int i = 0; i < 4; ++i)
std::cout<<v.get(i)<<" ";
}

Related

How would I get this functionality in my matrix class? [duplicate]

This question already has an answer here:
List initialization, Initializer_list and related questions in C++
(1 answer)
Closed 3 years ago.
#include <iostream>
#include <array>
template<typename T, std::size_t R, std::size_t C>
class matrix
{
std::array<T, R * C> m_data;
};
int main()
{
matrix<float, 2, 2> a = { 1,2,3,4 }; // COMPILER ERROR!
}
Clang reports that there is no matching constructor!
I have tried writing a constructor of the form
matrix(std::array<T,R*C> a);
and tried experimenting with && as I think the right-hand side of the expression in question is temporary. Which leads me to some confusion. As we would expect that it would be created then assigned(!) to the value of a.
Like others mentioned in the comments, you need a std::initializer_list<T> constructor for your matrix class.
#include <array> // std::array
#include <initializer_list> // std::initializer_list
#include <algorithm> // std::copy
#include <cassert>
template<typename T, std::size_t R, std::size_t C>
class matrix
{
std::array<T, R * C> m_data;
public:
matrix(const std::initializer_list<T> list)
// ^^^^^^^^^^^^^^^^^^^^^^^ --> constructor which takes std::initializer_list
{
assert(R * C == list.size());
std::copy(list.begin(), list.end(), m_data.begin());
}
};
int main()
{
matrix<float, 2, 2> a = { 1,2,3,4 }; // now compiles
}
(See live online)
However, that is not compile-time and come ups with the following drawbacks:
It does not check the passed types are same.
Size of the passed list can not be checked compile-time, as std::initializer_list is/can not be compile time.
Thirdly, it allows narrowing conversion.
In order to disallow the above, one solution is to provide a variadic template constructor which enables the above checks compile-time.
Something like as follows:(See live online)
#include <array> // std::array
#include <initializer_list> // std::initializer_list
#include <type_traits> // std::conjunction, std::is_same
#include <utility> // std::forward
// traits for checking the types (requires C++17)
template <typename T, typename ...Ts>
using are_same_types = std::conjunction<std::is_same<T, Ts>...>;
template<typename T, std::size_t R, std::size_t C>
class matrix
{
std::array<T, R * C> m_data;
public:
template<typename... Ts>
constexpr matrix(Ts&&... elemets) noexcept
{
static_assert(are_same_types<Ts...>::value, "types are not same!");
static_assert(sizeof...(Ts) == R*C, "size of the array does not match!");
m_data = std::array<T, R * C>{std::forward<Ts>(elemets)...};
}
};
int main()
{
matrix<float, 2, 2> a{ 1.f,2.f,3.f,4.f }; // now compiles
// matrix<float, 2, 2> a1{ 1,2,3,4 }; // error: narrowing conversion!
// matrix<float, 2, 2> a1{ 1.f,2.f,3.f, 4 }; // error: types are not same!
// matrix<float, 2, 2> a1{ 1.f,2.f,3.f,4.f, 5.f }; // error: size of the array does not match!
}
You don't need a std::initializer_list<T>.
One of it's drawbacks is that it doesn't check the number of arguments you pass it.
template<typename T, std::size_t R, std::size_t C>
class matrix
{
public:
matrix(const std::initializer_list<T> list) { /*...*/ }
};
int main()
{
matrix<float, 2, 2> a = { 1,2,3,4 }; // compiles
matrix<float, 2, 2> b = { 1,2,3,4,5 }; // also compiles
}
Instead, use the following:
template<typename T, std::size_t R, std::size_t C>
class matrix
{
std::array<T, R*C> m_data;
public:
matrix() = default;
template<typename... Us>
matrix(Us &&...args) : m_data{static_cast<T>(args)...}
{
}
};
int main()
{
matrix<float,2,2> a = {1,2,3}; // ok
matrix<float,2,2> b = {1,2,3,4}; // ok
matrix<float,2,2> c = {1,2,3,4,5}; // error
}
The cast is necessary to avoid a potential narrowing conversion.

Transpose a container of structs

Let we have a struct Record{uint8_t x, y;};, a container Container<Record> of structs and a struct Transposed{Container<uint8_t> x,y};. Container c is a template which first arg is a type of value, all of the rest args have default values. For example it can be a std::vector (the rest args are types) or std::span (the rest arg is a value). The template should work with all of them. Also we may like to pass tne rest of template arguments to underlying template.
How we can get the Transposed from the container using templates?
I have tried variadic templates,
#include <iostream>
#include <vector>
template <typename value_type=uint8_t> struct Record{
value_type x, y;
};
template<typename value_type, typename value_type2=uint8_t> class ContainerA: public std::vector<value_type>{
value_type2 b=1u;
};
template<typename value_type, uint8_t int_value=1u> class ContainerB: public std::vector<value_type>{};
template<typename value_type, template <typename ...> typename container_type> class Transposed{
container_type<value_type> x, y;
public:
Transposed(container_type<Record<value_type>> & recs){
x.reserve(recs.size());
y.reserve(recs.size());
x.resize(recs.size());
y.resize(recs.size());
size_t i=0;
for(auto &rec :recs){
x[i] = rec.x;
y[i] = rec.y;
++i;
}
}
};
int main(){
std::vector<Record<uint8_t>> recsV{
{1, 2},
{3, 4}
};
Transposed trV{recsV};
std::cout<<"vec"<<std::endl;
ContainerA<Record<uint8_t>> recsA{
{1, 2},
{3, 4}
};
Transposed trA{recsA};
std::cout<<"A"<<std::endl;
/*ContainerB<Record<uint8_t>> recsB{
{1, 2},
{3, 4}
};
Transposed trB{recsB};
std::cout<<"B"<<std::endl;*/
return 0;
}
but it seems they cannot match both types and values. Usage of more than 1 variadic template argument is not allowed. Is it a flaw in C++ language or a deliberate design choice and do we need something like any_template_arg keyword or should just specifying 2 variadic arguments of different types be allowed to allow this use case?
As far I know, there isn't a way to match types and values (and template-template) template arguments together. And I've searched it for a long time.
So I don't see a way to make what do you want in a simple and elegant way.
Trying to respond to your question
How we can get the Transposed from the container using templates?
the best I can imagine is declare (isn't necessary define them taking in count are used only inside a decltype()) a couple of trivial functions as follows
template <typename VT,
template <typename...> typename CT>
CT<VT> extract_func (CT<Record<VT>>);
template <typename VT,
template <typename, auto...> typename CT>
CT<VT> extract_func (CT<Record<VT>>);
The first one is to remove the Record part when the CT container accepts a variadic list of types (std::vector and ContainerA cases); the second one is for CT containers accepting (after the type parameter) one or more values (ContainerB case).
Obviously this doen't cover all possible cases, but it's trivial declare other extract_func() functions to cover other cases.
Now you can declare Tranposed as follows
template <typename T,
typename CT = decltype(extract_func(std::declval<T>()))>
class Transposed
Observe that, now, Transposed accept a generic type T but is SFINAE enabled only when the T type matches (as argument) an extract_func() declaration.
In the Transposed body you can use CT to declare x and y and T for the argument of the constructor.
The following is a full compiling example
#include <iostream>
#include <vector>
template <typename value_type=std::uint8_t>
struct Record
{ value_type x, y; };
template <typename value_type, typename value_type2=std::uint8_t>
class ContainerA : public std::vector<value_type>
{ value_type2 b=1u; };
template <typename value_type, std::uint8_t int_value=1u>
class ContainerB : public std::vector<value_type>
{ };
template <typename VT,
template <typename...> typename CT>
CT<VT> extract_func (CT<Record<VT>>);
template <typename VT,
template <typename, auto...> typename CT>
CT<VT> extract_func (CT<Record<VT>>);
template <typename T,
typename CT = decltype(extract_func(std::declval<T>()))>
class Transposed
{
private:
CT x, y;
public:
Transposed (T & recs)
{
x.reserve(recs.size());
y.reserve(recs.size());
x.resize(recs.size());
y.resize(recs.size());
std::size_t i=0u;
for(auto &rec :recs){
x[i] = rec.x;
y[i] = rec.y;
++i;
}
}
};
int main ()
{
std::vector<Record<std::uint8_t>> recsV { {1, 2}, {3, 4} };
Transposed trV{recsV};
std::cout<<"vec"<<std::endl;
ContainerA<Record<std::uint8_t>> recsA { };
Transposed trA{recsA};
std::cout<<"A"<<std::endl;
ContainerB<Record<std::uint8_t>> recsB { };
Transposed trB{recsB};
std::cout<<"B"<<std::endl;
}
One working example for you:
template<typename value_type=uint8_t>
struct Record{
value_type x, y;
};
template<class T>
std::vector<T> rebind_container(std::vector<Record<T>> const&);
// Span transposes into vector.
template<class T>
std::vector<T> rebind_container(std::span<Record<T>> const&);
template<class T>
std::list<T> rebind_container(std::list<Record<T>> const&);
inline void reserve(...) {}
template<class... Args>
inline void reserve(std::vector<Args...>* v, size_t n) {
v->reserve(n);
}
template<class container_type>
struct Transposed {
using container_type2 = decltype(rebind_container(std::declval<container_type>()));
container_type2 x, y;
Transposed(container_type const& recs) {
auto const n = recs.size();
reserve(&x, n);
reserve(&y, n);
for(auto& rec : recs) {
x.push_back(rec.x);
y.push_back(rec.y);
}
}
};
int main(){
std::vector<Record<uint8_t>> recsV{
{1, 2},
{3, 4}
};
Transposed<decltype(recsV)> trV{recsV};
std::cout << trV.x.size() << std::endl;
std::list<Record<uint8_t>> recsV2{
{1, 2},
{3, 4}
};
Transposed<decltype(recsV2)> trV2{recsV2};
std::cout << trV2.x.size() << std::endl;
}

How to have variadic templates with a type and a size?

Just for fun I am trying to overload a struct, one for a std::array<T,SIZE>, one for a std::vector<T>, and a std::unordered_map<T,U>. So I did the following:
template<typename... T>
struct Cont;
template<typename T, std::size_t SIZE>
struct Cont<T,SIZE>
{
Cont(std::string n) : name(n){}
std::string name;
std::array<T,SIZE> array;
};
template<typename T>
struct Cont<T>
{
Cont(std::string n) : name(n){}
std::string name;
std::vector<T> vector;
};
template<typename T, typename U>
struct Cont<T,U>
{
Cont(std::string n) : name(n){}
std::string name;
std::unordered_map<T,U> unordered_map;
};
However, when I try to compile it, I get the error expected a type, got SIZE. Which I totally understand is because typename... T is expecting a type and not a std::size_t. So I tried:
template<std::size_t SIZE, typename... T>
struct Cont;
template<typename... T>
struct Cont;
Which also doesn't work since I am then redefining and not overloading like I originally thought I was doing. I also realize, that I can do:
template<std::size_t SIZE, typename... T>
struct Cont;
However I am trying to keep this as clean as possible in the sense when I declare them I want to be able to do:
int main()
{
Cont<int,5> myArray("myArray");
myArray.array = {1,2,3,4,5};
Cont<int> myVector("myVector");
myVector.vector = {1,2,3,4};
Cont<int,int> myMap("myMap");
myMap.unordered_map[0] = 2;
}
Is there a way to either overload the template? (I assume this is no) or construct a template in a way that says I'm either typename... T or typename T, std::size_t SIZE?
I'm not sure how to get exactly what you want. Maybe a more clever user has an exact solution. But one alternative might be to just have a type parameter pack for your template and use std::integral_constants for your numeric template arguments. Then you could specialize for each case. For example :
#include <array>
#include <type_traits>
#include <unordered_map>
#include <vector>
template<class...>
struct Cont;
template<class T, class U, U S>
struct Cont<T, std::integral_constant<U, S>>
{
Cont(const char *) {};
std::array<T, S> array;
};
template<class T>
struct Cont<T>
{
Cont(const char *) {};
std::vector<T> vector;
};
template<class T>
struct Cont<T, T>
{
Cont(const char *) {};
std::unordered_map<T, T> unordered_map;
};
int main()
{
Cont<int,std::integral_constant<int, 5>> myArray("myArray");
myArray.array = {1,2,3,4,5};
Cont<int> myVector("myVector");
myVector.vector = {1,2,3,4};
Cont<int,int> myMap("myMap");
myMap.unordered_map[0] = 2;
}
You could make it be Cont<std::array<int, 5>>, and then have a template<typename T, std::size_t SIZE> struct Cont<std::array<T, SIZE>>; specialisation, but that breaks the pattern of the other types.
Another thing you can have is, in addition to making 5 into a type by wrapping it in a std::integral_constant, make a pair of overloaded helper functions to automatically do it for you:
template<typename... T>
Cont<T...> make_cont(std::string name) {
return { std::move(name) };
}
template<typename T, std::size_t SIZE>
Cont<T, std::integral_constant<std::size_t, SIZE>> make_cont(std::string name) {
return { std::move(name) };
}
int main() {
auto myArray = make_cont<int, 5>("myArray");
myArray.array = {1,2,3,4,5};
auto myVector = make_cont<int>("myVector");
myVector.vector = {1,2,3,4};
auto myMap = make_cont<int, int>("myMap");
myMap.unordered_map[0] = 2;
}
If you declare the array version like this
template <typename T, std::size_t SIZE>
struct Cont<std::array<T, SIZE>>
{
Cont(std::string n) : name(n) {}
std::string name;
std::array<T, SIZE> array;
};
Then in main you can use it like this:
int main() {
Cont<std::array<int, 5>> myArray("myArray");
myArray.array = {1, 2, 3, 4, 5};
Cont<int> myVector("myVector");
myVector.vector = {1, 2, 3, 4};
Cont<int, int> myMap("myMap");
myMap.unordered_map[0] = 2;
std::cout << "myArray " << myArray.array[0] << std::endl;
// myArray 1
std::cout << "myVector " << myVector.vector[0] << std::endl;
// myVector 1
std::cout << "myMap " << myMap.unordered_map[0] << std::endl;
// myMap 2
}
I think this is the functionality you are looking for and it's is clean and readable.
This works because std::array<int, 5> names a type that template <typename... T> is expecting and it contains the std::SIZE_T information you need to define a std::array.

C++: Can I have non-static member variable templates?

I'm trying to write a bit of code that requires me to have a lot of std::arrays in a container class. These arrays are all of varying sizes (all consecutive from 2-16, if that matters), and there is exactly one of each size. I want to put them in a container class, and be able to access them with templates.
It's probably easier to explain with code. I want something like this:
class ContainerClass {
public:
// I want to declare some number of arrays right here, all of different
// sizes, ranging from 2-16. I'd like to be able to access them as
// arr<2> through arr<16>.
// This code gives a compiler error, understandably.
// But this is what I'd think it'd look like.
template <size_t N> // I also need to find a way to restrict N to 2 through 16.
std::array<int, N> arr;
// An example method of how I want to be able to use this.
template <size_t N>
void printOutArr() {
for (int i = 0; i < N; i++) {
std::cout << arr<N>[i] << std::endl;
}
}
};
I'd like the code to expand out as if it just had 15 arrays in it, from 2-16. Like this, but with templates:
class ContainerClass {
public:
std::array<int, 2> arr2;
std::array<int, 3> arr3;
std::array<int, 4> arr4;
std::array<int, 5> arr5;
// ... and so on.
};
From what I understand, C++ supports variable templates, but it seems like it's only for static members in classes. Is there an alternative that could behave similarly (preferably with as little overhead as possible)?
If you need more information, please ask.
Thanks in advance.
Can I have non-static member variable templates?
No.
However, you can use templates to generate a list of members like you describe. Here is an example using recursive inheritance:
template<class T, std::size_t base, std::size_t size>
class Stair;
template<class T, std::size_t base>
class Stair<T, base, base> {};
template<class T, std::size_t base, std::size_t size>
class Stair : Stair<T, base, size - 1> {
protected:
std::array<T, size> arr;
public:
template<std::size_t s>
std::array<T, s>& array() {
return Stair<T, base, s>::arr;
}
};
int main()
{
Stair<int, 2, 10> s;
auto& arr = s.array<9>();
I think I may have a solution using recursive templates and std::tuple. I compiled and tested it using gcc 7.3.0.
This makes me feel dirty, but it seems to work.
#include <iostream>
#include <array>
#include <tuple>
#include <type_traits>
// Forward declare A since there is a circular dependency between A and Arrays
template <size_t size, size_t start, typename... T>
struct A;
// If size is greater than start define a type that is an A::ArrayTuple from the
// next step down (size - 1) otherwise type is void
template <size_t size, size_t start, typename E, typename... T>
struct Arrays {
using type = typename std::conditional<(size > start),
typename A<size-1, start, E, T...>::ArrayTuple,
void
>::type;
};
// Use template recursion to begin at size and define std::array<int, size>
// to add to a tuple and continue marching backwards (size--) until size == start
// When size == start take all of the std::arrays and put them into a std::tuple
//
// A<size, start>::ArrayTuple will be a tuple of length (size - start + 1) where
// the first element is std::array<int, start>, the second element is
// std::array<int, start + 1> continuing until std::array<int, size>
template <size_t size, size_t start, typename... T>
struct A {
using Array = typename std::array<int, size>;
using ArrayTuple = typename std::conditional<(size == start),
typename std::tuple<Array, T...>,
typename Arrays<size, start, Array, T...>::type
>::type;
};
// This specialization is necessary to avoid infinite template recursion
template <size_t start, typename... T>
struct A<0, start, T...> {
using Array = void;
using ArrayTuple = void;
};
template <size_t size, size_t start = 1>
class Container {
public:
using ArrayTuple = typename A<size, start>::ArrayTuple;
// Shorthand way to the get type of the Nth element in ArrayTuple
template <size_t N>
using TupleElementType = typename
std::tuple_element<N-start, ArrayTuple>::type;
ArrayTuple arrays_;
// Returns a reference to the tuple element that has the type of std::array<int, N>
template <size_t N>
TupleElementType<N>& get_array() {
// Static assertion that the size of the array at the Nth element is equal to N
static_assert(std::tuple_size< TupleElementType<N> >::value == N);
return std::get<N-start>(arrays_);
}
// Prints all elements of the tuple element that has the type of std::array<int, N>
template <size_t N>
void print_array() {
auto& arr = get_array<N>();
std::cout << "Array Size: " << arr.size() << std::endl;
for (int i = 0; i < arr.size(); i++) {
std::cout << arr[i] << std::endl;
}
}
};
int main() {
// Create a new Container object where the arrays_ member will be
// a tuple with 15 elements:
// std::tuple< std::array<int, 2>, std::array<int, 3> ... std::array<int, 16> >
Container<16,2> ctr;
auto& arr2 = ctr.get_array<2>();
arr2[0] = 20;
arr2[1] = 21;
//ctr.print_array<1>(); // Compiler error since 1 < the ctr start (2)
ctr.print_array<2>(); // prints 20 and 21
ctr.print_array<3>(); // prints 3 zeros
ctr.print_array<16>(); // prints 16 zeros
//ctr.print_array<17>(); // Compiler error since 17 > the ctr size (16)
//int x(ctr.arrays_); // Compiler error - uncomment to see the type of ctr.arrays_
return 0;
}
Here's the output from the compiler if I uncomment that line where I try to declare int x showing the type of ctr.arrays_:
so.cpp: In function ‘int main()’:
so.cpp:90:22: error: cannot convert ‘Container<16, 2>::ArrayTuple {aka std::tuple<std::array<int, 2>, std::array<int, 3>, std::array<int, 4>, std::array<int, 5>, std::array<int, 6>, std::array<int, 7>, std::array<int, 8>, std::array<int, 9>, std::array<int, 10>, std::array<int, 11>, std::array<int, 12>, std::array<int, 13>, std::array<int, 14>, std::array<int, 15>, std::array<int, 16> >}’ to ‘int’ in initialization
int x(ctr.arrays_); // Compiler error - uncomment to see the type of ctr.arrays_

Passing std::array argument with size restricted to expandable set of sizes

How would one best implement a single function that accepts two std::array<int, [size]> arguments, each with a size constrained by a corresponding set of values known at compile-time?
The function must only accept arrays with sizes derived from a given set (enum/macro/etc)
The sets of allowable array "sizes" may be changed in the future and may be large (effectively precluding function overloading)
The function itself should remain fixed regardless of changes to the sets of allowable array "sizes"
The question "Passing a std::array of unknown size to a function", while similar, doesn't appear to directly apply.
The following works in C++14 but seems unnecessarily redundant & messy:
#include <type_traits>
#include <array>
// Add legal/allowable sizes for std::array<> "types" here
// Note: Not married to this; perhaps preprocessor instead?
enum class SizesForArrayX : size_t { Three = 3, Four, Forty = 40 };
enum class SizesForArrayY : size_t { Two = 2, Three, EleventyTwelve = 122 };
// Messy, compile-time, value getter for the above enum classes
template <typename S>
constexpr size_t GetSizeValue(const S size)
{ return static_cast<std::underlying_type_t<S>>(size); }
// An example of the function in question; is Template Argument Deduction
// possible here?
// Note: only arrays of "legal"/"allowable" sizes should be passable
template <SizesForArrayX SX, SizesForArrayY SY>
void PickyArrayHandler(
const std::array<int, GetSizeValue(SX)>& x,
const std::array<int, GetSizeValue(SY)>& y)
{
// Do whatever
for (auto& i : x) i = 42;
for (auto& i : y) while (i --> -41) i = i;
}
Calling the above:
int main()
{
// Declare & (value-)initialize some arrays
std::array<int, GetSizeValue(SizesForArrayX::Forty)> x{};
std::array<int, GetSizeValue(SizesForArrayY::Two>) y{};
//PickyArrayHandler(x, y); // <- Doesn't work; C2672, C2783
// This works & handles arrays of any "allowable" size but the required
// template params are repetitions of the array declarations; ick
PickyArrayHandler<SizesForArrayX::Forty, SizesForArrayY::Two>(x, y);
}
...which is ugly, inelegant, slow-to-compile, and requires the declared array size match the explicit "size" passed to the PickyArrayHandler function template.
For the specific example above: Is there a way for the PickyArrayHandler template to deduce the sizes of the passed arrays?
Generally speaking: Is there a different, better approach?
Since you don't seem to be picky about how the valid sizes are defined, you can use type traits
#include <array>
template <size_t N> struct valid_size1 { enum { value = false }; };
template <size_t N> struct valid_size2 { enum { value = false }; };
template <> struct valid_size1<3> { enum { value = true }; };
template <> struct valid_size1<4> { enum { value = true }; };
template <> struct valid_size1<40> { enum { value = true }; };
template <> struct valid_size2<2> { enum { value = true }; };
template <> struct valid_size2<122> { enum { value = true }; };
template <size_t TX, size_t TY>
void PickyArrayHandler(const std::array<int, TX> &x,
const std::array<int, TY> &y)
{
static_assert(valid_size1<TX>::value, "Size 1 is invalid");
static_assert(valid_size2<TY>::value, "Size 2 is invalid");
// Do whatever
}
int main()
{
// Declare & (value-)initialize some arrays
std::array<int, 40> x{};
std::array<int, 2> y{};
PickyArrayHandler(x, y);
PickyArrayHandler(std::array<int, 4>{}, std::array<int, 2>{});
// PickyArrayHandler(std::array<int, 1>{}, std::array<int, 5>{}); // BOOM!
}
Here's a solution using an array:
#include <iostream>
#include <array>
constexpr size_t valid_1[] = { 3, 4, 40 };
constexpr size_t valid_2[] = { 2, 122 };
template <size_t V, size_t I=0>
struct is_valid1 { static constexpr bool value = V==valid_1[I] || is_valid1<V,I+1>::value; };
template <size_t V, size_t I=0>
struct is_valid2 { static constexpr bool value = V==valid_2[I] || is_valid2<V,I+1>::value; };
template <size_t V>
struct is_valid1<V, sizeof(valid_1)/sizeof(valid_1[0])>
{static constexpr bool value = false; };
template <size_t V>
struct is_valid2<V, sizeof(valid_2)/sizeof(valid_2[0])>
{static constexpr bool value = false; };
template <size_t TX, size_t TY>
void PickyArrayHandler(const std::array<int, TX> &x,
const std::array<int, TY> &y)
{
static_assert(is_valid1<TX>::value, "Size 1 is invalid");
static_assert(is_valid2<TY>::value, "Size 2 is invalid");
// Do whatever
}
twiddled around a bit and got this reduced one working: maybe it helps:
enum SizesForArrayX : size_t { Three = 3, Four, Forty = 40 };
enum SizesForArrayY : size_t { Two = 2, EleventyTwelve = 122 };
template <size_t TX, size_t TY>
void PickyArrayHandler(
const std::array<int, TX>& x,
const std::array<int, TY>& y)
{
// Do whatever
}
int main()
{
// Declare & (value-)initialize some arrays
std::array<int, SizesForArrayX::Forty> x{};
std::array<int, SizesForArrayY::Two> y{};
PickyArrayHandler(x, y);
return 0;
}
Unfortunately, your enums are not continuous so you cannot simply iterate over the enum and you have to handle all cases individually. Since the sizes are known at compile-time you can static_assert for it.
#include <array>
enum SizesForArrayX : size_t { Three = 3, Four, Forty = 40 };
enum SizesForArrayY : size_t { Two = 2, EleventyTwelve = 122 };
template <size_t TX, size_t TY>
void PickyArrayHandler(const std::array<int, TX> &x,
const std::array<int, TY> &y)
{
static_assert(TX == Three || TX == Four || TX == Forty,
"Size mismatch for x");
static_assert(TY == Two || TY == EleventyTwelve, "Size mismatch for y");
// Do whatever
}
int main()
{
// Declare & (value-)initialize some arrays
std::array<int, SizesForArrayX::Forty> x{};
std::array<int, SizesForArrayY::Two> y{};
PickyArrayHandler(x, y);
PickyArrayHandler(std::array<int, 4>{}, std::array<int, 2>{});
//PickyArrayHandler(std::array<int, 1>{}, std::array<int, 5>{}); // BOOM!
}
The best way I see to solve this problem is writing a custom type trait:
template <std::underlying_type_t<SizesForArrayX> SX>
struct is_size_x {
static constexpr bool value = false;
};
template <>
struct is_size_x<static_cast<std::underlying_type_t<SizesForArrayX>>(SizesForArrayX::Forty)>{
static constexpr bool value = true;
};
I'd put these right under the enum class declarations, just so it's easy to check that you got them all. Somebody more clever than I could probably figure out a way to even do this with variadic templates so you only need one specialization.
While tedious, if you have a small set of values this should be fast enough and easy to put in unit tests. The other nice thing about this approach is that if you have multiple functions that need one of these special sizes, you don't have to copy/paste static_asserts around.
With the type traits, your function becomes trivial:
template <std::size_t SX, std::size_t SY>
void PickyArrayHandler(
std::array<int, SX>& x,
std::array<int, SY>& y)
{
static_assert(is_size_x<SX>::value, "Invalid size SX");
static_assert(is_size_y<SY>::value, "Invalid size SY");
// Do whatever
for (auto& i : x) i = 42;
for (auto& i : y) while (i --> -41) i = i;
}
Lastly, you can make a type alias to avoid creating invalid arrays in the first place:
template <typename T, SizesForArrayX SIZE>
using XArray =
std::array<T, static_cast<std::underlying_type_t<SizesForArrayX>>(SIZE)>;
template <typename T, SizesForArrayY SIZE>
using YArray =
std::array<T, static_cast<std::underlying_type_t<SizesForArrayY>>(SIZE)>;
That'll prevent you from declaring an array if it's not an approved size:
XArray<int, SizesForArrayX::Forty> x{};
YArray<int, SizesForArrayY::Two> y{};
Personally I would just manually type the allowable sizes into a static_assert inside PickyArrayHandler. If that's not an option because the sizes will be used in other parts of your program and you're adhering to the DRY principal then I'd use the preprocessor.
#define FOREACH_ALLOWABLE_X(SEP_MACRO) \
SEP_MACRO(3) \
SEP_MACRO(4) \
SEP_MACRO(40) \
#define FOREACH_ALLOWABLE_Y(SEP_MACRO) \
SEP_MACRO(2) \
SEP_MACRO(3) \
SEP_MACRO(122) \
#define COMMA_SEP(NUM) NUM,
#define LOGIC_OR_SEP_X(NUM) N1 == NUM ||
#define LOGIC_OR_SEP_Y(NUM) N2 == NUM ||
#define END_LOGIC_OR false
// some arrays with your sizes incase you want to do runtime checking
namespace allowable_sizes
{
size_t x[] {FOREACH_ALLOWABLE_X(COMMA_SEP)};
size_t y[] {FOREACH_ALLOWABLE_Y(COMMA_SEP)};
}
template <size_t N1, size_t N2>
void PickyArrayHandler(const std::array<int, N1>& x, const std::array<int, N2>& y)
{
static_assert(FOREACH_ALLOWABLE_X(LOGIC_OR_SEP_X) END_LOGIC_OR);
static_assert(FOREACH_ALLOWABLE_Y(LOGIC_OR_SEP_Y) END_LOGIC_OR);
// do whatever
}
#undef FOREACH_ALLOWABLE_X
#undef FOREACH_ALLOWABLE_Y
#undef COMMA_SEP
#undef LOGIC_OR_SEP_X
#undef LOGIC_OR_SEP_Y
#undef END_LOGIC_OR
Some C++ purists will frown at it but it gets the job done.
You could have a is_of_size-like template that check the size of the array, and then use it to disable the template if one of the sizes does not match, something like:
#include <array>
#include <type_traits>
// Forward template declaration without definition.
template <class T, T N, T... Sizes>
struct is_one_of;
// Specialization when there is a single value: Ends of the recursion,
// the size was not found, so we inherit from std::false_type.
template <class T, T N>
struct is_one_of<T, N>: public std::false_type {};
// Generic case definition: We inherit from std::integral_constant<bool, X>, where X
// is true if N == Size or if N is in Sizes... (via recursion).
template <class T, T N, T Size, T... Sizes>
struct is_one_of<T, N, Size, Sizes... >:
public std::integral_constant<
bool, N == Size || is_one_of<T, N, Sizes... >::value> {};
// Alias variable template, for simpler usage.
template <class T, T N, T... Sizes>
constexpr bool is_one_of_v = is_one_of<T, N, Sizes... >::value;
template <std::size_t N1, std::size_t N2,
std::enable_if_t<
(is_one_of_v<std::size_t, N1, 3, 4, 40>
&& is_one_of_v<std::size_t, N2, 2, 3, 122>), int> = 0>
void PickyArrayHandler(
const std::array<int, N1>& x,
const std::array<int, N2>& y)
{
}
Then you can simply:
PickyArrayHandler(std::array<int, 3>{}, std::array<int, 122>{}); // OK
PickyArrayHandler(std::array<int, 2>{}, std::array<int, 3>{}); // NOK
In C++17, you could (I think) replace is_one_of with:
template <auto N, auto... Sizes>
struct is_one_of;
...and automatically deduce std::size_t.
In C++20, you could use a concept to have clearer error messages ;)
Using static_assert for invalid sizes is not a good solution because it doesn't play well with SFINAE; i.e., TMP facilities like std::is_invocable and the detection idiom will return false positives for calls that in fact always yield an error. Far better is to use SFINAE to remove invalid sizes from the overload set, resulting in something resembling the following:
template<std::size_t SX, std::size_t SY,
typename = std::enable_if_t<IsValidArrayXSize<SX>{} && IsValidArrayYSize<SY>{}>>
void PickyArrayHandler(std::array<int, SX> const& x, std::array<int, SY> const& y) {
// Do whatever
}
First we need to declare our valid sizes; I don't see any benefit to stronger typing here, so for a compile-time list of integers, std::integer_sequence works just fine and is very lightweight:
using SizesForArrayX = std::index_sequence<3, 4, 40>;
using SizesForArrayY = std::index_sequence<2, 3, 122>;
Now for the IsValidArraySize traits... The straightforward route is to make use of C++14's relaxed-constexpr rules and perform a simple linear search:
#include <initializer_list>
namespace detail {
template<std::size_t... VSs>
constexpr bool idx_seq_contains(std::index_sequence<VSs...>, std::size_t const s) {
for (auto const vs : {VSs...}) {
if (vs == s) {
return true;
}
}
return false;
}
} // namespace detail
template<std::size_t S>
using IsValidArrayXSize
= std::integral_constant<bool, detail::idx_seq_contains(SizesForArrayX{}, S)>;
template<std::size_t S>
using IsValidArrayYSize
= std::integral_constant<bool, detail::idx_seq_contains(SizesForArrayY{}, S)>;
Online Demo
However if compile times are at all a concern, I suspect the following will be better, if potentially less clear:
namespace detail {
template<bool... Bs>
using bool_sequence = std::integer_sequence<bool, Bs...>;
template<typename, std::size_t>
struct idx_seq_contains;
template<std::size_t... VSs, std::size_t S>
struct idx_seq_contains<std::index_sequence<VSs...>, S>
: std::integral_constant<bool, !std::is_same<bool_sequence<(VSs == S)...>,
bool_sequence<(VSs, false)...>>{}>
{ };
} // namespace detail
template<std::size_t S>
using IsValidArrayXSize = detail::idx_seq_contains<SizesForArrayX, S>;
template<std::size_t S>
using IsValidArrayYSize = detail::idx_seq_contains<SizesForArrayY, S>;
Online Demo
Whichever implementation route is chosen, using SFINAE in this way enables very nice error messages – e.g. for PickyArrayHandler(std::array<int, 5>{}, std::array<int, 3>{});, current Clang 7.0 ToT yields the following, telling you which array's size is invalid:
error: no matching function for call to 'PickyArrayHandler'
PickyArrayHandler(std::array<int, 5>{}, std::array<int, 3>{});
^~~~~~~~~~~~~~~~~
note: candidate template ignored: requirement 'IsValidArrayXSize<5UL>{}' was not satisfied [with SX = 5, SY = 3]
void PickyArrayHandler(std::array<int, SX> const& x, std::array<int, SY> const& y) {
^