How to use sfinae for selecting constructors?

2019-01-11 18:40发布

问题:

In template meta programming, one can use SFINAE on the return type to choose a certain template member function, i.e.

template<int N> struct A {
  int sum() const noexcept
  { return _sum<N-1>(); }
private:
  int _data[N];
  template<int I> typename std::enable_if< I,int>::type _sum() const noexcept
  { return _sum<I-1>() + _data[I]; }
  template<int I> typename std::enable_if<!I,int>::type _sum() const noexcept
  { return _data[I]; }
};

However, this doesn't work on constructors. Suppose, I want to declare the constructor

template<int N> struct A {
   /* ... */
   template<int otherN>
   explicit(A<otherN> const&); // only sensible if otherN >= N
};

but disallow it for otherN < N.

So, can SFINAE be used here? I'm only interested in solutions which allow automatic template-parameter deduction, so that

A<4> a4{};
A<5> a5{};
A<6> a6{a4};  // doesn't compile
A<3> a3{a5};  // compiles and automatically finds the correct constructor

Note: this is a very simplified example where SFINAE may be overkill and static_assert may suffice. However, I want to know whether I can use SFINAE instead.

回答1:

You can add a defaulted type argument to the template:

template <int otherN, typename = typename std::enable_if<otherN >= N>::type>
explicit A(A<otherN> const &);


回答2:

In C++11, you can use a defaulted template parameter:

template <int otherN, class = typename std::enable_if<otherN >= N>::type>
explicit A(A<otherN> const &);

However, if your compiler doesn't support defaulted template parameters yet, or you need multiple overloads, then you can use a defaulted function parameter like this:

template <int otherN>
explicit A(A<otherN> const &, typename std::enable_if<otherN >= N>::type* = 0);


回答3:

There are many ways to trigger SFINAE, being enable_if just one of them. First of all:

Wats is std::enable_if ?

It's just this:

template<bool, class T=void> enable_if{ typedef T type; };
template<class T> enable_if<false,T> {};
template<bool b, class T=void> using enable_if_t = typename enable_f<b,T>::type;

The idea is to make typename enable_if<false>::type to be an error, hence make any template declaration containing it skipped.

So how can this trigger function selection?

Disabling functions

The idea is making the declaration erroneous in some part:

By return type

template<class Type>
std::enable_if_t<cond<Type>::value,Return_type> function(Type);

By a actual parameter

template<class Type>
return_type function(Type param, std::enable_if_t<cond<Type>::value,int> =0) 

By a template parameter

template<class Type, 
    std::enable_if_t<cond<Type>::value,int> =0> //note the space between > and =
return_type function(Type param) 

Selecting functions

You can parametrise different alternatives with tricks like this:

tempplate<int N> struct ord: ord<N-1>{};
struct ord<0> {};

template<class T, std::enable_if<condition3, int> =0>
retval func(ord<3>, T param) { ... }

template<class T, std::enable_if<condition2, int> =0>
retval func(ord<2>, T param) { ... }

template<class T, std::enable_if<condition1, int> =0>
retval func(ord<1>, T param) { ... }

template<class T> // default one
retval func(ord<0>, T param) { ... }

// THIS WILL BE THE FUCNTION YOU'LL CALL
template<class T>
retval func(T param) { return func(ord<9>{},param); } //any "more than 3 value"

This will call the first/second/third/fourth function if condition3 is satisfied, than condition2 than condition1 than none of them.

Other SFINAE triggers

Writing compile-time conditions can be either a matter of explicit specialization or a matter of unevaluated expression success/failure:

for example:

template<class T, class = void>
struct is_vector: std::false_type {};
template<class X>
struct is_vector<vector<X> >:: std::true_type {};

so that is_vector<int>::value is false but is_vecttor<vector<int> >::value is true

Or, by means of introspection, like

template<class T>
struct is_container<class T, class = void>: std::false_type {};

template<class T>
struct is_container<T, decltype(
  std::begin(std::declval<T>()),
  std::end(std::declval<T>()),
  std::size(std::declval<T>()),
  void(0))>: std::true_type {};

so that is_container<X>::value will be true if given X x, you can compile std::begin(x) etc.

The trick is that the decltype(...) is actually void (the , operator discards the previous expressions) only if all the sub-expressions are compilale.


Ther can be even many other alternatives. Hope between all this you can find something useful.