forward_list: assign(_InputIterator __first, _Inpu

2019-02-28 07:11发布

问题:

I have implemented a subset of the forward_list and wanted to test the method assign(size_type __n, const _Tp& __val) but I get a compiler error because the compiler wants to call the method assign(_InputIterator __first, _InputIterator __last) instead.

I have written the following snippet, just to illustrate the problem:

test.h

#ifndef TEST_H
#define TEST_H

#include <utility> // Just to get the std::size_t

template<typename _Tp>
class forward_list {
  public:
    typedef std::size_t size_type;

    void assign(size_type n, const _Tp& val)
    {
      printf("%s\n", __PRETTY_FUNCTION__);
    }

    template<typename _InputIterator>
    void assign(_InputIterator first, _InputIterator last)
    {
      printf("%s\n", __PRETTY_FUNCTION__);
    }
};

#endif // TEST_H

test.cpp

#include <stdlib.h>
#include <stdio.h>
#include "test.h"

int main()
{
  forward_list<int> l;
  l.assign(10, 5);
  return 0;
}

The output of the execution is:

void forward_list<_Tp>::assign(_InputIterator, _InputIterator) [with _InputIterator = int; _Tp = int]

I would like to have the method assign(size_type __n, const _Tp& __val) called.

Compiler version (just in case it matters): g++ (Debian 4.7.2-5) 4.7.2

I have used similar signatures to the signatures used in the std::forward_list and, with the following code snippet (using the STL):

std::forward_list<int> l;
l.assign(10, 5);

The compiler knows that it has to call assign(size_type __n, const _Tp& __val) and doesn't get confused. What am I missing?

回答1:

When you call l.assign(10, 5);, there are two viable overloads:

void assign(size_type n, const int& val)

template <>
void assign(int first, int last)

When we say that non-template functions are preferred to template functions, that is only true if the two have indistinguishable conversion sequences. But in this case, the function template will match exactly (both of your arguments are int, no conversion necessary), while the non-template will have to undergo promotation (have to promote 10 from int to size_t). So that's why the function template overload is preferred.

As to how to fix it, you just need to make the template not a viable overload. That involves writing a type_trait for input iterator, which using void_t is not hard:

template <typename... >
using void_t = void;

template <typename T, typename = void>
struct is_input_iterator : std::false_type { };

template <typename T>
struct is_input_iterator<T, void_t<
    decltype(std::declval<T>() == std::declval<T>()),
    decltype(std::declval<T>() != std::declval<T>()),
    decltype(*std::declval<T>()),
    decltype(++std::declval<T>()),
    decltype(std::declval<T>()++)
    >> : std::true_type { };

And then require is_input_iterator:

template <typename _InputIterator,
          typename = std::enable_if_t<is_input_iterator<_InputIterator>::value>>
void assign(_InputIterator first, _InputIterator last);

There are lots of other ways to do this sort of thing, I just happen to like void_t. Regardless of which way you do it, you have to ensure that the template simply isn't viable.



回答2:

The assign overload that you want to be called takes an unsigned integral type as the first argument, but you're passing it two signed integers. If you change the call to

l.assign(10U, 5);  // make the first argument unsigned

the first assign overload is called. But clearly, this is not the right solution to your problem in general.

You need to constrain the assign template so that it is only viable when the type of the arguments satisfy requirements for iterators. One way to do this is to inspect iterator_traits for the type involved. If the iterator_category satisfies the requirements of an InputIterator, then the function template can be used.

template<typename InputIterator>
typename std::enable_if<
    std::is_base_of<std::input_iterator_tag,
                    typename std::iterator_traits<InputIterator>::iterator_category
                   >::value
         >::type
    assign(InputIterator first, InputIterator last)
{
  printf("%s\n", __PRETTY_FUNCTION__);
}

Live demo

Note that technically the above solution isn't guaranteed to work before C++17 (or whatever it'll be called) because iterator_traits isn't required to be SFINAE friendly until then, and it could result in a hard error instead of substitution failure. But chances are your implementation's iterator_traits is already SFINAE friendly, and you won't run into any issues.


size_t isn't guaranteed to be included by <utility>, use one of the headers listed on the linked page.

Don't use identifiers that begin with an underscore and are followed by an uppercase characters, those are reserved for the implementation.