[This is a variant of @mutableVoid's answer above, but a little more explicit on two things that I had to think about a little. It concerns only concept
s, which is what I wanted to get under test, so it only applies to C++20 onwards].
First, assume we have a concept
that we wish to test. This can be simple and just defer to existing type traits:
template <class T>
concept integral = std::is_integral_v<T>;
Or it could be more complex, in this case, a concept that checks that type T
has a size function that returns the type's size_type
(and you can add more requirements to this "compound requirement", say, that it has a []
operator):
template<typename T>
concept sizeable = requires(T x) {
{x.size()} -> std::same_as<typename T::size_type>;
};
So, now we have some concepts. We want to test that these concepts work like we expect.
What we will need to do is to get a bool
to test. Well, that's easy, because that's what concepts gives us naturally:
std::cout << std::string(typeid(integral<int>).name()) << std::endl;
std::cout << std::string(typeid(sizeable<int>).name()) << std::endl;
produces (with GCC):
b
b
So, we can check these, either with static_assert
(i.e. mutableVoid's answer), which will fail your compilation if your concept isn't working:
static_assert(integral<int>);
static_assert(!integral<float>);
static_assert(sizeable<std::vector<int>>);
static_assert(!sizeable<int>);
You can prove to yourself that this works by removing a !
and observing that the compilation fails.
However, you may not want compilation to fail with a compiler error. If you'd rather feed this to your unit test framework, it doesn't have to be static_assert
ed:
void say(bool b, const std::string& s) {
std::cout << s << " = " << b << std::endl;
}
int main() {
say(integral<int>, "integral, int");
say(integral<float>, "integral, float");
say(sizeable<std::vector<int>>, "sizeable, vector of int");
say(sizeable<int>, "sizeable, int");
return 0;
}
This produces something like this:
integral, int = 1
integral, float = 0
sizeable, vector of int = 1
sizeable, int = 0
Now, you can plug this into whatever unit testing library you want, and you can check that your concepts aren't accidentally permitting types that you expect to fail.
However, do note there are some limitations:
- You can't check that there are no unexpected type acceptances, because your tests would be infinite in size (the same as any negative test, really).
- Although these tests will compile and run even if the concepts are broken, it's possible that other code will choke if the concepts are "allowing" types though that they shouldn't, and your overall build may well still fail. For example, if functions or classes are
static_assert
ing their types for their own purposes.
- Indeed, you may still want
static_assert
s in the main program compilation to prevent non-compliant code even being written in the first place: after all, compile-time correctness is a good thing. However, this gives you a chance to make sure that your concepts and static assertions are working together as expected. If the static_assert
s are for some reason not compiled or are changed to be too permissive, you may not notice the concept is now defective.
boost::enable_if
, etc. – IngrowthTEST(false, "This wasn't supposed to compile :(")
). If the compilation succeeds, it gets added to the list of unit tests, and we get a failure. If the compilation fails, the build system just ignores the error and moves on. Haven't worked out the nuts and bolts of it yet, though, and even then it would probably not work for all testing frameworks. – Bankrupt