Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Short-circuiting template specialization in std::disjunction

Some Background:

I'm working on putting together a templated class which, as part of template specialization, deduces a type to use for one of its members. This data member needs to support being streamed over the wire, and I'm trying to keep the system as flexible and extensible as possible (with the goal that new variants of the type can be created by modifying some high-level elements of the specialization logic without getting into the guts of the implementation code). Some of the existing usages specialize this data member to be an enum, and the streaming code supports converting this value back and forth to a 32-bit integer for transmission over the wire.

Because an enum could be defined (either implicitly or explicitly) to be backed by a different type -- most dangerous in the case being a 64-bit value -- I'd like to be able enforce that if the resolved type is an enum, its underlying type must be a 32-bit integer (more generally, I just need to enforce that it's a maximum of 32 bits, but I'll be worrying about that complexity once the simpler case is working).

My Attempted Solution:

Stitching together some of the tools provided by type_traits, I came up with the following:

#include <cstdint>
#include <type_traits>

using TestedType = /* Logic for deducing a type */;
constexpr bool nonEnumOrIntBacked =
    !std::is_enum_v<TestedType>
    || std::is_same_v<std::underlying_type_t<TestedType>, std::int32_t>;
static_assert(nonEnumOrIntBacked,
    "TestedType is an enum backed by something other than a 32-bit integer");

However, when I tried to compile this (using Visual Studio 2017 on the latest update), I was met with the error text 'TestedType': only enumeration type is allowed as an argument to compiler intrinsic type trait '__underlying_type'. Seeing this, I tried an alternative formulation using std::disjunction, which I believe should short-circuit evaluation of templates if an earlier condition evaluates to true (I've ommitted the std qualifiers to make this a bit more readable):

disjunction_v<
    negation<is_enum<TestedType>>,
    is_same<underlying_type_t<TestedType>, int32_t>
>;

I've also tried wrapping the offending usage of underlying_type_t inside an enable_if predicated on the type being an enum, but didn't have success with that either.

My Question:

Do boolean operators in general (and std::disjunction in particular) not short-circuit evaluation of templates? On the cppreference page for std::disjunction, it states the following (emphasis mine):

Disjunction is short-circuiting: if there is a template type argument Bi with bool(Bi::value) != false, then instantiating disjunction::value does not require the instantiation of Bj::value for j > i

Reading this, I would have expected the ill-formed nature of underlying_type_t<TestedType> for some non-enum type to be irrelevant, since downstream types don't need to be considered once something upstream has been evaluated as true.

If my reading of the spec is incorrect on this point, is there another way to accomplish this check at compile-time, or will I need to add a runtime check to enforce this?

like image 234
bionicOnion Avatar asked Dec 24 '22 07:12

bionicOnion


1 Answers

Template arguments are "evaluated" eagerly just like regular arguments. The part causing problems is underyling_type_t, but it's present in full in both versions. You need to delay that part until after you know TestedType is an enum type. This is fairly straightforward with constexpr if (live example):

template<typename T>
constexpr bool nonEnumOrIntBackedImpl() {
    if constexpr (std::is_enum_v<T>) {
        return std::is_same_v<std::underlying_type_t<T>, std::int32_t>;
    } else {
        return false;
    }
}

template<typename T>
constexpr bool nonEnumOrIntBacked = nonEnumOrIntBackedImpl<T>();

Prior to C++17, one common method is tag dispatching (live example):

template<typename T>
constexpr bool nonEnumOrIntBackedImpl(std::true_type) {
    return std::is_same<std::underlying_type_t<T>, std::int32_t>{};
}

template<typename T>
constexpr bool nonEnumOrIntBackedImpl(std::false_type) {
    return false;
}

template<typename T>
constexpr bool nonEnumOrIntBacked = nonEnumOrIntBackedImpl<T>(std::is_enum<T>{});
like image 86
chris Avatar answered Dec 28 '22 08:12

chris