Reputation: 137
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?
Upvotes: 5
Views: 892
Reputation: 35449
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
.
Upvotes: 2
Reputation: 137860
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 );
}
Upvotes: 4
Reputation: 393427
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)
Upvotes: 3