Can I pattern-match a type without writing a custom trait class?
Asked Answered
B

2

7

Since C++20 concepts aren't standardized yet, I'm using static_assert as a makeshift concept check, to provide helpful error messages if a type requirement isn't met. In this particular case, I have a function which requires that a type is callable before getting its result type:

template <typename F, typename... Args>
void example() {
  static_assert(std::is_invocable_v<F, Args...>, "Function must be callable");
  using R = std::invoke_result_t<F, Args...>;

  // ...
}

In addition, I require that the callable's result must be some kind of std::optional, but I don't know what type the optional will hold, so I need to get that type from it:

using R = // ...
using T = typename R::value_type;  // std::optional defines a value_type

However, this will fail if type R doesn't have a value_type, e.g. if it's not a std::optional as expected. I'd like to have a static_assert to check for that first, with another nice error message if the assertion fails.

I could check for an exact type with something like std::is_same_v, but in this case I don't know the exact type. I want to check that R is some instance of std::optional, without specifying which instance it must be.

One way to do that is with a helper trait:

template <typename T>
struct is_optional { static constexpr bool value = false; };

template <typename T>
struct is_optional<std::optional<T>> { static constexpr bool value = true; };

template <typename T>
constexpr bool is_optional_v = is_optional<T>::value;

…and then I can write:

static_assert(is_optional_v<R>, "Function's result must be an optional");

That works, but it seems a little awkward to pollute my namespace with a helper trait just for a one-off check like this. I don't expect to need is_optional anywhere else, though I can imagine possibly ending up with other one-off traits like is_variant or is_pair too.

So I'm wondering: is there a more concise way to do this? Can I do the pattern matching on instances of std::optional without having to define the is_optional trait and its partial specialization?

Beadledom answered 29/6, 2019 at 5:38 Comment(2)
Does “concise” mean fewer lines, or just fewer namespace-scope declarations?Parthia
You could create a re-usable trait that would allow you to define constexpr bool is_optional_v = is_template_of<std::optional, T>::value; and could be reused for pair/variant with one liners.Rompish
B
5

Following the suggestion by several respondents, I made a re-usable trait:

template <typename T, template <typename...> typename Tpl>
struct is_template_instance : std::false_type { };

template <template <typename...> typename Tpl, typename... Args>
struct is_template_instance<Tpl<Args...>, Tpl> : std::true_type { };

template <typename T, template <typename...> typename Tpl>
constexpr bool is_template_instance_v = is_template_instance<T, Tpl>::value;

…so that I can write:

static_assert(is_template_instance_v<R, std::optional>, "Function's result must be an optional");

This is just as many lines and declarations as the is_optional trait, but it's no longer a one-off; I can use the same trait for checking other kinds of templates (like variants and pairs). So now it feels like a useful addition to my project instead of a kluge.

Beadledom answered 29/6, 2019 at 14:31 Comment(1)
Great idea a re-usable triait :-) - Anyway, I suggest a little improvement: instead of defining a static constexpr bool value, you could inherit from std::false_type (main template) or std::true_type (specialization); this way you inherit the facilities (by example: the operator() or the operator value_type) that are present in std::integral_constant.Teahouse
T
4

Can I do the pattern matching on instances of std::optional without having to define the is_optional trait and its partial specialization?

Maybe using implicit deduction guides for std::optional?

I mean... something as

using S = decltype(std::optional{std::declval<R>()});

static_assert( std::is_same_v<R, S>, "R isn't a std::optional" );

Explanation.

When R is std::optional<T> for some T type, std::optional{r} (for an r value of type R) should call the copy constructor and the resulting value should be of the same type R.

Otherwise, the type should be different (std::optional<R>).

The following is a full compiling example.

#include <iostream>
#include <optional>

template <typename T>
bool isOptional ()
 {
   using U = decltype(std::optional{std::declval<T>()});

   return std::is_same_v<T, U>;
 }

int main ()
 {
   std::cout << isOptional<int>() << std::endl;                // print 0
   std::cout << isOptional<std::optional<int>>() << std::endl; // print 1   
 }

Anyway, I support the suggestion by super: create a more generic type-traits that receive std::option as template-template argument.

Teahouse answered 29/6, 2019 at 12:4 Comment(1)
Clever. I think I'll go with the re-usable trait approach, but I like this idea. Having seen this, I'll probably find another use for this trick somewhere. :-)Beadledom

© 2022 - 2024 — McMap. All rights reserved.