13

Assuming I have a templated type, e.g.

template<typename A, typename B, typename C>
struct mytype { };

How do I write a concept that checks whether a type is an instantiation of that template?

template<typename T>
concept MyType = requires(T x) { ??? }

I can't figure an obvious way of doing it without resolving to old-style specialised detector types or maybe a marker base type.

user7610
  • 25,267
  • 15
  • 124
  • 150
MrMobster
  • 1,851
  • 16
  • 25
  • not sure what exactly you are asking. Concepts are not in the standard yet, or did i miss it? – 463035818_is_not_an_ai Jan 14 '19 at 13:14
  • They are in draft C++20, so of course my question is based on the current draft – MrMobster Jan 14 '19 at 13:18
  • 1
    What's the use of this? – cpplearner Jan 14 '19 at 13:25
  • @cpplearner One of such a use case might be perfect forwarding for arguments of a particular type only. Better concepts than SFINAE :) – Daniel Langr Jan 14 '19 at 13:29
  • @cpplearner The direct and obvious use is enforcing type safety without having to go through the usual verbose template dance. That is, instead of `template void do_stuff(mytype)` you could just do `template do_stuff(T)`. And hopefully one day `do_stuff(MyType T)`, as Stroustrup has envisioned. – MrMobster Jan 14 '19 at 13:49
  • @MrMobster: That's not really what concepts are about. You're talking about making a specific usage scenario more convenient. Concepts are about finding a way to express constraints on the capabilities of one or more template arguments. That is, you're not restricting a template to a specific type; you're restricting a template to a specific *interface*, which multiple types could (theoretically) satisfy. Using such a mechanism just so you don't have to type a few extra words is not the point of the thing. – Nicol Bolas Jan 15 '19 at 04:50
  • @NicolBolas I am talking about type safety. And that is what concepts are all about — fixing a huge gap in C++ type system — the fact that templates are not real types but instead macro-like constructs that rely on duck typing. Sure, main advantage of concepts is that they provide type safety around a common API (akin to what protocols and traits do in Swift and Rust), but I also see no reason why they should not be used to provide type safety around a single implementation of a parametrised type as well. – MrMobster Jan 15 '19 at 05:48
  • @MrMobster: It shouldn't provide that because you can already do that, as evidenced by the fact that you wrote the first version. `do_stuff(MyType auto T)` is a far more *obtuse* spelling of what you actually want, since you have to track down the template `MyType` to figure out that it means "an instantiation of `mytype`. I consider that no different from deciding to replace `my_func(int i)` with `my_func(Int auto i)` or some such. – Nicol Bolas Jan 15 '19 at 05:58
  • @NicolBolas so you'd argue that repeating the entire set of template parameters (which are completely irrelevant to the algorithm) every single time results in a better code than a single type specifier? And sure, I agree that there is a certain logical disconnect between mytype and MyType, which could again be avoided if templates were proper parametrised types in the system... – MrMobster Jan 15 '19 at 06:16
  • @MrMobster: Better code? Yes. First, it avoids the disconnect I outlined. But equally importantly second, it makes you stop and ask... if `do_stuff` doesn't use those template parameters, why do I *want* my template `do_stuff` function to *only take* a `mytype` instantiation? Why shouldn't I write a proper concept interface, which `do_stuff` can be written against? One that is not *explicitly* dependent on `mytype` itself? It's not like `range::sort` is written against `vector` specifically; it is written against any random access range; `vector` merely provides that interface. – Nicol Bolas Jan 15 '19 at 06:21
  • Related: https://stackoverflow.com/questions/17390605/doing-a-static-assert-that-a-template-type-is-another-template – ecatmur Feb 19 '21 at 09:32

5 Answers5

11

Using C++17 class template argument deduction, you should be able to do something like this:

template<typename A, typename B, typename C>
struct mytype { };

template<class T>
concept C1 = requires(T x) { 
    { mytype{x} } -> std::same_as<T>;
};

mytype{x} uses class template argument deduction to deduce A, B and C, so this is valid if you can construct a mytype<A, B, C> from a T. In particular, this is valid if mytype is copy-constructible since you have an implicitly declared copy-deduction guide similar to:

template <typename A, typename B, typename C>
mytype(mytype<A, B, C>) -> mytype<A, B, C>;

Checking that T is also the constructed mytype instantiation avoid matching other deduction guides, e.g., this would match for any type without the -> std::same_as<T>:

template <class A, class B, class C>
struct mytype {
    mytype(A);
};

template <class A>
mytype(A) -> mytype<A, A, A>;

The proposed solution does not work for non copy-constructible classes, even though should be possible to make it work for move-only classes.


Tested with and : https://godbolt.org/z/ojdcrYqKv

