I have a code that conditionaly modifies input type based on type predicate, like this:
template <typename T>
using result = std::conditional_t<type_pred<T>::value, type_cast<T>::type, T>;
It compiles only if T satisfies type_cast's constraints (regardless of the result of type_pred<T>::value).
As an example, in the following code std::make_unsigned_t requres T to be integral, so compiler complains if T is not.
template <typename T>
using make_unsigned_if_integral_t =
std::conditional_t<std::is_integral_v<T>, std::make_unsigned_t<T>, T>;
// this works
static_assert(std::is_same_v<make_unsigned_if_integral_t<int>, unsigned int>);
// compiler complains on this
using expect_float = make_unsigned_if_integral_t<float>;
If type_cast implementaion does not constraint its param, the approach works well, e.g:
// no constraints on T here, so custom_make_unsigned<float>::type is valid (but never actually used)
template <typename T> struct custom_make_unsigned {
template <typename U> struct impl {
using type = U;
};
template <std::signed_integral U> struct impl<U> {
using type = std::make_unsigned_t<U>;
};
using type = typename impl<T>::type;
};
template <typename T>
using custom_make_unsigned_if_integral_t =
std::conditional_t<std::is_integral_v<T>, typename custom_make_unsigned<T>::type, T>;
static_assert(std::is_same_v<custom_make_unsigned_if_integral_t<float>, float>);
The question is, why does std::conditional require both branches to be "valid" and what part of the c++ language (or standard library) defines such behaviour?
The question is, why does
std::conditionalrequire both branches to be "valid"?
You see it as branch, but instantiation happens before the resolution of std::conditional<..>::type.
It is similar to
int f(bool b, int n1, int n2) { return b ? n1 : n2; }
and
int n1 = 42;
int* n2 = nullptr;
f(true, n1, *n2); // UB with derefencing nullptr
Workaround is extra indirection:
template <typename T>
using result =
typename std::conditional_t<
type_pred<T>::value,
type_cast<T>,
std::type_identity<T>
>::type;
This is not specific to std::conditional. Consider for example
template <typename A, typename B>
struct Foo {};
You can only instantiate Foo<A,B> when A and B are types (in this example they need no definition, but they must be declared).
The only way to allow "invalid" types is by not instantiating certain specalizations. SFINAE does that. An example from cppreference:
template<class T> int f(typename T::B*); template<class T> int f(T); int i = f<int>(0); // uses second overload
In a nutshell, substition of the template argument in the first template fails when T is int, because int::B makes no sense. This is not an error, but the compiler continues and simply discards that overload. (Hence, the name Substitution Failure Is Not An Error). Only during substitution of the template argument such failure is not an error.
If you write for example
int i = f<int::B>(0);
this is just a hard error.
Your custom_make_unsigned_if_integral_t does not make use of SFINAE, but it uses a concept to select the "right" specialization. using type = std::make_unsigned_t<U>; is never instantiated for any T that does not satisfy the std::signed_integral concept. There is no substition failure nor any invalid type.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With