How to Create Custom Matchers for Custom Assertions in Catch2
Creating custom matchers in Catch2 involves deriving from MatcherBase<T> for type-specific matching or MatcherGenericBase for generic range matching, implementing the match() and describe() methods, and optionally providing factory functions for clean syntax.
Catch2’s matcher subsystem allows you to write expressive, domain-specific assertions that keep test code readable while handling complex validation logic. Whether you need to verify numeric ranges, compare containers, or validate custom data structures, custom matchers provide a type-safe extension mechanism. This guide walks through the two architectural styles available in the Catch2 repository and shows how to implement them correctly.
Understanding the Matcher Architecture
The matcher system in catchorg/Catch2 consists of three distinct layers working together to provide both flexibility and performance.
Factory functions provide type deduction and uniform call-site syntax (e.g., IsBetween(1, 10)), allowing users to write REQUIRE_THAT(value, Matcher()) without explicit template parameters.
Matcher base classes define the polymorphic interface used by the framework. Old-style matchers inherit from Catch::Matchers::MatcherBase<T> defined in src/catch2/matchers/catch_matchers.hpp, while new-style matchers use Catch::Matchers::MatcherGenericBase from src/catch2/matchers/catch_matchers_templated.hpp.
Combination operators (&&, ||, !) enable building composite matchers through MatchAllOf, MatchAnyOf, and MatchNotOf classes, also declared in src/catch2/matchers/catch_matchers.hpp. These operators work only on lvalue matchers that outlive the combined expression to prevent use-after-free errors.
Old-Style Matchers with MatcherBase
Old-style matchers derive from MatcherBase<T> and offer optimal compile-time performance for specific types. This approach works best when you know the exact type being matched at compile time.
Implementation Requirements
To create an old-style matcher, inherit from Catch::Matchers::MatcherBase<T> and override two virtual functions:
bool match(T const& arg) const– returnstruewhen the value satisfies your conditionstd::string describe() const– returns a human-readable description of what the matcher checks
Always provide a factory function to hide template instantiation details from test authors.
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <sstream>
template <typename T>
class IsBetweenMatcher : public Catch::Matchers::MatcherBase<T> {
T m_low, m_high;
public:
IsBetweenMatcher(T low, T high) : m_low(low), m_high(high) {}
bool match(T const& value) const override {
return value >= m_low && value <= m_high;
}
std::string describe() const override {
std::ostringstream oss;
oss << "is between " << m_low << " and " << m_high;
return oss.str();
}
};
template <typename T>
IsBetweenMatcher<T> IsBetween(T low, T high) {
return { low, high };
}
// Usage in tests
TEST_CASE("Old-style custom matcher") {
CHECK_THAT(5, IsBetween(1, 10));
CHECK_THAT(-3.5, IsBetween(-4.0, -2.0));
}
The describe() method generates failure messages like "5 is between 1 and 10" when assertions fail, making test output immediately actionable.
New-Style Matchers with MatcherGenericBase
New-style matchers inherit from Catch::Matchers::MatcherGenericBase and support templated match() functions, enabling polymorphic matching across different container types or ranges.
Templated Matching for Ranges
Unlike MatcherBase<T>, which locks you to a specific type, MatcherGenericBase allows the match function to accept arguments by value or as template parameters. This is essential when writing matchers that work with any range-like type (vectors, arrays, lists) simultaneously.
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_templated.hpp>
#include <algorithm>
template <typename Range>
struct EqualsRangeMatcher : Catch::Matchers::MatcherGenericBase {
explicit EqualsRangeMatcher(Range const& ref) : m_ref(ref) {}
template <typename OtherRange>
bool match(OtherRange const& oth) const {
using std::begin; using std::end;
return std::equal(begin(m_ref), end(m_ref), begin(oth), end(oth));
}
std::string describe() const override {
return "equals range " + Catch::rangeToString(m_ref);
}
private:
Range const& m_ref;
};
template <typename Range>
auto EqualsRange(Range const& r) -> EqualsRangeMatcher<Range> {
return EqualsRangeMatcher<Range>{ r };
}
// Usage demonstrating cross-type comparison
TEST_CASE("New-style custom matcher") {
std::vector<int> expected{1, 2, 3};
REQUIRE_THAT(std::array<int, 3>{ {1, 2, 3} }, EqualsRange(expected));
// Composite matcher example
REQUIRE_THAT(std::list<int>{4, 5, 6},
EqualsRange(std::vector<int>{4, 5, 6}) ||
EqualsRange(std::array<int, 3>{ {4, 5, 6} }));
}
As implemented in src/catch2/matchers/catch_matchers_templated.hpp, MatcherGenericBase removes the requirement to specify a single matched type upfront, making your matchers more reusable across the codebase.
Composing Matchers and Managing Lifetimes
Catch2 supports combining matchers with &&, ||, and ! operators, but these combinations create objects that store references to their constituent matchers.
Safe Usage Patterns
When using composite matchers in a single expression, temporaries live long enough for the check to complete safely:
auto closeToZero = Catch::Matchers::WithinAbs(0, 0.01);
auto notZero = !Catch::Matchers::WithinULP(0., 1);
CHECK_THAT(value, closeToZero && notZero); // Safe: temporaries live for the full expression
However, storing a combined matcher requires ensuring all sub-matchers outlive the composite:
// Store components first to extend their lifetime
auto isClose = Catch::Matchers::WithinAbs(0, 0.01);
auto isNotZero = !Catch::Matchers::WithinULP(0., 1);
auto combined = isClose && isNotZero; // Safe: both parts outlive 'combined'
CHECK_THAT(value, combined);
The operators return MatchAllOf, MatchAnyOf, or MatchNotOf objects defined in src/catch2/matchers/catch_matchers.hpp that hold references to the original matchers. Storing a combined matcher while allowing its components to go out of scope results in undefined behavior due to dangling references.
Summary
- Old-style matchers derive from
MatcherBase<T>insrc/catch2/matchers/catch_matchers.hppfor type-specific validation with minimal overhead. - New-style matchers derive from
MatcherGenericBaseinsrc/catch2/matchers/catch_matchers_templated.hppto support templatedmatch()functions working across multiple container types. - Every custom matcher must implement
bool match(Arg const& arg) constandstd::string describe() constto satisfy the framework interface. - Factory functions (e.g.,
IsBetween(),EqualsRange()) provide type deduction and clean syntax for test authors. - Composite matchers created with
&&,||, and!store references to their operands; ensure component matchers outlive any stored combined objects.
Frequently Asked Questions
What is the difference between MatcherBase and MatcherGenericBase?
MatcherBase<T> requires you to specify the matched type as a template parameter and implements match() as a virtual function, making it ideal for specific types and slightly faster to compile. MatcherGenericBase allows match() to be a template method accepting any type, enabling matchers that work with ranges, containers, or polymorphic types without upfront type constraints.
Can I combine custom matchers with built-in Catch2 matchers?
Yes. All matchers in Catch2, whether custom or built-in, support the &&, ||, and ! operators as long as they inherit from the appropriate base class. You can write expressions like REQUIRE_THAT(value, MyCustomMatcher() || Catch::Matchers::WithinAbs(0, 0.1)) provided you manage the lifetime of stored matcher objects correctly.
Where should I place my custom matcher definitions?
Define custom matchers in a header file included by your test files, or directly in the test translation unit if only needed locally. Ensure you include <catch2/matchers/catch_matchers.hpp> for old-style matchers or <catch2/matchers/catch_matchers_templated.hpp> for new-style matchers. Factory functions should be placed in a namespace to avoid global name pollution.
Why does my composite matcher cause a crash or undefined behavior?
This occurs when a combined matcher (created with &&, ||, or !) outlives its constituent parts. According to the Catch2 source in src/catch2/matchers/catch_matchers.hpp, these operators return objects storing const references to the original matchers. If you store auto combined = matcher1 && matcher2 while matcher1 or matcher2 are temporaries that get destroyed, the stored references become dangling. Store sub-matchers as named variables with longer lifetimes before combining them.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →