5

I'm having a hard time understanding how to stop code from being evaluated with std::conditional_t in the false branch.

#include <type_traits>

using namespace std;

namespace {
    template <typename T>
    using BaseDifferenceType = decltype (T{} - T{});
}

int main ()
{
    using T = int;
    static_assert(! is_enum_v<T>);
    BaseDifferenceType<T> xxx {};
    
    // PROBLEM IS HERE - though is_enum is false, we still evaluate underlying_type<T> and complain it fails
    using aaa = conditional_t<is_enum_v<T>, underlying_type_t<T>, BaseDifferenceType<T>>;
    
    return 0;
}

You can try this online at https://www.onlinegdb.com/uxlpSWVXr.

Compiling (with C++17) gives the error:

error: ‘int’ is not an enumeration type
  typedef __underlying_type(_Tp) type;
                                 ^~~~ main.cpp: In function ‘int main()’: main.cpp:16:87: error: template argument 2 is invalid
using aaa = conditional_t<is_enum_v<T>, underlying_type_t<T>, BaseDifferenceType<T>>;
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
lewis
  • 1,116
  • 1
  • 12
  • 28
  • As an aside: [`std::declval()`](//en.cppreference.com/w/cpp/utility/declval) is the canonical way to get an expression of specified type for an unevaluated context, without bothering with constructors. – Deduplicator Dec 14 '20 at 14:40

2 Answers2

6

The answer is simple:
You don't.

std::conditional_t always has three fully evaluated arguments:
Something boolean-like, and two types.

If one of them cannot be evaluated if the other is selected, you need to use a custom template and specialize it appropriately:

template <class T, bool = std::is_enum_v<T>>
struct type_finder { using type = std::underlying_type_t<T>; };
template <class T>
struct type_finder<T, false> { using type = BaseDifferenceType<T>; };
template <class T>
using type_finder_t = typename type_finder<T>::type;

An alternative is using if constexpr and automatic return-type-deduction:

template <class T>
auto type_finder_f() {
    if constexpr (std::is_enum_v<T>)
        return std::underlying_type_t<T>();
    else
        return BaseDifferenceType<T>();
}
template <class T>
using type_finder_t = decltype(type_finder_f<T>());
Deduplicator
  • 44,692
  • 7
  • 66
  • 118
  • Thank you for this. ALso a great solution! I had a tough choice WHICH answer to prefer, but for my current situation, the other solution looked slightly better, but thank you for this technique! I really like it and will no doubt find good use for it as well! – lewis Dec 14 '20 at 14:09
5

A possible alternative pass through a creation of a custom type traits as follows

template <template <typename...> class Cont, typename ... Ts>
struct LazyType
 { using type = Cont<Ts...>; };

and rewrite your std::conditional as follows

using aaa = std::conditional_t<
               std::is_enum_v<T>,
               LazyType<underlying_type_t, T>,
               LazyType<BaseDifferenceType, T>>::type;
// ............................................^^^^^^

Observe I've added a final ::type: it's the "constructor" of the required type, completely avoiding the unwanted type.

Off Topic Unrequested Suggestion: avoid

using namespace std;

It's considered bad practice

max66
  • 65,235
  • 10
  • 71
  • 111
  • That works nicely if all template-parameters are types. Admittedly not uncommon, especially in TMP. – Deduplicator Dec 12 '20 at 00:36
  • @Deduplicator - Nothing forbid to develop different custom type traits for different template parameters. For example, a `Lazy_array` that receive a type and a `std::size_t`, compatible with `std::array`. It's just required a common `type` that can be extracted. Starting from C++20 you can also use `std::type_identity`, for non problematic types (but, before C++20, construct something `std::type_identity` is very simple). – max66 Dec 12 '20 at 11:22