Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++: SFINAE to differentiate fill and range constructors?

Here is a question asking about how to differentiate fill and range constructors. The code is copied here:

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;
    }
};

The code above doesn't work, as described in that question. The solution is using SFINAE to define the range constructor, like this example:

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;
}

This gist of this solution is comparing the dereferenced type of InputIterator with T &. If it is truly an iterator that points to T, the comparison of std::is_same will be true, and the range constructor will be selected; if it is not an iterator, there will be a template substitution failure so the range constructor will be deleted, and hence the fill constructor will be selected.

However, there is a problem for the solution above. If the input InputIterator is of const_iterator type (e.g. cbegin()), then dereferencing it will yield a const T &, and its const-ness cannot be removed by std::remove_const (explained here), so the comparison in std::is_same will be false, resulting in the range constructor being wrongly deleted.

GNU's C++ STL had a bug possibly (I guess) because of this. In this bug, the method only accepts an iterator, but not a const_iterator.

Questions:

(1) is there a nicer workaround than combining two std::is_same conditions with an OR operator, one comparing the dereferenced type with T &, the other with const T &?

(2) if the workaround described in (1) is adopted, is std::remove_const still necessary, now that it cannot remove const-ness from a reference type, and dereferencing an (const or non-const) iterator will always yield a reference, either const T & or T &.

like image 926
Leedehai Avatar asked Jan 03 '23 13:01

Leedehai


2 Answers

The example code is indeed wrong, as it doesn't support const_iterator, std::decay would be more appropriate:

template
<
    typename InputIterator
,   typename = typename ::std::enable_if
    <
        ::std::is_same
        <
            T
        ,   typename ::std::decay
            <
                decltype(*(::std::declval< InputIterator >()))
            >::type
        >::value
    ,  void
    >::type
>
NaiveVector(InputIterator first, InputIterator last);

or even simpler std::iterator_traits<InputIterator>::value_type.

But even better, IMO would be std::is_constructible. That handle when iterator returns wrapper (as potentially std::vector<bool>)

template <typename InputIterator,
          typename ::std::enable_if<
              ::std::is_constructible<
                  T,
                  decltype(*::std::declval<InputIterator>())
              >::value
          >::type* = nullptr
>
NaiveVector(InputIterator first, InputIterator last);

Demo

like image 164
Jarod42 Avatar answered Jan 27 '23 11:01

Jarod42


The other answer is correct. To complete it, I put here the code using iterator_traits. I think it is conceptually much better:

template<
    typename InputIterator, 
    typename = typename std::iterator_traits<InputIterator>::value_type
>
NaiveVector(InputIterator first, InputIterator last);

For reference, this is how it is done in gcc C++11 library (it checks the InputIter concept, which can be more restrictive):

  template<typename _InputIterator,
           typename = std::_RequireInputIter<_InputIterator>>
    vector(_InputIterator __first, _InputIterator __last,
           const allocator_type& __a = allocator_type())
    : _Base(__a)
    { _M_initialize_dispatch(__first, __last, __false_type()); }

In old C++98, surprisingly nothing special is done, so probably there are ambiguous cases for std::vector<std::size_t> (I guess!) the dispatch is done inside the constructor function, in case InputIterator is not really an InputIterator.

  template<typename _InputIterator>
    vector(_InputIterator __first, _InputIterator __last,
           const allocator_type& __a = allocator_type())
    : _Base(__a)
    {
      // Check whether it's an integral type.  If so, it's not an iterator.
      typedef typename std::__is_integer<_InputIterator>::__type _Integral;
      _M_initialize_dispatch(__first, __last, _Integral());
    }
like image 45
alfC Avatar answered Jan 27 '23 09:01

alfC