Holt
  • 36,600
  • 7
  • 92
  • 139
  • But who's to say A, B and C can be deduced like that? – einpoklum Jan 14 '19 at 15:11
  • @einpoklum There is always an implicitly generated [copy-deduction guide](http://www.eel.is/c++draft/over.match.class.deduct#1.3). This guide exists even with a (implicitly) deleted copy-constructor, but in this case the concept will fail (which is specified in my answer). – Holt Jan 14 '19 at 15:15
  • You mean, a deduction guide which will manage to deduce A, B and C exactly for those types T which are instantiations of the template? – einpoklum Jan 14 '19 at 22:09
  • I have no idea why anyone would downvote you answer — it is spot on and answers my question exactly. – MrMobster Jan 15 '19 at 05:50
  • @einpoklum Assuming `mytype` has a non-deleted copy-constructor, you have an implicitly generated deduction guide similar to: `template mystruct(mystruct const&) -> mystruct;`, which is the copy-deduction guide. – Holt Jan 15 '19 at 07:58
  • 1
    This solution does not conform to the C++20 standard. The return type requirement in the requires expression should be a concept, not a type. Replace -> T with -> std::same_as. – Björn Sundin Dec 28 '20 at 12:45
  • @Holt Could you kindly provide a solution for move-only classes? I am not sure about the correct way to do it. Thanks! – fdev Oct 02 '22 at 14:51
10

You can define your own meta-function (type trait) for that purpose:

template <typename T>
struct is_mytype : std::false_type { };

template <typename A, typename B, typename C>
struct is_mytype<mytype<A, B, C>> : std::true_type { };

template <typename T>
concept MyType = is_mytype<T>::value;

But to say the truth, I don't know whether there isn't a way how to defining such a concept directly without the need of a separate metafunction.

Alastair Harrison
  • 1,281
  • 12
  • 14
Daniel Langr
  • 22,196
  • 3
  • 50
  • 93
4

You can write a generalized trait to check for specializations:

template <typename T, template <typename...> class Z>
struct is_specialization_of : std::false_type {};

template <typename... Args, template <typename...> class Z>
struct is_specialization_of<Z<Args...>, Z> : std::true_type {};

template <typename T, template <typename...> class Z>
inline constexpr bool is_specialization_of_v = is_specialization_of<T,Z>::value;

Which you can make into either a generalized concept:

template<typename T, template <typename...> class Z>
concept Specializes = is_specialization_of_v<T, Z>;

template<typename T>
concept MyType = Specializes<T, mytype>;

or just a specialized one:

template<typename T>
concept MyType = is_specialization_of_v<T, mytype>;
Holt
  • 36,600
  • 7
  • 92
  • 139
Barry
  • 286,269
  • 29
  • 621
  • 977
  • 2
    It's not really generalized, as it only works on template classes that themselves take types. So you can't ask if it is a specialization of `std::array` or even `std::span`, since those take non-type template parameters. – Nicol Bolas Jan 15 '19 at 04:45
  • @NicolBolas Vast majority of templates only take type parameters though. So I'd say it's pretty generalized. But yes, it won't work on those two. – Barry Jan 15 '19 at 04:57
  • 4
    http://open-std.org/JTC1/SC22/WG21/docs/papers/2020/p2098r1.pdf – pooya13 Dec 27 '20 at 18:34
2

In the interests of terseness:

template<typename T>
concept MyType = requires(T** x) {
    []<typename A, typename B, typename C>(mytype<A, B, C>**){}(x);
};

The double-pointer is necessary to avoid derived-to-base conversions, ex. struct S : mytype<int, int, int> {}.

This doesn't work in clang at present, since it doesn't allow lambdas in unevaluated context. You can workaround by providing a helper variable template:

template<class T> constexpr auto L = []<typename A, typename B, typename C>(mytype<A, B, C>**){};
template<typename T>
concept MyType = requires(T** x) { L<T>(x); };

As long as mytype's template arguments are all types, you can make this even terser using placeholders:

template<typename T>
concept MyType = requires(T** x) { [](mytype<auto, auto, auto>**){}(x); };

At present, this only works in gcc.

ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • Brilliant, thanks! I used the last one, but with only single pointers because I _want_ to be able to detect derived classes of the templated type. That's 14 fewer lines of code. \o/ – underscore_d Mar 22 '21 at 08:45
  • 1
    It seems that `auto` in template arguments of function argument types is not actually standard C++20, although GCC silently allows it :-( Clang does not though, and [this other thread](https://stackoverflow.com/questions/60358154/auto-as-a-template-argument-placeholder-for-a-function-parameter) indicates that was only in the Concepts TS but is not valid Standard C++20. So much for GCC's `-std=c++20` flag, then. :-/ I've rewritten without `auto`. – underscore_d Dec 04 '21 at 17:11
0

If you give your template class some traits you can do the following:

template<typename A, typename B, typename C>
struct mytype {
    using a_type = A;
    using b_type = B;
    using c_type = C;
};

With the associate concept:

template <typename T>
concept is_mytype =
    std::is_same_v<
        std::remove_const_t<T>,
        mytype<typename T::a_type, typename T::b_type, typename T::c_type>>;

Or, if mytype has members of those types you can skip the traits:

template<typename A, typename B, typename C>
struct mytype {
    A a_inst;
    B b_inst;
    C c_inst;
};

Giving the concept:

template <typename T>
concept is_mytype =
    std::is_same_v<
        std::remove_const_t<T>,
        mytype<decltype(T::a_inst), decltype(T::b_inst), decltype(T::c_inst)>>;
JGLA
  • 1