1

What's the idiomatic way for constraining a type template parameter so that it only accepts instantiations of a specific template?

e.g.,

template<typename P> class C { C() = default; ... }; template<typename T> class Accepted { ... }; template<typename T> class Other { ... }; C<Accepted<float>> obj1; // should compile C<Accepted<int>> obj2; // should compile C<Other<int>> obj3; // should not compile C<double> obj4; // should not compile 
5
  • What about something like C<double>? Commented Dec 25, 2017 at 9:12
  • @StoryTeller should not compile. edited to add. Commented Dec 25, 2017 at 9:13
  • As a partial solution: declare as template<typename> class C; and define as template<template<typename> typename TT, typename T> struct C<TT<T>> {...} with a static assertion that TT is Accepted, a constraint which you could relax by asserting only the contracts satisfied by (instantiations of) Accepted are satisfied by the given type. I would generally design Accepted to internally require/assert that its contracts are satisfied, and leave C agnostic to the type beyond asserting that an instantiation of Accepted with the given type is well-formed. Commented Dec 25, 2017 at 9:29
  • Maybe you can define C to have a T parameter that will be used to form corresponding Accepted specialization instead of taking that Accepted specialization as parameter? So C<int> will utilize Accepted<int> automatically. Commented Dec 25, 2017 at 9:34
  • @VTT See "...an instantiation of Accepted with the given type is well-formed." @Danra You may get more mileage out of std::is_base_of<Accepted<T>,TT<T>>, assuming extensions of an instantiation satisfy the same constraints as their base (necessarily or voluntarily). It remains to be seen if this is your intention, but as you are, you're not just requiring that the type is derived from Accepted<T>, you're stripping all non-trivial derivations. Commented Dec 25, 2017 at 9:41

2 Answers 2

3

Specialization is the answer.

template<typename P> class C; template<typename T> class Accepted { ... }; template<typename P> class C<Accepted<P>> { C() = default; ... }; 

The above makes any C<Accepted<T>> be well-formed because it chooses the specialization when instantiating. While any sort of other C<T> chooses the primary specialization, which is not defined, and so will not compile.

Sign up to request clarification or add additional context in comments.

3 Comments

I think this needs a little adaptation to suit OP's needs as they're written - this only works until an alternative is defined. It sounds like the X-Y problem, without more information, but the exact phrasing was "it only accepts instantiations of a specific template", which would require an explicit assertion. I suggested std::is_base_of if not something closer to Concepts, but my general approach to X-Y's is to answer in both the letter and the spirit of the question.
@JohnP - Then answer, by all means. SO's strength is answer diversity.
I don't often think I bring enough to the discussion to compete with the first responders - I'm used to comprehensive solutions showing up while I'm working on a rough draft - but thank you for reminding me it's not a competition at all.
2

I see two possible answers without knowing more about the context.

template<typename T> class C; template<template<typename> typename TT, typename T> class C<TT<T>> { ... static_assert(std::is_same<TT<T>, Accepted<T>>::value, "The given type is not an instantiation of Accepted!"); ... } 

This is only so helpful - what is so special about Accepted? You may have a second template, e.g. SortedAccepted<T>, that meets the same requirements, but you've set out to constrain the template argument to Accepted<T>.

One option is to design Accepted as a contract so that derivations from it must satisfy the same constraints as Accepted itself:

template<template<typename> typename TT, typename T> class C { ... static_assert(std::is_base_of<Accepted<T>, TT<T>>::value, "The given type is not a derivation of Accepted!"); ... }; 

This follows from the design principle that Accepted should be extended, but not modified, by its derivations. From the previous example, maybe Accepted is required to have an idempotent 'sort' method -- accepted.sort().sort() == accepted.sort() -- whereas SortedAccepted implicitly retains this property, but additionally provides sorted_accepted.sort() == sorted_accepted. You have nothing to lose in allowing a SortedAccepted wherever you expect an Accepted, but plenty to gain!

If I understood your problem correctly, you might be interested in Traits and Concepts.

Type traits as seen in <type_traits> and Boost's extensions are reified contracts, providing compile-time guarantees about collections sharing traits, expressions of these, and so on. For example, S s; T t; auto u = s + t where std::is_arithmetic<S>::value and std::is_arithmetic<T>::value are true, guarantees that std::is_arithmetic<decltype(u)>::value is true; if either is_floating_point, so is u.

Concepts are the next logical step from type traits: essentially, the reified requirement of traits. (See: 'Concepts Lite')

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.