Pietro Saccardi Pietro Saccardi - 3 months ago 18
C++ Question

Convert class<T> to class<U> if T is convertible to U

I would like to create a templated class

test<T>
which I can convert into
test<U>
(possibly implicitly) if
T
is convertible to
U
. The simplest idea that came to my mind is to add a templated constructor that takes a
test<U>
, where the template argument
U
is controlled by
enable_if
.

#include <iostream>
#include <type_traits>

template <typename T>
class test {
int _foo;
public:
int foo() const { return _foo; }

test() : _foo(5) {}

// This works:

// template <typename U>
// test(test<U> other)
// : _foo(other.foo()) {}

// This doesn't, can't resolve `U`:

template <typename U>
test(test<typename std::enable_if<std::is_convertible<U, T>::value, U>::type> other)
: _foo(other.foo()) {}

};

int main() {
test<int> a;
test<long> b = a; // T = long, U = int

// true
std::cout << std::boolalpha << std::is_convertible<int, long>::value << std::endl;

return 0;
}


If I just declare the first templated constructor, the code works fine. With the second constructor, it doesn't compile:

21:9: note: candidate template ignored: couldn't infer template argument 'U'
test(test<typename std::enable_if<std::is_convertible<U, T>::value, U>::type> other)
^


Why can't the compiler infer
U
in this case?
It looks quite straightforward, I must be missing something in the template deduction. Also, if the conversion cannot be achieved in this way, what would be the best way of making
test<T>
convertible into
test<U>
?





As a side note, I managed to get it to work by making all
test<T>
friends, and declaring a conversion operator which uses
enable_if
in the implementation (as follows), but I still would like to know why the first, simpler approach is not working.

template <typename T>
class test {
int _foo;

template <typename U>
friend class test;

test(int foo) : _foo(foo) {}

public:
int foo() const { return _foo; }

test() : _foo(5) {}

template <typename U>
operator test<U>() const {
using return_t = typename std::enable_if<std::is_convertible<U, T>::value, U>::type;
return test<return_t>(_foo);
}
};

hvd hvd
Answer

Why can't the compiler infer U in this case?

Type deduction fundamentally cannot infer T from parameter types such as a<T>::b. Even if you have

template <typename T> struct identity { typedef T type; };

then the compiler still cannot rule out you providing a specialisation somewhere that sneakily makes identity<X>::type Y for some type X.

It's the same with std::enable_if: standard library class templates don't get special treatment in the language rules, so the compiler cannot figure out that if std::enable_if<cond, U>::type is supposed to be X, then U has to be X.

This is why for regular functions, std::enable_if typically appears in the return type. It can't be in the parameters, for the same reason as for constructors. It can't be in a template parameter, as the caller could specify a different type and bypass your restriction. But the return type is safe.

Constructors don't have a return type. Luckily though, callers cannot explicitly specify constructor template arguments, so putting std::enable_if in a template default argument as already answered by m.s. is safe there.

Comments