Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Priorities of constructors c++

I got very weird 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);

the second constructor for iterators is used instead of first one. How then vectors constructors work, when they looks preety 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 having any troubles.

PS I got 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’)
like image 881
Prazek Avatar asked Jun 06 '13 16:06

Prazek


People also ask

What is the role of a constructor in classes?

The constructor method is a special method of a class for creating and initializing an object instance of that class.

What is constructor in C?

Constructor in C++ is a special method that is invoked automatically at the time of object creation. It is used to initialize the data members of new objects generally. The constructor in C++ has the same name as the class or structure. Constructor is invoked at the time of object creation.

Can default constructor have parameters?

A default constructor is a constructor that either has no parameters, or if it has parameters, all the parameters have default values.

How do you initialize a constructor?

There are two ways to initialize a class object: Using a parenthesized expression list. The compiler calls the constructor of the class using this list as the constructor's argument list. Using a single initialization value and the = operator.


2 Answers

The reason the compiler chooses the second constructor in case of your A is simple: your 10 is a value of signed type int, while size_type is some unsigned integer type. This means that 10 has to be converted to that unsigned integer type. The need for that conversion is what makes 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 int -> unsigned conversion and makes the first constructor win the overload resolution through "non-template is better than template" clause.

Meanwhile, the reason it works differently with std::vector is that the language specification gives special treatment to the constructors of standard sequences. It simply requires that std::vector constructor invocation with two integers of the same type as arguments is somehow "magically" resolved to the first constructor from your quote (i.e. the size-and-initializer constructor). How each specific implementation achieves that is up to the implementation. It can overload the constructor for all integer types. It can use a functionality similar to enable_if. It can even hardcode it into the compiler itself. And so on.

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

23.1.1 Sequences

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

C++11, takes it even further by approaching it from a different angle, although the intent remains the same: it states that if InputIterator does not qualify as input iterator, then the template constructor should be excluded from overload resolution.

So, if you want your class template A to behave the same way std::vector does, you have to deliberately design it that way. You can actually take a peek into the standard library implementation on your platform to see how they do it for std::vector.

Anyway, a low-tech brute-force solution would be to add a dedicated overloaded constructor for int argument

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

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

That might, of course, imply that you'll eventually have to add overloads for all integer types.

A better solution, which I already mentioned above, would be to disable the template constructor for integer arguments by using enable_if or similar SFINAE-based technique. For example

    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?

like image 163
AnT Avatar answered Oct 10 '22 00:10

AnT


When InputIterator isint, the instantiated

A(int first, int last)

is a better match than the instantiated

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

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

#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 to T is too strict, there are looser formulations. For instance, you can guarantee that both arguments are iterators. You could go further (but not quite as far as the example above) and ensure that they "point" to a type that is convertible to T (using std::is_convertible).

like image 36
jerry Avatar answered Oct 10 '22 01:10

jerry