Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does std::conditional require both branches to be defined? [duplicate]

Tags:

c++

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?

like image 984
Andrii Avatar asked Oct 22 '25 05:10

Andrii


2 Answers

The question is, why does std::conditional require 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;
like image 133
Jarod42 Avatar answered Oct 23 '25 17:10

Jarod42


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.

like image 32
463035818_is_not_a_number Avatar answered Oct 23 '25 19:10

463035818_is_not_a_number



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!