0

I'm trying to implement my own vector dml::vector whose API is same as std::vector. What make me confuse is the insert() function's overloading resolution.

I would like to call:

template<class InputIt>
void insert(iterator pos, InputIt first, InputIt last)

But failed with either compile or link error.

Just removed unnecessary code to keep it simple, the key code is:

        template<class InputIt, typename = std::_RequireInputIter<InputIt>> // no matching member function for call to 'insert'
        //template<class InputIt> //undefined reference to `dml::operator+(dml::VectorIterator<dml::vector<int> > const&, unsigned long)
        void insert(iterator pos, InputIt first, InputIt last)
        {
            std::cout << "----- 3" << std::endl;
        }

The complete code is:

#include <new>
#include <string.h>
#include <stdlib.h>
#include <stdexcept>
#include <iostream>

namespace dml
{
    template<typename T>
    void create_memory(T** data, std::size_t num_elem) {
        *data = static_cast<T*>(operator new[](sizeof(T) * num_elem));
    }

    template<typename T>
    void destroy_memory(T* data, std::size_t num_elem) {
        for (std::size_t i=0; i<num_elem; i++) {
            data[i].~T();
        }
        operator delete[](data);
    }

    template<typename vector_type>
    class VectorIterator
    {
    public:
        using ValueType = typename vector_type::value_type;
        using PointerType = ValueType*;
        using ReferenceType = ValueType&;
        using DifferenceType = std::size_t;
    public:
        VectorIterator(PointerType ptr): ptr_(ptr) {}

        VectorIterator& operator ++ () {
            ptr_ ++;
            return *this;
        }

        VectorIterator operator ++ (int) {
            VectorIterator iterator = *this;
            ptr_ ++;
            return iterator;
        }

        VectorIterator& operator -- () {
            ptr_ --;
            return *this;
        }

        VectorIterator operator -- (int) {
            VectorIterator iterator = *this;
            ptr_ --;
            return iterator;
        }

        bool operator == (const VectorIterator& other) {
            return ptr_ == other.ptr_;
        }

        bool operator != (const VectorIterator& other) {
            return ptr_ != other.ptr_;
        }

        ValueType& operator * () {
            return *ptr_;
        }

    private:
        PointerType ptr_;

        template <typename T>
        friend class vector;

        friend VectorIterator<vector_type> operator + (const VectorIterator<vector_type>& lhs, size_t count);
    };

    template <typename vector_type>
    VectorIterator<vector_type> operator + (const VectorIterator<vector_type>& lhs, size_t count)
    {
        return VectorIterator<vector_type>(lhs.ptr_ + count);
    }


    template <typename T>
    class vector {
    public:
        using size_type = std::size_t;
        using value_type = T;
        using reference = value_type&;
        using const_reference = const value_type&;
        using iterator = VectorIterator<vector<T>>;
        using const_iterator = const VectorIterator<vector<T>>;
        using reverse_iterator = VectorIterator<vector<T>>;
        
    public:
        vector(): size_(0), capacity_(0), data_(nullptr) {}

        vector(size_type count, const T& value = T())
            : size_(count), capacity_(count)
        {
            create_memory(&data_, size_);
            for (size_type i=0; i<size_; i++) {
                new (&data_[i]) T (value);
            }
        }

        //! braced-init-list
        vector(std::initializer_list<T> init): size_(init.size()), capacity_(init.size())
        {
            create_memory(&data_, size_);
            typename std::initializer_list<T>::iterator it = init.begin();
            for (size_type i=0; i<size_; i++) {
                new (&data_[i]) T (*it);
                it ++;
            }
        }

        //! cpoy constructor
        vector(const vector& v): size_(v.size_), capacity_(v.capacity_) {
            create_memory(&data_, size_);
            for (size_type i=0; i<size_; i++) {
                new (&data_[i]) T (v.data_[i]);
            }
        }

        ~vector() {
            if (data_!=nullptr) {
                for (int i=0; i<size_; i++) {
                    data_[i].~T();
                }
                operator delete[](data_);
            }
        }

