2

I suspect the prototypes of fill constructor and range constructor of std::vector (and many other STL types) given in this webpage are not right, so I implement a NaiveVector to mimic these two prototypes.

My code is:

#include <iostream>
#include <vector>
using namespace std;

template <typename T>
struct NaiveVector {
  vector<T> v;
  NaiveVector(size_t num, const T &val) : v(num, val) { // fill
    cout << "(int num, const T &val)" << endl;
  }

  template <typename InputIterator>
  NaiveVector(InputIterator first, InputIterator last) : v(first, last) { // range
    cout << "(InputIterator first, InputIterator last)" << endl;
  }

  size_t size() const { return v.size(); }
};

int main() {
  NaiveVector<int> myVec1(5,1);                   // A
  cout << "size = " << myVec1.size() << endl;
  for (auto n : myVec1.v) { cout << n << " "; }
  cout << endl;

  cout << "-----" << endl;

  vector<int> vec({1,2,3,4,5});               
  NaiveVector<int> myVec2(vec.begin(), vec.end());// B
  cout << "size = " << myVec2.size() << endl;
  for (auto n : myVec2.v) { cout << n << " "; }
  cout << endl;
}

And the output is:

$ g++ overload.cc -o overload -std=c++11
$ ./overload
(InputIterator first, InputIterator last) // should be: (int num, const T &val)
size = 5
1 1 1 1 1
-----
(InputIterator first, InputIterator last)
size = 5
1 2 3 4 5

As I suspected from the beginning, the compiler cannot differentiate the two constructors properly. Then my question is: how does std::vector's fill constructor and range constructor differentiate from each other?

Rephrase: how to implement the two constructors of this NaiveVector?

This question seems to be a duplicate of this question but the answer is not satisfying. Additionally, C++11 itself doesn't provide a is_iterator<>.. (MSVC has lots of hacks).

Edit: it is allowed to bind an rvalue to a constant lvalue reference, so the first constructor of NaiveVector is valid for A.

genpfault
  • 51,148
  • 11
  • 85
  • 139
