Shachar Shemesh
Shachar Shemesh

Reputation: 8573

Conditional definition of constructor in template class

I want the following to compile:

#include <type_traits>

template<typename T>
class C {
public:
    C() {}
    C( const C &that ) {}
};

void foo( const C<const char> &c );

int main() {
    C<const int> i;

    foo( C<char>{} );
}

Of course, it doesn't, as foo expects C<const char>, and I'm passing C<char>. So I want to add an implicit conversion from C<T> to C<const T>. First attempt, we add the following constructor to C:

C( const C< std::remove_const_t<T> > &that ) {}

That doesn't work, because if T is not, itself, const, this definition is identical to the copy constructor, which means we redefine the same constructor twice:

so.cpp: In instantiation of ‘class C<char>’:
so.cpp:17:18:   required from here
so.cpp:9:5: error: ‘C<T>::C(const C<typename std::remove_const<_Tp>::type>&) [with T = char; typename std::remove_const<_Tp>::type = char]’ cannot be overloaded with ‘C<T>::C(const C<T>&) [with T = char]’
     C( const C< std::remove_const_t<T> > &that ) {}
     ^
so.cpp:7:5: note: previous declaration ‘C<T>::C(const C<T>&) [with T = char]’
     C( const C &that ) {}

Second attempt, try to use enable_if:

template< std::enable_if_t< std::is_const_v<T>, int > = 0 >
C( const C< std::remove_const_t<T> > &that ) {}

Which doesn't work. Since the template doesn't depend on any deduction done in the constructor's arguments, it is evaluated too early:

so.cpp: In instantiation of ‘class C<char>’:
so.cpp:18:18:   required from here
so.cpp:10:5: error: no type named ‘type’ in ‘struct std::enable_if<false, int>’
     C( const C< std::remove_const_t<T> > &that ) {}
     ^

Let's artificially make it depend on the arguments, then:

template<
        typename V,
        std::enable_if_t<
                std::is_const_v<T> && std::is_same_v<T, V>,
                int
        > = 0
>
C( const C< std::remove_const_t<V> > &that ) {}

I'm not sure why this fails. I get the following error:

so.cpp: In function ‘int main()’:
so.cpp:24:10: error: invalid initialization of reference of type ‘const C<const char>&’ from expression of type ‘C<char>’
     foo( C<char>{} );
          ^~~~~~~~~
so.cpp:19:6: note: in passing argument 1 of ‘void foo(const C<const char>&)’
 void foo( const C<const char> &c );
      ^~~

I suspect that the deduction happens too deep for the compiler to understand what substitution it needs to make.

How do I resolve this?

P.s. Yes, I know that in this case I can use the implicit copy constructor.

Upvotes: 1

Views: 149

Answers (1)

Jarod42
Jarod42

Reputation: 217245

You can use SFINAE on function but function should be template, and condition should depend of that template, remember that T from class is fixed for the member function:

template<typename T>
class C {
public:
    C() {}
    C( const C &that ) {}

    template <typename U,
              std::enable_if_t<std::is_same_v<U, std::remove_const_t<T>>, int> = 0>
    C( const C<U> &that ) {}
};

Demo

In C++20 would have nice syntax:

template<typename T>
class C {
public:
    C() {}
    C( const C &that ) {}
    C( const C< std::remove_const_t<T> > &that ) requires(std::is_const<T>::value) {}
};

For your code:

template< std::enable_if_t< std::is_const_v<T>, int > = 0 >
C( const C< std::remove_const_t<T> > &that ) {}

is not a Substitution context, parameter are fixed by class, you have hard error instead of your expected SFINAE.

For

template<
        typename V,
        std::enable_if_t<
                std::is_const_v<T> && std::is_same_v<T, V>,
                int
        > = 0
>
C( const C< std::remove_const_t<V> > &that ) {}

You might have SFINAE, but V is not deducible, so you have to provide it, but cannot do it at calling site as it is a constructor.

Simple fix is to give default value:

 template<
        typename V = T,
        std::enable_if_t<
                std::is_const_v<T> && std::is_same_v<T, V>,
                int
        > = 0
>
C( const C< std::remove_const_t<V> > &that ) {}

Demo

(as V is neccessary T, you might drop std::is_same_v<T, V> but change std::is_const_v<T> into std::is_const_v<V> to have SFINAE)

Upvotes: 1

Related Questions