4

Consider the following code:

#include<iostream>
#include<utility>


struct Base
{
    int baseint;
};

struct Der1 : Base
{
    int der1int;
    Der1() : der1int(1) {}
    explicit Der1(const Base& a) : Base(a), der1int(1)
    {
        std::cerr << "cc1" << std::endl;
    }
};

struct Der2 : Base
{
    int der2int;
    Der2() : der2int(2) {}
    explicit Der2(const Base& a) : Base(a), der2int(2)
    {
        std::cerr << "cc2" << std::endl;
    }
};


template <typename T, typename U>
struct MyPair
{
    T first;
    U second;
};

int main()
{
    Der1 d1;
    Der2 d2;

    std::pair<Der1, int> p1;
    std::pair<Der2, int> p2;

    p1 = p2; // This compiles successfully

    MyPair<Der1, int> mp1;
    MyPair<Der2, int> mp2;

    mp1 = mp2; // This will raise compiler error, as expected.
}

Tested under GCC 4.5.2

The reason lies in std::pair sources:

  /** There is also a templated copy ctor for the @c pair class itself.  */
  template<class _U1, class _U2>
    pair(const pair<_U1, _U2>& __p)
    : first(__p.first),
      second(__p.second) { }

Is that behaviour compliant with the C++ standard? For a first sight it looks inconsistent and counterintuitive. Do the other implementations of STL work the same way?

Rafał Rawicki
  • 22,324
  • 5
  • 59
  • 79

4 Answers4

5

I am not sure that I understand the question, but basically you are asking why two unrelated std::pair can be implicitly convertible even if the instantiating types are not implicitly convertible. That is, why the implicitly convertible property of the instantiating types does not propagate to the pair.

The standard does not provide explicit assignment operators for the std::pair template, which means that it will use the implicitly generated assignment operator. To be able to assign pairs of convertible types, it relies on a templated constructor that allows an implicit conversion from std::pair<A,B> to std::pair<C,D>, the behavior of which is defined in §20.2.2 [lib.pairs]/4

template<class U, class V> pair(const pair<U, V> &p);

Effects: Initializes members from the corresponding members of the argument, performing implicit con- versions as needed.

The standard seems to only require the implementation to use implicit conversions, and in this particular implementation the conversion is actually explicit, which seems to contradict the wording of the standard.

David Rodríguez - dribeas
  • 204,818
  • 23
  • 294
  • 489
  • 1
    How would you actually implement that, though? Putting `__p.first` in the initializer list, calls explicit constructors. But if you don't do that, `this->first` needs to be default-constructible, which also doesn't conform. Does `std::pair` need magic compiler internals to avoid explicit conversion, or am I missing the trick? – Steve Jessop Jul 20 '11 at 11:21
  • 1
    @Steve Jessop: I was thinking about that and it does not seem easy to do. I guess that the solution would template metaprogramming to detect whether the type is implicitly convertible and an static assert if it isn't. That should be easy enough to implement, but will add some complexity to the implementation. Now that I think of it, this will probably break explicit conversions from `std::pair` to `std::pair` so this might not be a solution either – David Rodríguez - dribeas Jul 20 '11 at 11:35
  • That's a very good question. Unfortunately, I do not know any STL implementation which behaves correctly. – Rafał Rawicki Jul 20 '11 at 18:02
  • 1
    @rawicki: I don't think that it can actually be done. I have [implemented](http://definedbehavior.blogspot.com/2011/07/i-decided-to-start-writing-few-months.html) the SFINAE approach (and took the opportunity to start yet another blog --shame on me for self promotion here) and because it is a constructor (no return type) the failure has to be triggered with an extra constructor parameter, and that breaks standard compliance. – David Rodríguez - dribeas Jul 20 '11 at 23:00
2

As part of class std::pair the constructor

template<class T1, T2>
class pair
{
public:

    template<class _U1, class _U2>
    pair(const pair<_U1, _U2>& __p)
         : first(__p.first),
           second(__p.second)
    { } 

};

is not a copy constructor, but a converting constructor from any pair<_U1, _U2> to pair<T1, T2>. This works for cases where the first and second members are convertible to the corresponding member of the other pair.

Converting each member separately is according to the standard.

Bo Persson
  • 90,663
  • 31
  • 146
  • 203
  • *Converting each member separately is according to the standard*: yes, but the standard adds *implicit* to that sentence, and in this case the implementation is doing *explicit* conversions, which is the reason for the question itself. – David Rodríguez - dribeas Jul 20 '11 at 11:14
  • @David How would you write this constructor? – Sjoerd Jul 20 '11 at 11:21
  • @David - Ok, I might have focused on the part of this not being an assignment or a copy constructor and missed the `explicit` part of the question. C++0x says that this constructor should not be considered if the members are are not implicitly convertible, but the C++98 standard does not say so, and provides no tools to decide it anyway. – Bo Persson Jul 20 '11 at 11:47
1

This should really be a comment, but I prefer some room to type this out.

So, lets say we have two types:

typedef std::pair<A,B> pairAB;
typedef std::pair<S,T> pairST;

Now I want to assign one to the other:

pairAB x;
pairST w;

x = w; // how?

Since std::pair doesn't have an explicit assigment operator, we can only use the default assignment pairAB & operator=(const pairAB &). Thus we invoke the implicit conversion constructor, which is equivalent to:

x = pairAB(w);  // this happens when we say "x = w;"

However, has has been pointed out, this conversion constructor calls the explicit member constructors:

pairAB(const pairST & other) : first(other.first), second(other.second) { }

Thus for each member individually we do use explicit conversion.

Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084
0

Quick answer: Because the standard says it should.

Your next question, of course, will be: Why does the standard say so?

Imagine this line, which I think you agree should work:

std::pair<long, long> x = std::make_pair(3, 5);

But as 3 and 5 are ints, we are trying to assign a std::pair<int, int> to a std::pair<long, long>. Without a templated constructor and a templated assignment operator, it would fail, as your MyPair proved.

So to answer your question: the templated constructor is for convenience. Everyone expects to be able to assign an int to a long. So it is reasonable to be able to assign e.g. a pair of ints to a pair of longs.

Sjoerd
  • 6,837
  • 31
  • 44