Leedehai
  • 3,660
  • 3
  • 21
  • 44
  • "I suspect the prototypes of fill constructor and range constructor of std::vector (and many other STL types) given in this webpage are not right, " - well, it wouldn't surprise me, but why not simply use the correct prototypes from the Standard Library? –  Aug 23 '17 at 19:22
  • `InputIterator` just means a template parameter name. Nothing to do with `std::vector::iterator`. – user0042 Aug 23 '17 at 19:27
  • @user0042 you are right, but I am not implying `InputIterator` should be `std::vector::iterator`. It can indeed be any class that satisfies the requirements of an [input iterator](http://www.cplusplus.com/reference/iterator/InputIterator/). – Leedehai Aug 23 '17 at 19:29
  • 2
    cplusplus.com is garbage. Much better reference at [cppreference.com](http://en.cppreference.com/w/cpp/container/vector/vector). Note in particular: "This overload only participates in overload resolution if `InputIt` satisfies `InputIterator`, to avoid ambiguity with the overload (2)." This is usually achieved via [SFINAE](http://en.cppreference.com/w/cpp/language/sfinae) – Igor Tandetnik Aug 23 '17 at 19:30
  • @IgorTandetnik I know the concept of SFINAE but don't know how it is used here.. so here comes the million-dollar question: how to implement it? – Leedehai Aug 23 '17 at 19:32
  • You have on your machine the source code of at least one standard library implementation. You could do worse than study how the experts did it. – Igor Tandetnik Aug 23 '17 at 19:33
  • @user8385554 But `std::vector` doesn't imply that. So what you wrote doesn't have to do anything with how `std::vector` distinguishes these constructor signatures. – user0042 Aug 23 '17 at 19:35
  • This is a dupe, I answered teh same question about insert last week. Finding it. – Nir Friedman Aug 23 '17 at 19:37
  • 1
    Possible duplicate of [How does a compiler distinguish between two variations of "vector::insert"?](https://stackoverflow.com/questions/45581142/how-does-a-compiler-distinguish-between-two-variations-of-vectorinsert) – Nir Friedman Aug 23 '17 at 19:37
  • @IgorTandetnik um.. I just checked.. experts expertly entangles the checking mechanism with other things (GCC 4.4.7 bits/stl_vector.h uses std::__is_integer<>, which is not directly given in C++11), so not straightforward enough. – Leedehai Aug 23 '17 at 20:00
  • @user0042 yes, my code is an **intentional** example to demonstrate the fallacy of the prototypes given in that webpage. At least those prototypes are only intended for reference, not for real implementation. – Leedehai Aug 23 '17 at 20:06

3 Answers3

6

C++03

[lib.sequence.reqmts]/9

For every sequence defined in this clause and in clause 21:

  • the constructor

    template <class InputIterator>
    X(InputIterator f, InputIterator l, const Allocator& a = Allocator())
    

    shall have the same effect as:

    X(static_cast<typename X::size_type>(f),
      static_cast<typename X::value_type>(l),
      a)
    

    if InputIterator is an integral type.

...

[lib.sequence.reqmts]/11

One way that sequence implementors can satisfy this requirement is to specialize the member template for every integral type. Less cumbersome implementation techniques also exist.

In other words, the standard says that if the range constructor gets selected by overload resolution but the "iterator" type is actually an integral type, it has to delegate to the fill constructor by casting its argument types to force the latter to be an exact match.

C++11/C++14

[sequence.reqmts]/14

For every sequence container defined in this clause and in clause 21:

  • If the constructor

    template <class InputIterator>
    X(InputIterator first, InputIterator last,
      const allocator_type& alloc = allocator_type())
    

    is called with a type InputIterator that does not qualify as an input iterator, then the constructor shall not participate in overload resolution. ...

[sequence.reqmts]/15

The extent to which an implementation determines that a type cannot be an input iterator is unspecified, except that as a minimum integral types shall not qualify as input iterators.

This is more or less the way the standard hints to you to use SFINAE (which works reliably in C++11 as opposed to C++03).

C++17

The wording is similar, except that the paragraph about integral types not being iterators has been moved to [container.requirements.general]/17.

Conclusion

You can write your range constructor to look something like this:

template <typename InputIterator,
          typename = std::enable_if<is_likely_iterator<InputIterator>>::type>
NaiveVector(InputIterator first, InputIterator last)

The is_iterator helper template might simply disqualify integral types and accept all other types, or it might do something more sophisticated such as checking whether there is an std::iterator_traits specialization that indicates that the type is an input iterator (or stricter). See libstdc++ source, libc++ source

Both approaches are consistent with the standard's mandated behaviour of std::vector in C++11 and later. The latter approach is recommended (and will be consistent with what the real std::vector is likely to do on typical implementations), because if you pass arguments that are implicitly convertible to the types expected by the fill constructor, you would hope that the class would "do the right thing" and not select the range constructor only to have it fail to compile. (Although I suspect this is fairly uncommon.) Relative to the C++03 std::vector, it is a "conforming extension" since in that case the constructor call would not compile.

Brian Bi
  • 111,498
  • 10
  • 176
  • 312
  • For comprehensiveness, the answer [here](https://stackoverflow.com/a/45847950/8385554) gives a straightforward code and runnable online implementation. – Leedehai Aug 23 '17 at 20:17
2

If you read the documentation at http://en.cppreference.com/w/cpp/container/vector/vector carefully, you will notice the following (emphasis mine):

4) Constructs the container with the contents of the range [first, last).

This constructor has the same effect as vector(static_cast<size_type>(first), static_cast<value_type>(last), a) if InputIt is an integral type. (until C++11)

This overload only participates in overload resolution if InputIt satisfies InputIterator, to avoid ambiguity with the overload (2).

The confusion is not with the std::vector but your expectation of which of your constructors gets called. Your code doesn't have the checks to make sure that the second constructor gets called only if the above condition of std::vector is met.

In your case, when you use

NaiveVector<int> myVec1(5,1);

the second constructor can be called by using int as the template parameter without requiring any conversion. The compiler needs to convert an int to a size_t in order to be able to call the first constructor. Hence, the second constructor is a better fit.

R Sahu
  • 204,454
  • 14
  • 159
  • 270
1

You can try adding some type trait check to verify that arguments of second constructor are iterators over T elements:

template
<
    typename InputIterator
,   typename = typename ::std::enable_if
    <
        ::std::is_same
        <
            T &
        ,   typename ::std::remove_const
            <
                decltype(*(::std::declval< InputIterator >()))
            >::type
        >::value
    ,  void
    >::type
>
NaiveVector(InputIterator first, InputIterator last) : v(first, last)
{
    cout << "(InputIterator first, InputIterator last)" << endl;
}

Run this code online

user7860670
  • 35,849
  • 4
  • 58
  • 84
  • `std::vector`'s constructor accepts iterators whose `value_type` is different from, but convertible to, `T`. – Igor Tandetnik Aug 23 '17 at 19:32
  • @IgorTandetnik so you mean the code given in this answer should be further generalized? – Leedehai Aug 23 '17 at 20:08
  • 1
    @user8385554 Yes, it is possible to adjust this code to achieve that behavior using `::std::is_convertible`. – user7860670 Aug 23 '17 at 20:10
  • In your solution there is a bug: if `InputIt` is a const_iterator, then dereferencing it with `*` will yield a `const T &`, whose `const` cannot be cast away by `std::remove_const`. So comparing it with `T &` would return `false`, causing the compiler to select the fill constructor. There seems to be no nicer workaround other than combining two `is_same` conditions with OR operator, one comparing with `T &`, the other with `const T &`. – Leedehai Aug 25 '17 at 08:29