Wildcard for C++ concepts saying "accepting anything for this template argument"
Asked Answered
M

1

2

Is there a way to allow a concept with template arguments, to be ok with any template parameter provided?

I.e. some kind of wildcard magic for template argument placeholder?

A usage example:

template<class Me, class TestAgainst>
concept derived_from_or_same_as = 
    std::same_as<Me, TestAgainst> ||
    std::derived_from<Me, TestAgainst>;

Above is needed because unfortunately primitive types behave differently than class types for is_base_of and derived_from.

Now we can define a Pair concept that checks the provided types:

template<class P, class First, class Second>
concept Pair = requires(P p) {
    requires derived_from_or_same_as<decltype(p.first), First>;
    requires derived_from_or_same_as<decltype(p.second), Second>;
};

Use case [a] - accept any valid pair of As or sub-type of As:

// this works well
void doWithPairOfA(const Pair<A, A> auto& p) { /* */ }

Use case [b] - accept any valid pair, no restrictions on the inner types:

// this is *pseudo code* as Pair<auto, auto> is not allowed
void doWithAnyPair(const Pair<auto, auto> auto& p) { /* */ }

Unfortunately, auto is not allowed as template argument placeholder in C++20.

So Pair<auto, auto> is not the solution for now.


Other languages allow such a syntax in a way, though not with the same exact semantics and meaning as requested here, but the usage looks quite similar.

Python:

// Any as wildcard
foo(lst: List[Any]) -> List[str]

Java:

// '?' as wildcard
List<String> foo(List<?> lst)

The pre C++20 syntax would look something like1:

Use case [a] - trying to accept any valid pair of As or sub-type of As:

// not as good as with concepts above, this allows only "pair" of A and A
// **but rejects sub-types of A, which is not good**
// and there is no check that this is actually a pair (can be added with SFINAE)
template<template<class, class> typename PAIR>
void doWithPairOfA(const PAIR<A, A>& p) { /* */ }

Use case [b] - accept any valid pair, no restrictions on the inner types:

// not as good as we would wish - we do allow any kind of "pair"
// but there is no check that this is actually a pair (can be added with SFINAE)
template<template<class, class> typename PAIR, typename ANY1, typename ANY2>
void doWithAnyPair(const PAIR<ANY1, ANY2>& p) { /* */ }

Pre C++20 code

Can concepts present a better solution?


1 Related question (pre C++20) on templates: Templates accepting “anything” in C++

Mither answered 25/2, 2020 at 17:50 Comment(0)
M
5

You can achieve wildcard behavior by modifying the Pair concept to accept and check a tag type Any.

Let's first declare Any as a tag class, no need to implement it.

class Any;

Now we can create a type_matches concept to check if a type T matches a given type A, with the following rules:

T matches A

  • if A is Any -- or --
  • if T==A or if T is derived from A

As noted in the question, the check for T==A or T is derived from A can be made for class types just with std::derived_from however primitive types require adding the test for std::same_as.

The wildcard match would be achieved with the following code:

template<class Me, class TestAgainst>
concept type_matches =
    std::same_as<TestAgainst, Any> ||
    std::same_as<Me, TestAgainst>  ||
    std::derived_from<Me, TestAgainst>;

The Pair concept would be modified to:

template<class P, class First, class Second>
concept Pair = requires(P p) {
    requires type_matches<decltype(p.first), First>;
    requires type_matches<decltype(p.second), Second>;
};

The code can allow now both required use cases.

Use case [a] - accept any valid pair of As or sub-type of As:

// can be called with a Pair of As or sub-type of As
void doWithPairOfA(const Pair<A, A> auto& p) { /* */ }

Use case [b] - accept any valid pair, no restrictions on the inner types:

void doWithAnyPair(const Pair<Any, Any> auto& p) { /* */ }

Code: https://godbolt.org/z/higX9f

Mither answered 25/2, 2020 at 17:50 Comment(5)
Same change you made to type_matches applies equally to Pair: concept Pair = type_matches<typename P::first_type, First> && type_matches<typename P::second_type, Second>;Langrage
@Langrage I prefer not to require having first_type and second_type thus to a allow user defined pair with only first and second. But I thought requires requires is a distinguished part of C++20...Mither
Hm? There's no requires requires in your solution?Langrage
is it required?Mither
I don't know what you're asking.Langrage

© 2022 - 2024 — McMap. All rights reserved.