C ++ Constructors Priorities

I have a very strange problem

for code like this

template <typename T> struct A{ explicit A(unsigned int size = 0, const T &t = T()) { } template <typename InputIterator> A(InputIterator first, InputIterator last) { for(;first != last ; ++first) { *first; //do something with iterator } } }; 

when I, for example, define

  A<int> a(10,10); 

instead of the first, the second constructor for iterators is used. How do vector constructors work when they look the same?

  explicit vector (size_type n, const value_type& val = value_type(), const allocator_type& alloc = allocator_type()); template <class InputIterator> vector (InputIterator first, InputIterator last, const allocator_type& alloc = allocator_type()); 

And I can make vector v (10,10) without any problems.

PS I got an error like this

  temp.cpp: In instantiation of 'A<T>::A(InputIterator, InputIterator) [with = int; T = int]': temp.cpp:17:15: required from here temp.cpp:12:4: error: invalid type argument of unary '*' (have 'int') 
+4
source share
5 answers

The reason the compiler chooses the second constructor in the case of your A is simple: your 10 is the type value of the signed type int , and size_type is some unsigned integer type. This means that 10 needs to be converted to this unsigned integer type. The need for this conversion is what causes the first constructor to lose the overload resolution to the second constructor (which is an exact match for InputIterator = int ). You can work around this problem by doing

 A<int> a(10u, 10); 

This eliminates the need for an int -> unsigned conversion and makes the first constructor a gain in overload resolution with the sentence "no template is better than template".

Meanwhile, the reason it works differently with std::vector is because the language specification gives a special relation to standard sequence constructors. It just requires that the call to the std::vector constructor with two integers of the same type as the arguments is somehow "magically" resolved by the first constructor from your quote (i.e., the size-initializer constructor). How each specific implementation is achieved, which depends on the implementation. It can overload the constructor for all integer types. It can use functionality similar to enable_if . It can even hard write it to the compiler itself. And so on.

Here is how it is stated in C ++ 03, for example

23.1.1 Sequences

9 For each sequence defined in this section and in paragraph 21:

- constructor

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

should 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

C ++ 11 does it even further, approaching it from a different angle, although the intention remains the same: it states that if the InputIterator cannot be qualified as an input iterator, then the template constructor should be excluded from permission overloading.

So, if you want your template A template to behave just like std::vector does, you must intentionally design it that way. You can really look into the standard library implementation on your platform to see how they do it for std::vector .

In any case, a low-tech brute force solution would have to add a dedicated overloaded constructor for the int argument

  explicit A(unsigned int size = 0, const T &t = T()) { ... } explicit A(int size = 0, const T &t = T()) { ... } 

This may, of course, mean that you end up having to add overloads for all integer types.

The best solution that I mentioned above would be to disable the template constructor for integer arguments using enable_if or a similar technique based on SFINAE. for instance

  template <typename InputIterator> A(InputIterator first, InputIterator last, typename std::enable_if<!std::is_integral<InputIterator>::value>::type* = 0) 

Do you have C ++ 11 features available to you in your compiler?

+4
source

When InputIterator is int , instantiated

 A(int first, int last) 

is a better match than instance

 explicit A(unsigned int size = 0, const int &t = int()) 

due to the first argument unsigned . A<int> a((unsigned int)10,10) should call the constructor you expect. You can also use SFINAE to prevent a match if two iterators on T are really passed to the constructor:

 #include <iostream> using namespace std; template <typename T> struct A{ explicit A(unsigned int size = 0, const T &t = T()) { cout << "size constructor for struct A" << endl; } template <class I> using is_T_iterator = typename enable_if<is_same<typename iterator_traits<I>::value_type, T>::value, T>::type; template <typename InputIterator> A(InputIterator first, InputIterator last, is_T_iterator<InputIterator> = 0) { cout << "iterator constructor for struct A" << endl; for(;first != last ; ++first) { *first; //do something with iterator } } }; int main() { A<int>(10,10); A<int>((int*)0,(int*)0); //A<int>((char*)0,(char*)0); //<-- would cause compile time error since (char *) doesn't dereference to int return 0; } 

If the condition that both arguments are iterators for T is too strict, there are easier formulations. For example, you can guarantee that both arguments are iterators. You can go further (but not as far as in the example above) and make sure that they "indicate" a type that can be converted to T (using std::is_convertible ).

+2
source

That's right, the template thing is better suited, so it is selected. The standard library implementation makes it very difficult to manage reasonable behavior with all templates. You might want to find some implementation code for specializations if you want to implement a similar collection of your own.

Or you may find a way to avoid the problem.

There was a good GOTW article with all the occasions for choosing an overload function and some tips for dealing with it.

0
source

Your first argument in A<int>(10, 10) does not match your explicit constructor, because 10 signed, so a template constructor is used instead. change it to A<int>(10u, 10) and you can get the expected results.

0
source

If you are writing a shared library, you can make extra efforts and use meta-programming templates to catch all cases. Or simply specify explicit overloads for all integral types. For less general use, it is usually sufficient to follow the rule that at any time when you provide overloading for any integral type, you also provide one for int (so you would have the constructor A::A( int size, T const& initialValue = T() ) , in addition to the ones you have already provided).

More generally: you probably should just make a size int and execute with it. The standard library has been caught up on many historical issues, and by default use size_t but in general, if there is no very good reason, otherwise the normal integral type in C ++ is int ; In addition, unsigned types in C ++ have very strange semantics and should be avoided at any time when arithmetic is likely to occur.

0
source

Source: https://habr.com/ru/post/1484875/


All Articles