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:

  1. bool match(T const& arg) const – returns true when the value satisfies your condition
  2. std::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> in src/catch2/matchers/catch_matchers.hpp for type-specific validation with minimal overhead.
  • New-style matchers derive from MatcherGenericBase in src/catch2/matchers/catch_matchers_templated.hpp to support templated match() functions working across multiple container types.
  • Every custom matcher must implement bool match(Arg const& arg) const and std::string describe() const to 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →