AstrOne
AstrOne

Reputation: 3769

Wide variety of unambiguous constructors for the same class

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

Answers (3)

Daniel Trugman
Daniel Trugman

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

Knoep
Knoep

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

Pavel Lučivň&#225;k
Pavel Lučivň&#225;k

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

Related Questions