Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

generalized copy constructor in an inner class

Tags:

c++

templates

I'd like to specify a conversion from A<T>::B to A<U>::B.

template<typename T>
struct A
{
    struct B
    {
        B() {}

        template<typename U>
        B(const typename A<U>::B& rhs) {}
    };
};

int main()
{
    A<int>::B x;
    A<double>::B y = x;
}

I thought this would do it, but I get the compiler error:

conversion from ‘A<int>::B’ to non-scalar type ‘A<double>::B’ requested"

Why isn't my code correct, and what's the correct way to achieve the desired conversion?

like image 203
Robert D Avatar asked Sep 11 '11 00:09

Robert D


3 Answers

A template cannot be a copy constructor. §12.8/2, footnote:

Because a template constructor is never a copy constructor, the presence of such a template does not suppress the implicit declaration of a copy constructor. Template constructors participate in overload resolution with other constructors, including copy constructors, and a template constructor may be used to copy an object if it provides a better match than other constructors.

Since your template's argument is const &, its signature will be exactly the same as the implicitly-declared function in the copying case, so it will never be used as a copy constructor.

That may be OK, because in the example you are using it as a conversion constructor. However, template arguments before a :: are a non-deduced context, so the compiler cannot plug in A<int>::B and resolve int. Because of the various ways of specializing templates, there is no way for the compiler to figure out which A, if any, qualifies. You could add typedef A<int>::B B; inside A<float>, and then both int and float would qualify as U.

You can fix this by using SFINAE and adding member types to the classes to help navigate the hierarchy. Here is a demo.

#include <typeinfo>
#include <iostream>

template<typename T>
struct A
{
    typedef T type;

    struct B
    {
        B() {}

        template<typename U>
        B(const U& rhs, typename U::nest_A_parent * = NULL ) {
            std::cout << "copied from type "
            << typeid( typename U::nest_A_parent::type ).name() << '\n';
        }

    private:
        typedef A nest_A_parent;
        template< typename U >
        friend struct B;
    };
};

int main()
{
    A<int>::B x;
    A<double>::B y( x );
}
like image 149
Potatoswatter Avatar answered Oct 24 '22 20:10

Potatoswatter


Slightly amended source, to demonstrate that this is about the limits to the type deduction system:

template<typename T>
struct A
{
    struct B
    {
        B() {}

        template<typename U>
            B(const typename A<U>::B& rhs) {}

        template<typename U>
            B& operator=(const typename A<U>::B& rhs) {}

        template<typename U>
            B& something(const typename A<U>::B& rhs) {}
    };
};

int main()
{
    A<int>::B x;

    A<double>::B y(x);     // fails to deduce
    A<double>::B y = x;    // fails to deduce
    A<double>::B y; y = x; // fails to deduce

    x.something(y);        // fails to deduce
    x.something<double>(y);// NO PROBLEM
}

You see that when we help the compiler a little, there is no more problem. Also note the actual compiler error (gcc) shows it's confusion:

test.cpp|24 col 15| note: candidate is:
test.cpp|15 col 44| note: template<class U> A<T>::B& A<T>::B::something(const typename A<U>::B&) 
 [with U = U, T = int, A<T>::B = A<int>::B, typename A<U>::B = A<T>::B]

Notice the part where U = U (i.e. unresolved)

like image 26
sehe Avatar answered Oct 24 '22 20:10

sehe


How to do a conversion constructor (as Potatoswatter pointed out, it can't be a copy constructor by definition) that only matches the nested type B, for any A<T>::B:

namespace detail {

// private type to identify all A<T>::B
struct B {};

} // detail

// trait to identify all A<T>::B
// a template alias could also be used here
template<typename T>
struct is_b: std::is_base_of<detail::B, T> {};

template<typename T>
struct A
{
    struct B: detail::B {
        B() {}

        template<typename U>
        B(U&& u)
        {
            static_assert( is_b<typename std::decay<U>::type>::value
                           , "Conversion only allowed from A<T>::B" );
        }
    };
};

This technique has the advantage that it's not using SFINAE so an invalid conversion attempt will be reported via static_assert instead of failing silently. On the other hand, you'll need SFINAE if there is at least one other template conversion constructor.

This is assuming that A<T>::B will differ from A<U>::B (for different T and U). If the nested types are identical (as it is the case in your simplified, example code) you'd be better served by defining B somewhere else and using a typedef some_private_namespace::B B; in A.

like image 2
Luc Danton Avatar answered Oct 24 '22 19:10

Luc Danton