I want to define a class template Bar<T>
, which uses another class template Foo<U>
, and the template parameter T
is passed as the template argument of Foo<U>
somewhere.
Then if U
in Foo<U>
has some concept constraits (std::floating_point
for example), should I repeat the constrait on T
when defining Bar
?
#include <concepts>
template <std::floating_point U>
struct Foo { /* ... */ };
// <typename T> or <std::floating_point T>?
template <typename T>
struct Bar {
void Func() {
Foo<T> foo; // Uses Foo here
/* ... */
}
};
<typename T>
and <std::floating_point T>
, what are the benefits and tradeoffs of each version?
Write a one-sentence or one-paragraph overview of the purpose of Bar
. On the basis of that overview, is it reasonable to infer a constraint on the template parameter? If so, add that constraint. It might be that Bar
has an intrinsic reason for a constraint that you had overlooked (brought to your attention by the use of Foo
).
If not, you might want to reconsider if your use of Foo
is justified. If it is, you might want to constrain just the function that uses Foo
, and constrain it on the basis of Foo<T>
being valid (e.g. requires requires { typename Foo<T>; }
), since that accurately reflects why (and where) there is a constraint. This also minimizes the "blast radius" should something break because of changes to other parts of the code.
One other consideration I would take into account is user-friendliness. How actionable is the error message should someone provide an invalid template argument? After all, one of the benefits of concepts is greater readability of error messages. Try compiling intentionally-bad code like Bar<int> b;
b.Func();
and see what your compiler tells you. Does your compiler give you enough information to fix the error? Or is the message likely to send someone to some Q&A website somewhere to ask for clarification?
For instance, if you do not add a constraint to Bar
, the error message might lead someone to think the error is in Bar
rather than in their use of Bar
. In my trial, the error message did mention the line number where Func()
was invoked, but it called out the line Foo<T> foo; // Uses Foo here
in Bar::Func()
as the likely location of the error. On the surface, that looks like an error in Bar
. You might be blamed as the author of Bar
.
Also consider whether it would be helpful to have a compilation error even if the programmer forgets to invoke Func()
. If every valid use-case of Bar
necessitates calling Func()
, it might be worth having the constraint on Bar
itself rather than on Bar::Func()
. (Otherwise, adding a forgotten call to Func()
could cause a compilation error, making the fix look more like a break.)
So a lot depends on the purpose of Bar
. As a starting point, I would constrain Bar::Func
to template parameters for which its definition is valid; i.e. for which Foo<T>
is valid. Then I'd take a look at the error messages, factor in the experience level of the programmers, and make a judgment call.