gowrath
gowrath

Reputation: 3224

Correctly Writing a Range Based Constructor

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

Answers (1)

Praetorian
Praetorian

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

Related Questions