        T& operator [] (size_type pos) {
            return data_[pos];
        }

        const T& operator [] (size_type pos) const {
            return data_[pos];
        }


        ///////////////////// Iterators //////////////////
        iterator begin() noexcept {
            return iterator(data_);
        }

        const_iterator begin() const noexcept {
            return const_iterator(data_);
        }

        iterator end() noexcept {
            return iterator(data_ + size_);
        }

        const_iterator end() const noexcept {
            return const_iterator(data_ + size_);
        }

        size_type size() const {
            return size_;
        }

        size_type capacity() const {
            return capacity_;
        }

        iterator insert(iterator pos, const T& value)
        {
            std::cout << "----- 1" << std::endl;
            return insert(pos, static_cast<size_type>(1), value);
        }

        iterator insert(iterator pos, size_type count, const T& value)
        {
            std::cout << "----- 2" << std::endl;
            iterator it(0);
            return it;
        }

        template<class InputIt, typename = std::_RequireInputIter<InputIt>> // no matching member function for call to 'insert'
        //template<class InputIt> //undefined reference to `dml::operator+(dml::VectorIterator<dml::vector<int> > const&, unsigned long)
        void insert(iterator pos, InputIt first, InputIt last)
        {
            std::cout << "----- 3" << std::endl;
        }

    private:
        size_type size_; // actual size
        size_type capacity_; // actual capacity
        T* data_;
    };

    template<class T>
    bool operator== (const dml::vector<T>& lhs, const dml::vector<T>& rhs)
    {
        if (lhs.size()!=rhs.size()) return false;
        for (size_t i=0; i<lhs.size(); i++) {
            if (lhs[i]!=rhs[i]) return false;
        }
        return true;
    }
}



template<class T>
std::ostream & operator << (std::ostream & os, const dml::vector<T>& v)
{
    for (int i=0; i<v.size(); i++) {
        os << v[i] << ", ";
    }
    os << std::endl;
    return os;
}

#include <vector>

template<class T>
std::ostream & operator << (std::ostream & os, const std::vector<T>& v)
{
    for (int i=0; i<v.size(); i++) {
        os << v[i] << ", ";
    }
    os << std::endl;
    return os;
}

static void insert_test()
{
    std::cout << "---- insert test" << std::endl;
    dml::vector<int> vec(3,100);
    std::cout << vec;

    auto it = vec.begin();
    it = vec.insert(it, 200);
    std::cout << vec;

    vec.insert(it,2,300);
    std::cout << vec;

    // "it" no longer valid, get a new one:
    it = vec.begin();
 
    dml::vector<int> vec2(2,400);
    vec.insert(it+2, vec2.begin(), vec2.end()); // ! this line cause compile/link error
    std::cout << vec;
 
    int arr[] = { 501,502,503 };
    vec.insert(vec.begin(), arr, arr+3);
    std::cout << vec;
}

int main()
{
    insert_test();

    return 0;
}

Note: replace dml:: to std:: in insert_test(), will compile and link ok. What I expect is using dml:: compile & link OK and run same result as when using std::.

Note2: There is a similar question: How does overload resolution work for std::vector<int>::insert , but I don't really understand what people say about enable_if. Just using my code, how can I modify it?


UPDATE

As mentioned in the answers and comments, the std::_RequireInputIter seems not correctly used. I've also tried write my own one:

        template <typename InputIterator>
        using RequireInputIterator = typename std::enable_if<
        std::is_convertible<typename std::iterator_traits<InputIterator>::iterator_category, 
                    std::input_iterator_tag
                    >::value
            >::type;


        //template<class InputIt, typename = std::_RequireInputIter<InputIt>>
        template<class InputIt, typename = RequireInputIterator<InputIt>>
        void insert(iterator pos, InputIt first, InputIt last)
        {
           ...
        }

But this still cause overload resolution failed.


UPDATE 2

In the answer of @JDługosz , a "duplicated destructor called" is mentioned, new [] / delete [] and operator new[] / operator delete[] involved. Let me provide this simple snippet to demonstrate my opinion: operator new[] only allocates memory, won't call constructor, and is different with new[].

