Reputation: 3769
I have the following class:
class Foo
{
public:
// Constructors here
private:
std::vector<X> m_data; // X can be any (unsigned) integer type
};
I want the following code to work:
Foo f0;
Foo f1(1); // and/or f1({1})
Foo f2(1, 2); // and/or f2({1, 2})
Foo f3(1, 2, 3); // and/or f3({1, 2, 3})
Foo f4(1, 2, 3, 4); // ... and so on
std::vector<int> vec = {1, 2, 3, 4};
Foo f5(vec);
Foo f6(vec.begin(), vec.end());
std::list<std::size_t> list = {1, 2, 3, 4};
Foo f7(list);
Foo f8(list.begin(), list.end());
std::any_iterable_container container = {1, 2, 3, 4};
Foo f9(container);
Foo f10(container.begin(), container.end());
// PS: I guess I only want containers/iterators that store individual
// values and not pairs (e.g., I don't want care about std::map
// because it does not make sense anyway).
So far I have tried to combine SFINAE, constructor overloading with all possible types, variadic templates, etc. Every time I fix one constructor case, others break down. Also, the code I write becomes very complex and hard to read. However, the problem seems quite simple and I guess I am just approaching it in a wrong way. Any suggestions on how to write the constructors (ideally in C++17), while also keeping the code as simple as possible, is more than welcome.
Thank you.
Upvotes: 2
Views: 101
Reputation: 8501
Assuming the class is defines as following:
template <class T>
class Foo
{
public:
[..]
private:
std::vector<T> m_data;
}
Let's break this task into sub-tasks:
Construct from iterators
template <class Iterator>
Foo (Iterator begin, Iterator end, typename Iterator::iterator_category * = 0)
: m_data(begin, end);
We will fill in our m_data
from begin
and end
.
The third parameter will make sure only Iterator
types that declare iterator_category
will match this prototype. Since this argument has a default value of 0
and is never specified, it serves a purpose only during the template deduction process. When the compiler checks if this is the right prototype, if the type Iterator::iterator_category
doesn't exist, it will skip it. Since iterator_category
is a must-have type for every standard iterator, it will work for them.
This c'tor will allow the following calls:
std::vector<int> vec = {1, 2, 3, 4};
Foo<int> f(vec.begin(), vec.end());
-- AND --
std::list<std::size_t> list = {1, 2, 3, 4};
Foo<int> f(list.begin(), list.end());
Construct from container
template <class Container>
Foo (const Container & container, decltype(std::begin(container))* = 0, decltype(std::end(container))* = 0)
: m_data(std::begin(container), std::end(container));
We will fill our m_data
from the given container. We iterate over it using std::begin
and std::end
, since they are more generic than their .begin()
and .end()
counterparts and support more types, e.g. primitive arrays.
This c'tor will allow the following calls:
std::vector<int> vec = {1, 2, 3, 4};
Foo<int> f(vec);
-- AND --
std::list<std::size_t> list = {1, 2, 3, 4};
Foo<int> f(list);
-- AND --
std::array<int,4> arr = {1, 2, 3, 4};
Foo<int> f(arr);
-- AND --
int arr[] = {1, 2, 3, 4};
Foo<int> f(arr);
Construct from an initializer list
template <class X>
Foo (std::initializer_list<X> && list)
: m_data(std::begin(list), std::end(list));
Note: We take the list as an Rvalue-reference as it's usually the case, but we could also add a Foo (const std::initializer_list<X> & list)
to support construction from Lvalues.
We fill in our m_data
by iterating over the list once again. And this c'tor will support:
Foo<int> f1({1});
Foo<int> f2({1, 2});
Foo<int> f3({1, 2, 3});
Foo<int> f4({1, 2, 3, 4});
Constructor from variable number of arguments
template <class ... X>
Foo (X ... args) {
int dummy[sizeof...(args)] = { (m_data.push_back(args), 0)... };
static_cast<void>(dummy);
}
Here, filling in the data into the container is a bit trickier. We use parameter expansion to unpack and push each of the arguments. This c'tor allows us to call:
Foo<int> f1(1);
Foo<int> f2(1, 2);
Foo<int> f3(1, 2, 3);
Foo<int> f4(1, 2, 3, 4);
Entire class
The final result is quite nice:
template <class T>
class Foo
{
public:
Foo () {
std::cout << "Default" << std::endl;
}
template <class ... X>
Foo (X ... args) {
int dummy[sizeof...(args)] = { (m_data.push_back(args), 0)... };
static_cast<void>(dummy);
std::cout << "VA-args" << std::endl;
}
template <class X>
Foo (std::initializer_list<X> && list)
: m_data(std::begin(list), std::end(list)) {
std::cout << "Initializer" << std::endl;
}
template <class Container>
Foo (const Container & container, decltype(std::begin(container))* = 0, decltype(std::end(container))* = 0)
: m_data(std::begin(container), std::end(container)) {
std::cout << "Container" << std::endl;
}
template <class Iterator>
Foo (Iterator first, Iterator last, typename Iterator::iterator_category * = 0)
: m_data(first, last) {
std::cout << "Iterators" << std::endl;
}
private:
std::vector<T> m_data;
};
Upvotes: 1
Reputation: 890
The simplest way to implement f1
-f4
(which seem to take a variable number of arguments of a known type T
that is not a container or iterator) ist this:
template<typename... Args>
Foo(T arg, Args... args) {...}
As this constructor takes at least 1 argument, there is no ambiguity with the default constructor f0
. As the first argument is of type T
, there is no ambiguity with the following constructors.
If you want to treat std::vector
and std::list
differently than other containers, you can create a partly specialized helper template to check if an argument is an instance of a given template:
template<typename>
struct is_vector : std::false_type {};
template<typename T, typename Allocator>
struct is_vector<std::vector<T, Allocator>> : std::true_type {};
And use it like this to implement f5
and f7
:
template<typename T, Constraint = typename std::enable_if<is_vector<typename std::remove_reference<T>::type>::value, void>::type>
Foo(T arg) {...}
By testing for the respective iterator types of std::vector
and std::list
you can implement f6
and f8
in the same way.
You can check for the presence of member functions begin()
and end()
to implement f9
(I suppose) like this:
template<typename T>
Foo(T arg, decltype(arg.begin())* = 0, decltype(arg.end())* = 0) {...}
However, you'll have to explicitly disable this constructor for std::vector
and std::list
using the helper templates you created to avoid ambiguity.
To check if an argument is some iterator to implement f10
, you can use std::iterator_traits
:
template<typename T, typename Constraint = typename std::iterator_traits<T>::iterator_category>
Foo(T begin, T end) {...}
Again, you'll have to explicitly disable this constructor for the iterator types of std::vector
and std::list
.
Upvotes: 3
Reputation: 21
The idea is to define the class like this:
template <typename X>
class Foo
{
public:
Foo() { };
Foo(initializer_list<int> l) :m_data(l) { };
template<typename container>
Foo(container const & c) :m_data(c.begin(), c.end()) {};
template<typename iterator>
Foo(iterator begin, iterator end) :m_data(begin, end) { };
private:
std::vector<X> m_data;
};
Where:
Foo()
is the default (non-parametric) constructor.
Foo(initializer_list<int> l)
accepts a list like {1, 2, 3}
.
Foo(container const & c)
accepts any container that supports begin
and end
iterators.
Foo(iterator begin, iterator end)
initializes the class with begin
and end
iterators.
Usage:
Foo<int> f0;
Foo<int> f1({1});
Foo<int> f2({1, 2});
Foo<int> f3({1, 2, 3});
Foo<int> f4({1, 2, 3, 4});
std::vector<int> vec = {1, 2, 3, 4};
Foo<int> f5(vec);
Foo<int> f6(vec.begin(), vec.end());
std::list<size_t> list = {1, 2, 3, 4};
Foo<size_t> f7(list);
Foo<size_t> f8(list.begin(), list.end());
set<unsigned> container = {1, 2, 3, 4};
Foo<unsigned> f9(container);
Foo<unsigned> f10(container.begin(), container.end());
Upvotes: 2