Skip to content

frog-singing/Logicwise

Repository files navigation

Logicwise

Factory for C++ Concepts

English | 简体中文

🔍 Syntax Overview

rangewise<all_of, cartesian_pair>
	::between<RangeA, RangeB>()
	.satisfies(predicate);

Which translates as:

Rangewise, all of the cartesian pairs between range A and range B satisfy the given predicate P.

∀x ∈ A, ∀y ∈ B, P(x, y)

Or:

∀(x, y) ∈ A×B, P(x, y)

Orthogonally compose your logic:

Quantifier × Arrangement × Range × Predicate

Validate as you declare. Let the code speak for itself.

📝 Vocabulary List

Standard Quantifier

Quantifier Symbol Term Meaning
all_of Universal Quantifier For all
any_of Existential Quantifier There exists
none_of ¬∃, ∀¬ Negation of Existential Quantifier There does not exist
not_every ¬∀, ∃¬ Negation of Universal Quantifier Not for all

Numerical Quantifier

Quantifier Symbol Term Meaning
exactly<N> =N Exact Numerical Quantifier Exactly N
at_least<N> ≥N Lower-Bound Quantifier Greater than or equal to N
at_most<N> ≤N Upper-Bound Quantifier Less than or equal to N
more_than<N> >N Strict Lower-Bound Quantifier Strictly greater than N
less_than<N> <N Strict Upper-Bound Quantifier Strictly less than N

[Prologue Sneak Peek] Wondering what you can do with these quantifiers?

Elementwise

Arrangement Meaning
element Individual elements

Pairwise

Arrangement Meaning
permutation_pair Permutations of any 2 elements
combination_pair Order-preserving combination pairs
linear_adjacent_pair One-way linear adjacent pairs
circular_adjacent_pair One-way circular adjacent pairs

Bipartite

Arrangement Meaning
cartesian_pair Cartesian product pairs
zip_pair_truncation Truncated zip-aligned pairs
zip_pair_padding Padded zip-aligned pairs

Both quantifier and arrangement are extensible!

📦 Container Adaptation

Container Requirements Examples Counterexamples
Type Wrapper Class template
where all parameters are types
1. wrapper::type_list
        <bool, int, double, char>
2. std::tuple<bool, int, double, char>
std::array<int, 4>

* The second argument is not a type
Value Wrapper Class template
where all parameters
are values (NTTP)
wrapper::value_list
    <true, 2, 3.0, '4'>
std::make_index_sequence<4>

* This alias actually resolves to:
  std::integer_sequence
      <std::size_t, 0, 1, 2, 3>,
  where the first argument is not a value
Vector-like
Container
Contiguous memory,
amortized O(1) size retrieval,
and subscript access
1. std::vector<int>{ 1, 2, 3, 4 }
2. std::array{ 1, 2, 3, 4 }
3. int my_array[4]{ 1, 2, 3, 4 };
4. std::span{ my_array }
std::list<int>{ 1, 2, 3, 4 }

* Non-contiguous memory
Tuple-like
Container
Not directly supported;
could be converted
using to_variant_array
and accessed via std::visit
container::to_variant_array(
    std::make_tuple(true, 2, 3.0, '4')
)
1. std::make_tuple(1, 2, 3, 4)
2. wrapper::value_list
        <1, 2, 3, 4>{}

* Direct use of tuple-like containers
  is not supported

Note: For bipartite arrangements, in scenarios of a meta container and an instance container, the static size of the instance container is required. Thus, the requirement for the instance container is elevated from a Vector-like Container to an Array-like Container.

🎭 Logic Theater

Logic does not emerge from the void; it resides within our daily experiences and practical needs.
The following test cases, structured as a series of "plays", will guide you through those overlooked corners — embarking on a logical journey from the trivial to the abstract, and ultimately returning to the everyday.

Take your seats — the curtain is about to rise!

Prologue: Another Encounter with Quantifiers 🏃‍➡️💖🏃‍

Don't panic! You’ve probably used quantifiers a thousand times in your code without even knowing their formal name.
Let's get reacquainted with them and see how they act in some fundamental scenarios.

Act I: I Am One of Them — Elementwise 😒🤪😑

rangewise<Quantifier, ElementwiseArrangement>
	::in<Range>()
	.satisfies(predicate);

In the past, once a template parameter entered a template, it was like falling into a black hole, completely losing its trace.
But now, not only can you see them, you can scrutinize them!

Act II: Relations Compose the Whole — Pairwise 😊🤝😊

rangewise<Quantifier, PairwiseArrangement>
	::in<Range>()
	.satisfies(predicate);

Spring, summer, autumn, and winter. Seasons change exactly as they should, but sticking to plans is never quite so regular.
How do our seasonal goals look by the end of the year? And if plans do change, can we still remain true to our original aspirations?

Act III: It Takes Two — Bipartite ✊✋✌️

rangewise<Quantifier, BipartiteArrangement>
	::between<RangeA, RangeB>()
	.satisfies(predicate);

