简体   繁体   中英

std::enable_if with multiple or-conditions

I am struggling to find the correct syntax for allowing multiple OR'ed std::enable_if conditions for my template functions.

#include <type_traits>

template <typename T>
using Foo = std::enable_if_t<std::is_same_v<T, int>>;

template <typename T>
using Bar = std::enable_if_t<std::is_same_v<T, float>>;

// Fine - accepts only ints
template <typename T, typename = Foo<T>>
void FooIt(T t) {}

// Fine - accepts only floats
template <typename T, typename = Bar<T>>
void BarIt(T t) {}

template <typename T,
          // This does not work
          typename = std::enable_if_t<Bar<T>::value || Foo<T>::value>>
void FooOrBarIt(T t) {}

int main() {
  FooIt(1);
  BarIt(1.0f);

  // FooIt(1.0f);  // OK - No float version of FooIt
  // BarIt(1);     // OK - No int version of BarIt

  // Not OK - Both fails to compile
  // FooOrBarIt(1);
  // FooOrBarIt(1.0f);
}

I want the FooOrBarIt function to accept both int s and float s but nothing else by combining the previous Foo and Bar conditions.

Note that I want to avoid changing the previous conditions, I want to combine them as is. Anything C++17 is fine.

The current code fails with compile errors like:

candidate template ignored: requirement 'std::is_same_v<int, float>' was not satisfied [with T = int]

in clang.

You are trying to use enable_if_t inside of enable_if_t , which is not what you need. You need to use is_same_v inside of 1 enable_if_t , eg:

template <typename T,
          typename = std::enable_if_t<std::is_same_v<T,float> || std::is_same_v<T,int>>>

So adjust your using statements accordingly, eg:

#include <type_traits>

template <typename T>
inline constexpr bool Is_Int = std::is_same_v<T, int>;

template <typename T>
using Enable_If_Int = std::enable_if_t<Is_Int<T>>;

template <typename T>
inline constexpr bool Is_Float = std::is_same_v<T, float>;

template <typename T>
using Enable_If_Float = std::enable_if_t<Is_Float<T>>;

// Fine - accepts only ints
template <typename T, typename = Enable_If_Int<T>>
void FooIt(T t) {}

// Fine - accepts only floats
template <typename T, typename = Enable_If_Float<T>>
void BarIt(T t) {}

template <typename T,
          typename = std::enable_if_t<Is_Float<T> || Is_Int<T>>>
void FooOrBarIt(T t) {}

int main() {
    FooIt(1);
    BarIt(1.0f);

    // FooIt(1.0f);  // OK - No float version of FooIt
    // BarIt(1);     // OK - No int version of BarIt

    // OK - Both compile fine
    FooOrBarIt(1);
    FooOrBarIt(1.0f);
}

Online Demo

SFINAE doesn't work quiet as simply with logical operators. You should look to use std::disjunction and its counterpart std::conjunction to combine logical tests for SFINAE. These where introduced with C++17, so you should be able to use them with the setup you have.

Your || operator works on bool types.

The bool types you are looking for already exist in your code. std::is_same_v is always a bool.

template <typename T, typename = std::enable_if_t<
           std::is_same_v<T,int> || std::is_same_v<T,float>
         >>* = nullptr

Consider that an expression containing both Foo<T> and Bar<T> can never be well-defined.

You are doing cargo-cult SFINAE. By this, I mean you are using SFINAE patterns to pull off what you need, and not understanding why you are doing those patterns.

This usually isn't all that bad. SFINAE is sort of insane, as an accidental feature that leaked into the language and gave us Turing-complete overload resolution. I mean, last I checked, the fact it worked with template class specialization isn't actually in the standard.

So following existing patterns to make it look reasonable is a good plan.

When you want to do something new, however, you have to understand what is actually going on.

template <typename T>
using Foo = std::enable_if_t<std::is_same_v<T, int>>;

this is a template alias that results in the type void if T passes the test, and in a substitution failure if T fails the test.

// Fine - accepts only ints
template <typename T, typename = Foo<T>>
void FooIt(T t) {}

your comment is a bit wrong. FooIt<double, void> works fine here.

What is going on here is that your default type argument produces a substitution failure if T is not an int . If T is an int , the default value for the 2nd type argument is void .

template <typename T,
      // This does not work
      typename = std::enable_if_t<Bar<T>::value ||  Foo<T>::value>>
void FooOrBarIt(T t) {}

of course it doesn't.

There is no ::value on the type void . Bar<T> is either void or a substitution failure, and the same for Foo<T> .

Neither supports ::value . So you get an error.

Your Foo<T> and Bar<T> are not a great pattern. But you stated you want to keep them.

So what I'd start with is this:

template<class T, template<class>class Z, class=void, template<class>class...Zs>
struct test{};
template<class T, template<class>class Z>
struct test<T,Z,Z<T>>{
  using type=void;
};
template<class T,
  template<class>class Z0,
  template<class>class Z1, 
  template<class>class...Zs
>
struct test<T, Z0, Z0<T>, Z1, Zs...>:
  test<T, Z1, void, Zs...>
{};
template<class T, template<class>class Z0, template<class>class...Zs>
using test_t = typename test<T, Z0, void, Zs...>::type;

now test_t can be used to fuze your traits.

template <typename T,
      typename = test_t<T, Bar, Foo>>
void FooOrBarIt(T t) {}

in a way you are familiar with.

Digression

Note that this style of SFINAE has problems where you can't use it to dispatch between two overloads of FooOrBarIt . Ie,

template<class T, class=Bar<T>>
void bob(T) {}
template<class T, class=Foo<T>>
void bob(T) {}

this results in an overload resolution error.

Another cargo-cult pattern:

template<class T>
using Bar = std::enable_if_t<std::is_same_v<T,int>, bool>;
template<class T>
using Foo = std::enable_if_t<std::is_same_v<T,double>, bool>;

template<class T, Bar<T> =true>
void bob(T) {}
template<class T, Foo<T> =true>
void bob(T) {}

looks slightly different, but permits the two bob overloads above to work.

For that variant, you'd have to change how you joined them:

template<Ts...>
using AllTraits = bool;

template <typename T,
      AllTraits<Foo<T>, Bar<T>> = true>
void FooOrBarIt(T t) {}

note that the =true in this particular cargo cult version is not a test; =false would produce the exact same results.

For the sake of providing a contrast, a C++20 version:

#include <type_traits>

template<typename T>
concept Foo = std::is_same_v<T, int>;

template<typename T>
concept Bar = std::is_same_v<T, float>;

template<typename T>
concept FooOrBar = Foo<T> || Bar<T>;

template<Foo T>
void FooIt(T) { }

template<Bar T>
void BarIt(T) { }

template<FooOrBar T>
void FooOrBarIt(T) { }

int main() {
  FooIt(1); // OK
  BarIt(1.0f); // OK
  
  // FooIt(1.0f);  // OK - No float version of FooIt
  // BarIt(1);     // OK - No int version of BarIt


  FooOrBarIt(1); // OK
  FooOrBarIt(1.0f); // OK
  // FooOrBarIt(1.0); // No double version of FooOrBarIt
}

Concepts are a lot easier to combine in this fashion.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM