Is the decltype of this immediately-invoked lambda expression in C++20 well-formed?

Viewed 123

While trying to create a toy DSL, I managed to come across an odd construct that neither clang nor gcc handled the way I expected:

#include <utility>

template <class...>
struct disjunction {};

template <auto>
struct literal {};

template <class T, T Min, T Max>
using range = decltype([]<T... Cs>(std::integer_sequence<T, Cs...>) {
    return disjunction<literal<T{Min + Cs}>..., literal<Max>>{};
}(std::make_integer_sequence<T, Max - Min>{}));

template <class T, T... Cs> requires (sizeof...(Cs) == 1)
constexpr literal<Cs...> operator""_c() { return {}; }

template <class T, T Min, T Max>
constexpr range<T, Min, Max> operator-(literal<Min>, literal<Max>) { return {}; }

constexpr auto digits = "0"_c - "9"_c;

using expected = const disjunction<
    literal<'0'>, literal<'1'>, literal<'2'>, literal<'3'>, literal<'4'>,
    literal<'5'>, literal<'6'>, literal<'7'>, literal<'8'>, literal<'9'>>;

static_assert(std::is_same_v<decltype(digits), expected>);

Here, clang crashes and gcc evaluates digits as the value 0 of type int.

I managed to find the following workaround using partial template specialization:

namespace detail {
template <class, auto, auto> struct range;

template <class T, T... Cs, T Min, T Max>
struct range<std::integer_sequence<T, Cs...>, Min, Max> {
    using type = disjunction<literal<T{Min + Cs}>..., literal<Max>>;
};
} // namespace detail

template <class T, T Min, T Max>
using range = typename detail::range<
    std::make_integer_sequence<T, Max - Min>, Min, Max>::type;

In this case, the implementation of range allows the static_assert above to succeed for both clang and gcc, but it would be nice to be able to implement this template without a class template helper.

Finally, while using the initial implementation of range, if the expression "0"_c - "9"_c is wrapped in decltype(...) like this:

using digits = decltype("0"_c - "9"_c);

using expected = disjunction<
    literal<'0'>, literal<'1'>, literal<'2'>, literal<'3'>, literal<'4'>,
    literal<'5'>, literal<'6'>, literal<'7'>, literal<'8'>, literal<'9'>>;

static_assert(std::is_same_v<digits, expected>);

Then clang does not crash or report a diagnostic for the static_assert as shown here. However, gcc still fails, and when checking the actual type of digits like this:

template <class Type>
auto f() { static_assert(sizeof(Type) == 0); }

int main() { f<digits>(); }

clang crashes again with

error: cannot compile this l-value expression yet
int main() { f<digits>(); }
             ^~~~~~~~~

This last error message seems to suggest clang acknowledging this construct as well-formed, but I'm not certain.

As the title reads, I'd like to seek an understanding of whether my attempted implementation of range using decltype of an immediately-invoked lambda expression is well-formed.

0 Answers
Related