The battle gets intense. How do we break the stalemate? Let's see what classic game theory can teach us.

rangewise<Quantifier, BipartiteArrangement>
	::between(rangeA, rangeB)
	.satisfies(predicate);

The adventurer party sets out on their journey. The dungeon holds mystical treasures, but danger also lurks at every turn. Sometimes their path is smooth sailing; other times, they are beset on all sides.
Unknown depths lie ahead for them to explore, where the next foe could be a monster, or perhaps, other adventurers.

rangewise<Quantifier, BipartiteArrangement>
	::between<RangeA>(rangeB)
	.satisfies(predicate);

Tom has a fruit list. One day, he feels like grabbing some fruit to eat, only to find himself sharing with...

🤔 Design Philosophy

  • Speak Human: Directly model your logic through intuitive syntax, requiring only a fraction of mathematical thinking.

  • Orthogonal Architecture: Quantifiers, arrangements, ranges, and predicates are mutually independent, freely combinable, and highly extensible.

  • Uniform Syntax: Learn one syntax; rule them all. Whether you are validating types, values, or instances, the syntax remains consistent and smooth.

  • Interspecific Hybridization: In bipartite validation, type containers, value containers, and instance containers can be seamlessly mixed and matched, breaking down the reproductive isolation of C++.

  • Zero-Cost Abstraction: Pure compile-time logic verification collapses directly into a boolean constant, introducing zero runtime overhead. Meanwhile, it theoretically minimizes unnecessary compilation overhead.

🏭 Logic Factory

Logicwise goes beyond validation — it can also be used to forge new C++ concepts!
Just feed in some existing concepts as raw materials, choose the right quantifier and arrangement, and the Logicwise assembly line will manufacture a brand-new concept for you!

Clunk, Clunk! (Sounds of factory machinery)

Here, a wrapper refers to a class template that bundles a group of pure types or pure values into a single entity — essentially, a meta container.
Utilizing Logicwise's syntax, you can easily construct various practical wrappers. Logicwise provides several preset wrappers out of the box, such as sets, posets (partially ordered sets), and antichains.
These wrappers do more than just replace the basic type_list and value_list; they can be plugged directly into the range slot of the rangewise syntax to enable increasingly sophisticated logical modeling.

If you want to build your own wrapper, take a look at the example below.
First, define a concept for your wrapper:

template<typename Range>
concept Set =
	rangewise<none_of, combination_pair>
	::template in<Range>()
	.satisfies([] <typename T1, typename T2> { return std::same_as<T1, T2>; });

Then, use this concept to constrain your wrapper:

template<typename... Type>
	requires Set<type_list<Type...>>
struct set {};

Of course, if you prefer not to split them up, writing the full rangewise syntax directly within the wrapper's definition works perfectly too:

template<typename... Type>
	requires (
		rangewise<none_of, combination_pair>
		::template in<type_list<Type...>>()
		.satisfies([] <typename T1, typename T2> { return std::same_as<T1, T2>; })
	)
struct set {};

Next, try putting your newly defined wrapper to work. It can be passed straight into the range slot of the rangewise syntax:

template<typename... Integral>
concept DistinctRawIntegrals =
	rangewise<all_of, element>
	::in<set<Integral...>>()
	.satisfies([] <typename T> { return std::integral<T>; });

If you are curious about how Logicwise implements wrappers under the hood, check out the type set. The example above is a simplified version of it.

Here, a relation is defined specifically over types or values, typically appearing as a binary predicate in the form of a concept or a Lambda expression.
Utilizing Logicwise's syntax, you can take fundamental relations as raw materials to forge complex relations. Logicwise provides several preset relations out of the box, such as inclusion relations and exclusion relations.
From there, you can feed these relations right back into the predicate slot of the rangewise syntax to construct even higher-level relations.

If you want to build your own relations, take a look at the example below.
First, take the fundamental relation std::same_as as raw material to construct a BelongsTo relation:

template<typename Type, typename Range>
concept BelongsTo =
	rangewise<at_least<1>, element>
	::template in<Range>()
	.satisfies([] <typename Element> { return std::same_as<Type, Element>; });

Then, you can use this BelongsTo relation as raw material to construct a SubRangeOf relation:

template<typename SubRange, typename SuperRange>
concept SubRangeOf =
	rangewise<all_of, element>
	::template in<SubRange>()
	.satisfies([] <typename Element> { return BelongsTo<Element, SuperRange>; });

Finally, combine the Set constraint from the previous example with the SubRangeOf relation here, and you assemble a mathematically flawless SubSetOf relation:

template<typename SubSet, typename SuperSet>
concept SubSetOf =
	Set<SubSet> && Set<SuperSet> &&
	SubRangeOf<SubSet, SuperSet>;

If you are curious about how Logicwise implements relations under the hood, check out the typewise inclusion relation. The example above is a simplified version of it.

🌀 Logic Limbo