Here is the code:

#include <iostream>

int main() {
    {
        std::cout << "--- begin of case 1" << std::endl;
        Entity* p = static_cast<Entity*>(operator new[](sizeof(Entity)*10));
        std::cout << "--- end of case 1" << std::endl;
    }

    std::cout << std::endl;

    {
        std::cout << "--- begin of case 2" << std::endl;
        Entity* q = new Entity[10];
        std::cout << "--- end of case 2" << std::endl;
    }

    return 0;
}

Here is the output (x64 ubuntu 20.04, clang 10.0):

--- begin of case 1
--- end of case 1

--- begin of case 2
--- Entity()
--- Entity()
--- Entity()
--- Entity()
--- Entity()
--- Entity()
--- Entity()
--- Entity()
--- Entity()
--- Entity()
--- end of case 2
JDługosz
  • 5,592
  • 3
  • 24
  • 45
ChrisZZ
  • 1,521
  • 2
  • 17
  • 24
  • From your "Note2", it seems that you are aware of SFINAE/`enable_if`, but you maybe have a question about how to use it? – Drew Dormann Jun 22 '21 at 14:46
  • `std::_RequireInputIter` has not to be used... internal type. – Jarod42 Jun 22 '21 at 14:46
  • 1
    Why wouldn't you place body of `operator+` right where it's declared as friend? It also doesn't seem to be friend at all, because `operator+` for iterator seems more logical with right-addition only (no symmetry needed) – Alexey S. Larionov Jun 22 '21 at 14:46
  • Your new version of `RequireInputIterator` detects it by using `std::input_iterator_tag`. Again, your class `VectorIterator` is _not_ convertable to `std::input_iterator_tag`, so the requirement is not met. The tag is specifically how it recognises a class as being an iterator. I suggest using Boost.Iterator library when writing an iterator. – JDługosz Jun 23 '21 at 14:07
  • @JDługosz Re: "Your new version of RequireInputIterator detects it by using" I just would like to implement `dml::vector`, and when not implementing `insert()` function families, all works fine. Boost.iterator may be a good choice, but I initially would be without dependency libraries. – ChrisZZ Jun 23 '21 at 15:42
  • See https://en.cppreference.com/w/cpp/iterator/iterator_tags to learn about the iterator tag. Also, learn what the stuff like `std::is_convertible::iterator_category, std::input_iterator_tag>` _means_ so you can just see why the requirement is not being met. That's rather hard to read, BTW, being pre-C++14; furthermore the whole awkward use of a dummy template argument is no longer necessary in C++17, which can express it directly. – JDługosz Jun 23 '21 at 17:37

1 Answers1

0

I think the problem is that your VectorIterator type is not known to be an iterator by the std::_RequireInputIter check. So, with that check in place, it won't use that template.

This appears to be an internal helper you must have copied from the std:: headers for a library container. It's not a standard thing.

the overloading issue

Consider the basic C++11 signature of the template member function:

template <typename T> void insert (iterator pos, T first, T last)

This is implemented to handle any pair of iterators, but nothing tells the compiler that only certain types are meant to be used as T here. Now template argument deduction makes it greedy, so any call to x.insert(pos, a,b); where a and b are the same type will cause this form to be an exact match, so it will be taken, even if you meant a to be a count and b to be a value. This is a problem when you have a vector of some simple numeric type (or something that has a constructor that takes a single numeric argument, etc.)

So, look at what cppreference states:

This overload participates in overload resolution only if InputIt qualifies as LegacyInputIterator, to avoid ambiguity with the overload (3).

Naming the template argument InputIt instead of T does not mean anything; it's just a name.

