2

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);
    }
};
Pietro Saccardi
  • 2,602
  • 34
  • 41
  • 2
    Somewhat relevant: [Why should I avoid std::enable_if in function signatures](http://stackoverflow.com/questions/14600201/why-should-i-avoid-stdenable-if-in-function-signatures), _"Put the hack in the template parameters"_. – dfrib Aug 06 '16 at 17:12

2 Answers2

3

U appears in a non-deduced context.

This solves the compiler error:

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

live example

Community
  • 1
  • 1
m.s.
  • 16,063
  • 7
  • 53
  • 88
2

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.