Reputation: 3224
I had a question to do with writing a range based constructor for a class but couldn't find the right phrasing to search for help on google.
Suppose I am writing a simple class, like vector, which involves having a range based constructor that inserts elements from the range into the current container:
// foo.h
#ifndef FOO_H
#define FOO_H
#include <iostream>
class Foo {
public:
Foo() {
std::cout << "Default constructor called" << std::endl;
}
template<class InputIterator> Foo(InputIterator first, InputIterator last) {
std::cout << "Range based constructor called" << std::endl;
}
Foo(size_t n, int val) {
std::cout << "size_t, int constructor called" << std::endl;
}
};
#endif // FOO_H
and have a cpp file
#include <iostream>
#include <vector>
#include "foo.h"
using std::cout; using std::endl;
int main() {
std::vector<int> v(10, 5);
Foo f_default;
Foo f_range(v.begin(), v.end());
Foo f_int(314, 159); // want this to call size_t, int ctctr
return 0;
}
In the third line in main, we create a Foo f_int(314, 159)
which intuitively we want to call the size_t, int constructor. However it is matching the generic template constructor for ranges instead. Is there a way issues like this are addressed in C++? I feel like I am incorrectly dealing with writing range based constructors.
I can imagine you can use template specialization maybe, but don't really see how.
An example where such a situation might happen is if we are writing a vector class where there is a constructor based on a size_t and default value (which would be templatised on the class but I simplified here) and another constructor based on iterator ranges.
Upvotes: 1
Views: 2444
Reputation: 109119
The constructor template is a better match in the third case because you're passing two int
arguments, while the Foo(size_t n, int val)
requires an int
to size_t
conversion for the first argument. If you modify your code to
Foo f_int(static_cast<size_t>(314), 159);
the constructor you want is called. But of course, this is not a good solution because it's easy to accidentally call the wrong constructor. Instead, you can use SFINAE to drop the constructor template from the overload resolution set by ensuring the argument types are iterators.
template<class InputIterator,
class = std::enable_if_t<
std::is_base_of<
std::input_iterator_tag,
typename std::iterator_traits<InputIterator>::iterator_category
>{}
>
>
Foo(InputIterator first, InputIterator last) {
std::cout << "Range based constructor called" << std::endl;
}
If you look at the table here, all iterators you can read from are InputIterators or derived types. So the code above is checking that the InputIterator
type being passed to the constructor is of that type or something derived from that type.
Upvotes: 4