The crazy stuff in your original, adding an extra unnamed template argument, is a Pre-concept way to impose this constraint. You can read up on the obsolete enable_if the creative use of SFINAE for advanced template metaprogramming (which I've called A Slow Descent Into Madness) if you like.

What your update shows is that you allow the type for the InputIt argument if (and only if) std::iterator_traits<InputIt>::iterator_category produces input_iterator_tag or better. That is, a bidirectional iterator "isa" input iterator, etc. (BTW, the formulation would be somewhat cleaner in C++14, which makes me think that this incantation was written for C++11 before C++14.)

If you tested your implementation with iterators from a std::vector or std::array, it would work just fine (assuming that it's all correct). The problem isn't with your declaration of insert. The issue is that your VectorIterator doesn't have any iterator_traits defined for it, so the template code deems it to not be an iterator at all.


meanwhile...

for (size_t i=0; i<lhs.size(); i++) {
if (lhs[i]!=rhs[i]) return false;

Just use std::equal and don't re-implement standard algorithms with your own loops.

Also, I suggest t esting to container with a "noisy" class that logs all the constructor and destructor calls along with the address.

JDługosz
  • 5,592
  • 3
  • 24
  • 45
  • "This appears to be an internal helper you must have copied ...", don't really understand... Do you mean different std lib / compiler are with specific implementation, and names can't be changed? like `std::initializer_list` instead of `std::InitializerList`? – ChrisZZ Jun 22 '21 at 15:25
  • "Just use `std::equal`", I don't know this function until you mention. Thanks. – ChrisZZ Jun 22 '21 at 15:27
  • " But you already called their destructors on the preceding loop!" I've read the cppreference again, and I'm sure `operator new []` won't call constructor, and `operator delete[]` won't call dtor. What you sugguest deleting `data_[i].~T()` is wrong. With a self defined Array class whose member including `double* data` allocated during ctor, if do as you said, will cause memory leak. – ChrisZZ Jun 22 '21 at 15:56
  • `C* p= new C[20];` will call `C`s default constructor 20 times. Then `delete[] p;` will call the destructor 20 times, in the reverse order. I've been doing this since 1987, and this is pretty elementary; I'm certainly not wrong about it. Try it with a simple test program. In [cppreference](https://en.cppreference.com/w/cpp/language/new), _Construction: ... If type is an array type, an array of objects is initialized. If initializer is absent, each element is default-initialized._ – JDługosz Jun 23 '21 at 13:54
  • internal helper: names beginning with an underscore and capital letter, like `std::_RequireInputIter`, are internal implementation details. Names defined by the standard, including `std::initializer_list`, will be the same on any standard-conforming implementation. – JDługosz Jun 23 '21 at 13:59
  • Re: "I don't know this function" An essential piece of advice for programmers of _all skill levels_ is Know Your Libraries. I suggest reading through https://en.cppreference.com/w/cpp/algorithm every six months just so you don't forget about those that you don't use much. See also my article: https://www.codeproject.com/Tips/5249485/The-Most-Essential-Cplusplus-Advice – JDługosz Jun 23 '21 at 14:03
  • Re: " An essential piece of advice for programmers of all skill levels is Know Your Libraries." Thanks for this suggest, I agree with you and the mentioned article, and will try practise that. – ChrisZZ Jun 23 '21 at 15:36
  • Re: "C* p= new C[20]; will call Cs default constructor 20 times." I provide a snippet in the "UPDATE 2" to demonstrate what I think and what the result is on my machine. – ChrisZZ Jun 23 '21 at 15:37
  • Re: "internal helper: names beginning with an underscore and capital letter, " Got it now, didn't know this meaning yesterday. – ChrisZZ Jun 23 '21 at 15:38
  • Oh, yes: calling `operator new (size)` as a function only does the allocation. That is not the same thing as a _`new` expression_. Likewise for `delete[] p;` vs the function call `operator delete (p);`. I misread the function as I went over it in a hurry, between meetings :) Also, in my defense, the use of explicitly calling `operator delete` like this is a few decades out of date (now you should use Allocator objects) and I've not used that since the 90's. – JDługosz Jun 23 '21 at 16:02
  • I've expanded my answer to better focus on the reason for that extra stuff in the declaration of `insert`. – JDługosz Jun 23 '21 at 18:50