This is the limbo of logic.
From here, you will delve into the logical structures, internal mechanisms, consistency validation, and degenerate cases, step by step approaching the brink of logical collapse.

If you have stumbled into this place, this is your last chance to turn back.

Arrangement Behavior

Verify the correctness of index sequence for each arrangement in both general and degenerate cases.

Arrangement Meaning
element Individual elements
Arrangement Meaning
permutation_pair Permutations of any 2 elements
combination_pair Order-preserving combination pairs
linear_adjacent_pair One-way linear adjacent pairs
circular_adjacent_pair One-way circular adjacent pairs
Arrangement Meaning
cartesian_pair Cartesian product pairs
zip_pair_truncation Truncated zip-aligned pairs
Arrangement Meaning
zip_pair_padding Padded zip-aligned pairs

Quantifier Behavior

Quantifiers support short-circuit evaluation, meaning that once the result is determined, no further predicate evaluation is performed on the subsequent sequence.
Note that a determined result does not imply that the quantifier is satisfied.

De Morgan's First Law: The negation of a conjunction is equivalent to the disjunction of the negations.
¬(P1 ∧ P2 ∧ ... ∧ Pn) ≡ (¬P1) ∨ (¬P2) ∨ ... ∨ (¬Pn)

De Morgan's Second Law: The negation of a disjunction is equivalent to the conjunction of the negations.
¬(P1 ∨ P2 ∨ ... ∨ Pn) ≡ (¬P1) ∧ (¬P2) ∧ ... ∧ (¬Pn)

Used to verify the correctness of all_of, any_of, none_of, and not_every behaviors.

Degeneracy

In template parameter validation scenarios involving const and volatile qualifiers, as well as reference and pointer declarators, the identity of template parameters may deviate unexpectedly.
This test verifies Logicwise's correctness in handling these edge cases by constructing matrices of various qualifier and declarator combinations.

For scenarios involving a meta container, when the extent is non-traversable, since there are no single variables or variable groups that need to be validated by the predicate, it is impossible to guarantee that valid meta elements — whether types or values — can be retrieved as probes.
In such cases, no constraints are applied on the shape of the predicate, specifically the number and properties of its parameters.
That is, the empty set can match predicates of any shape.

🎬 Behind the Scenes

Important Notes

In principle, the contents within namespace logicwise::detail are intended for internal use only and their stability is not guaranteed. Unless you know exactly what you are doing, please do not use or depend on them directly.
You can use other contents within namespace logicwise as long as they are not marked as [unimplemented].

Side Effects

For now, Logicwise is targeted just for validation, so don't ever stuff side effects into the callback functions! And also don't treat it as a traversal framework...

Well, I know the following code compiles:

using my_list = wrapper::type_list<bool, int, double, char>;

constexpr int count = []
	{
		int i{ 0 };

		(void)rangewise<quantifier::all_of, arrangement::element>
			::in<my_list>()
			.satisfies([&] <typename T> { ++i; return true; });

		return i;
	}();

static_assert(count == 4, "");

But that's way too weird! The validation mode does not guarantee a uniform traversal direction. Just promise me you won't write code like this, alright? (If you guys really need it, maybe I'll consider adding a traversal mode...)

Known Compiler Issues

For detailed information about compiler compatibility and known issues, see KNOWN_COMPILER_ISSUES.

To Be Done

Although I have said a lot about wrappers and relations, I didn't get around to testing them yet. Actually, they belong to a higher-level system, and some of the dependency features haven't been implemented. For now, let's just treat them as bonus features.

The performance test remains a quest for another day. Right now, correctness is still the main focus.

As for the features labeled [unimplemented] — don't take them seriously. I just wrote them for fun.

🔌 Integration

Prerequisites

  • C++ Standard: C++20 or later
  • Compiler Support: Any modern C++ compiler with C++20 support (MSVC, Clang, GCC)
  • Build System: CMake 3.21+

Header-Only Library

Logicwise is a header-only library — simply include it:

#include <logicwise.h>

CMake Integration

Add the following to your CMakeLists.txt:

include(FetchContent)

FetchContent_Declare(logicwise
  GIT_REPOSITORY "https://github.com/frog-singing/Logicwise.git"
  GIT_TAG "v1.0.0"
)

FetchContent_MakeAvailable(logicwise)

target_link_libraries(your_target PRIVATE logicwise::logicwise)

Manual Integration

Download the Logicwise library and include the Logicwise/library directory in your project's include paths.

Logicwise also depends on the Manipond library. Download Manipond and include the Manipond/exosuit_library and Manipond/meta_library directories in your project's include paths.

It is recommended to place Manipond at Logicwise/external/Manipond, which is the default path specified in Logicwise/CMakeLists.txt.

📜 License

MIT License
Copyright © 2026 Frog Singing (@frog-singing)

See the LICENSE file for details.

💡 Hello Logic!

std::cout << "Hello Logic!" << std::endl;

About

Factory for C++ Concepts – mass & structural compile-time validation for cpp20 metaprogramming

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors