nkr
A C++20 library with a custom meta-programming language.
Designs

Many key concepts concerning design exist in nkr, and their presence is felt all throughout the library. Their purposes involve the goals of memorization, scalability, performance, readability, and more. This page exists to argue the case for each design decision, to show how to take advantage of each pattern, and importantly, how to maintain that pattern when adding to the library.

Every single one of these designs has a non-trivial purpose and a lot of research and prototyping to back them up. A voluminous number of code examples exist per design and should be taken full advantage of in order to grasp why and how each pattern exists to specifically benefit you and your end-users. Every single piece of code is sampled from various test suites and thus is always up-to-date. Due to the length of these code examples, a table of contents located to your right is available for your convenience.

Exception Avoidance

(W.I.P)


Global Equality Operators

We define equality operators outside of the nkr namespace and in the global scope. We follow a very specific pattern. For constexpr types we write:

namespace nkr {
class constexpr_t;
}
constexpr nkr::boolean::cpp_t operator ==(const nkr::tr<nkr::any_tg, nkr::t<nkr::constexpr_t>> auto& a, const auto& b) noexcept;
constexpr nkr::boolean::cpp_t operator ==(const nkr::tr<nkr::any_tg, nkr::t<nkr::constexpr_t>> auto& a, const auto&& b) noexcept;
constexpr nkr::boolean::cpp_t operator ==(const nkr::tr<nkr::any_tg, nkr::t<nkr::constexpr_t>> auto&& a, const auto& b) noexcept;
constexpr nkr::boolean::cpp_t operator ==(const nkr::tr<nkr::any_tg, nkr::t<nkr::constexpr_t>> auto&& a, const auto&& b) noexcept;
constexpr nkr::boolean::cpp_t operator !=(const nkr::tr<nkr::any_tg, nkr::t<nkr::constexpr_t>> auto& a, const auto& b) noexcept;
constexpr nkr::boolean::cpp_t operator !=(const nkr::tr<nkr::any_tg, nkr::t<nkr::constexpr_t>> auto& a, const auto&& b) noexcept;
constexpr nkr::boolean::cpp_t operator !=(const nkr::tr<nkr::any_tg, nkr::t<nkr::constexpr_t>> auto&& a, const auto& b) noexcept;
constexpr nkr::boolean::cpp_t operator !=(const nkr::tr<nkr::any_tg, nkr::t<nkr::constexpr_t>> auto&& a, const auto&& b) noexcept;
Used to filter a type by its qualifications, and by other types, templates, identities,...
Definition: tr_dec.h:262
The entire library is contained within this namespace.
Definition: array/cpp_t_dec.h:17
nkr::tr$::ts< AND_tg, nkr::tuple::types_t< type_p > > t
A way to wrap a single type for use with an nkr::TR expression, to differentiate it from a template.
Definition: tr_dec.h:176

And for non-constexpr constructible types we merely drop the constexpr at the beginning of the declaration:

namespace nkr {
class non_constexpr_t;
}
nkr::boolean::cpp_t operator ==(const nkr::tr<nkr::any_tg, nkr::t<nkr::non_constexpr_t>> auto& a, const auto& b) noexcept;
nkr::boolean::cpp_t operator ==(const nkr::tr<nkr::any_tg, nkr::t<nkr::non_constexpr_t>> auto& a, const auto&& b) noexcept;
nkr::boolean::cpp_t operator ==(const nkr::tr<nkr::any_tg, nkr::t<nkr::non_constexpr_t>> auto&& a, const auto& b) noexcept;
nkr::boolean::cpp_t operator ==(const nkr::tr<nkr::any_tg, nkr::t<nkr::non_constexpr_t>> auto&& a, const auto&& b) noexcept;
nkr::boolean::cpp_t operator !=(const nkr::tr<nkr::any_tg, nkr::t<nkr::non_constexpr_t>> auto& a, const auto& b) noexcept;
nkr::boolean::cpp_t operator !=(const nkr::tr<nkr::any_tg, nkr::t<nkr::non_constexpr_t>> auto& a, const auto&& b) noexcept;
nkr::boolean::cpp_t operator !=(const nkr::tr<nkr::any_tg, nkr::t<nkr::non_constexpr_t>> auto&& a, const auto& b) noexcept;
nkr::boolean::cpp_t operator !=(const nkr::tr<nkr::any_tg, nkr::t<nkr::non_constexpr_t>> auto&& a, const auto&& b) noexcept;

Following the above pattern makes all equality operator overloads templates, and with that point in mind, this pattern avoids two very important conflicts:

  1. Because the first template parameter, and only the first template parameter is constrained specifically to an identity and never a generic, this pattern can never have ambiguous operator overload collisions with other operators that follow the same exact pattern. This allows us to extend this pattern to all types ad infinitum, including types that inherit base types with their own overloads defined or types that can otherwise implicitly be converted to another.
  2. Because all possible values are covered in the second parameter, including both lvalues and rvalues of any type whatsoever, it is impossible for operator overload resolution to resolve to any implicit conversions from the second type, in particular during reverse operator resolution - a potentially frustrating addition to the C++20 standard.

Following this pattern gives an extreme amount of flexibility for users. A user need not worry about the order of their arguments and whether including this or that file will somehow cause the compiler to spit out a thousand-line-long error message, resulting in a headache for the user every time it happens.

However, this comes at the cost of extra development effort. Every single type must define their own operator overloads explicitly. This means if you wish to use the overload of another type, you must define its operators and explicitly cast to that type. For this reason, the pattern was designed such that you only need to define one of the eight operators, and the rest can be easily defined as proxies.

The following is a full example of how one would define the operators for two different types, neither of which knows if the other is compatible. Both of them have an identifiable primary inner type, which adds options to their algorithms that otherwise might not be there:

namespace nkr {
class equality_a_t
{
public:
// The primary inner type.
using value_t = long;
public:
value_t value;
public:
constexpr equality_a_t(value_t value) noexcept :
value(value)
{
}
};
class equality_b_t
{
public:
// The primary inner type. It's different from equality_a_t's.
using value_t = long long;
public:
value_t value;
public:
constexpr equality_b_t(value_t value) noexcept :
value(value)
{
}
// Notice that we can construct this type with equality_a_t.
constexpr equality_b_t(equality_a_t value) noexcept :
value(value.value)
{
}
};
}
// Forward declare equality_a_t's ops so it can see equality_b_t's ops.
constexpr nkr::boolean::cpp_t operator ==(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_a_t>> auto& a, const auto& b) noexcept;
constexpr nkr::boolean::cpp_t operator ==(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_a_t>> auto& a, const auto&& b) noexcept;
constexpr nkr::boolean::cpp_t operator ==(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_a_t>> auto&& a, const auto& b) noexcept;
constexpr nkr::boolean::cpp_t operator ==(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_a_t>> auto&& a, const auto&& b) noexcept;
constexpr nkr::boolean::cpp_t operator !=(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_a_t>> auto& a, const auto& b) noexcept;
constexpr nkr::boolean::cpp_t operator !=(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_a_t>> auto& a, const auto&& b) noexcept;
constexpr nkr::boolean::cpp_t operator !=(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_a_t>> auto&& a, const auto& b) noexcept;
constexpr nkr::boolean::cpp_t operator !=(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_a_t>> auto&& a, const auto&& b) noexcept;
// And vice versa.
constexpr nkr::boolean::cpp_t operator ==(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_b_t>> auto& a, const auto& b) noexcept;
constexpr nkr::boolean::cpp_t operator ==(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_b_t>> auto& a, const auto&& b) noexcept;
constexpr nkr::boolean::cpp_t operator ==(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_b_t>> auto&& a, const auto& b) noexcept;
constexpr nkr::boolean::cpp_t operator ==(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_b_t>> auto&& a, const auto&& b) noexcept;
constexpr nkr::boolean::cpp_t operator !=(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_b_t>> auto& a, const auto& b) noexcept;
constexpr nkr::boolean::cpp_t operator !=(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_b_t>> auto& a, const auto&& b) noexcept;
constexpr nkr::boolean::cpp_t operator !=(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_b_t>> auto&& a, const auto& b) noexcept;
constexpr nkr::boolean::cpp_t operator !=(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_b_t>> auto&& a, const auto&& b) noexcept;
// Here we define equality_a_t's operators.
inline constexpr nkr::boolean::cpp_t
operator ==(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_a_t>> auto& a, const auto& b)
noexcept
{
// We want the value_t and not a value_t& type.
using a_t = nkr::cpp::reference_value_t<decltype(a)>;
using b_t = nkr::cpp::reference_value_t<decltype(b)>;
if constexpr (nkr::cpp::is_any_tr<b_t, a_t>) {
// If b_t is equality_a_t, it's super easy to compare the two.
return a.value == b.value;
} else if constexpr (nkr::cpp::to_tr<b_t, nkr::cpp::just_non_qualified_t<typename a_t::value_t>>) {
// If b_t is equality_b_t, then this won't be branched to because it doesn't have a cast to
// value_t operator. However another type might, and because our type defines equality based on
// its primary inner type, it makes sense to have this branch. However if our type doesn't have
// a primary inner type, it maybe should be excluded. This technique relies on a good design of
// b_t, which should be castable to other types only when it's meaningful.
return a.value == static_cast<nkr::cpp::just_non_qualified_t<typename a_t::value_t>>(b);
} else if constexpr (nkr::cpp::to_tr<b_t, nkr::cpp::just_non_qualified_t<a_t>>) {
// If b_t is equality_b_t, then this won't be branched to because a_t cannot be constructed from
// an equality_b_t, nor can equality_b_t be cast to a_t. Notice that this recursively calls this
// operator, we don't need to define a_t == a_t twice!
return a == static_cast<nkr::cpp::just_non_qualified_t<a_t>>(b);
} else if constexpr (nkr::cpp::to_tr<a_t, nkr::cpp::just_non_qualified_t<b_t>>) {
// If b_t is equality_b_t, then even though equality_a_t cannot be cast to equality_b_t,
// equality_b_t can be constructed with equality_a_t, so this is the branch that will compile.
// However take note that equality_b_t's operators must be defined!
return static_cast<nkr::cpp::just_non_qualified_t<b_t>>(a) == b;
} else {
// Bail, because there's no defined way to compare these two types. Make sure to let the user
// know what happend. As complicated as it is, this standards compliant technique works great:
[] <nkr::boolean::cpp_t _ = false>() { static_assert(_, "these two values can not be compared."); }();
}
}
// The rest of these just call the above operator.
inline constexpr nkr::boolean::cpp_t
operator ==(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_a_t>> auto& a, const auto&& b)
noexcept
{
return operator ==(a, b);
}
inline constexpr nkr::boolean::cpp_t
operator ==(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_a_t>> auto&& a, const auto& b)
noexcept
{
return operator ==(a, b);
}
inline constexpr nkr::boolean::cpp_t
operator ==(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_a_t>> auto&& a, const auto&& b)
noexcept
{
return operator ==(a, b);
}
inline constexpr nkr::boolean::cpp_t
operator !=(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_a_t>> auto& a, const auto& b)
noexcept
{
return !operator ==(a, b);
}
inline constexpr nkr::boolean::cpp_t
operator !=(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_a_t>> auto& a, const auto&& b)
noexcept
{
return !operator ==(a, b);
}
inline constexpr nkr::boolean::cpp_t
operator !=(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_a_t>> auto&& a, const auto& b)
noexcept
{
return !operator ==(a, b);
}
inline constexpr nkr::boolean::cpp_t
operator !=(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_a_t>> auto&& a, const auto&& b)
noexcept
{
return !operator ==(a, b);
}
// Here we do the same thing as above, but for equality_b_t.
// We have the same algorithm because the two types are similar, but other types may have different algorithms.
inline constexpr nkr::boolean::cpp_t
operator ==(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_b_t>> auto& a, const auto& b)
noexcept
{
using a_t = nkr::cpp::reference_value_t<decltype(a)>;
using b_t = nkr::cpp::reference_value_t<decltype(b)>;
if constexpr (nkr::cpp::is_any_tr<b_t, a_t>) {
return a.value == b.value;
} else if constexpr (nkr::cpp::to_tr<b_t, nkr::cpp::just_non_qualified_t<typename a_t::value_t>>) {
return a.value == static_cast<nkr::cpp::just_non_qualified_t<typename a_t::value_t>>(b);
} else if constexpr (nkr::cpp::to_tr<b_t, nkr::cpp::just_non_qualified_t<a_t>>) {
return a == static_cast<nkr::cpp::just_non_qualified_t<a_t>>(b);
} else if constexpr (nkr::cpp::to_tr<a_t, nkr::cpp::just_non_qualified_t<b_t>>) {
return static_cast<nkr::cpp::just_non_qualified_t<b_t>>(a) == b;
} else {
[] <nkr::boolean::cpp_t _ = false>() { static_assert(_, "these two values can not be compared."); }();
}
}
inline constexpr nkr::boolean::cpp_t
operator ==(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_b_t>> auto& a, const auto&& b)
noexcept
{
return operator ==(a, b);
}
inline constexpr nkr::boolean::cpp_t
operator ==(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_b_t>> auto&& a, const auto& b)
noexcept
{
return operator ==(a, b);
}
inline constexpr nkr::boolean::cpp_t
operator ==(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_b_t>> auto&& a, const auto&& b)
noexcept
{
return operator ==(a, b);
}
inline constexpr nkr::boolean::cpp_t
operator !=(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_b_t>> auto& a, const auto& b)
noexcept
{
return !operator ==(a, b);
}
inline constexpr nkr::boolean::cpp_t
operator !=(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_b_t>> auto& a, const auto&& b)
noexcept
{
return !operator ==(a, b);
}
inline constexpr nkr::boolean::cpp_t
operator !=(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_b_t>> auto&& a, const auto& b)
noexcept
{
return !operator ==(a, b);
}
inline constexpr nkr::boolean::cpp_t
operator !=(const nkr::tr<nkr::any_tg, nkr::t<nkr::equality_b_t>> auto&& a, const auto&& b)
noexcept
{
return !operator ==(a, b);
}

Now we can fully equate values of these two types in every imaginable way:

// This calls equality_a_t branch 1
static_assert(nkr::equality_a_t(1) == nkr::equality_a_t(1));
// This calls equality_a_t branch 4 then equality_b_t branch 1
static_assert(nkr::equality_a_t(1) == nkr::equality_b_t(1));
// This calls equality_b_t branch 1
static_assert(nkr::equality_b_t(1) == nkr::equality_b_t(1));
// This calls equality_b_t branch 3 then equality_b_t branch 1
static_assert(nkr::equality_b_t(1) == nkr::equality_a_t(1));
// Even const and/or volatile values play nicely:
const nkr::equality_a_t equality_a = 1;
volatile nkr::equality_a_t equality_b = 1;
CHECK((equality_a == equality_b));
CHECK((equality_b == equality_a));
// Any combination of lvalues, rvalues, and temp values will just work:
CHECK((nkr::equality_a_t(1) == equality_b));
CHECK((equality_a == nkr::cpp::Move(equality_b)));
CHECK((nkr::equality_b_t(1) == equality_a));
CHECK((nkr::cpp::Move(equality_b) == nkr::equality_a_t(1)));

Because all of these operators are templates, even for non-constexpr subjects, if constexpr expressions can and should be used to define the algorithms. This allows the compiler to completely optimize away most if not all the function calls that result when equating values of these and other types that have these operators.


Identities

Identities are an integral part of the library because they provide an abstraction over functionality which necessarily needs multiple syntactical entities in order to be uniquely distinguished from other functionalities in the meta-program.

We call these distinct functionalities "identities" because each component entity that makes up an identity is integral to the identification of the primary entity of interest providing the functionality, such as a type, a template, or a template template ad infinitum.

In order to form an identity, we define the primary entity together with its various identity traits and identity tags. We take advantage of the label postfix design and utilize the same base name with different postfixes to strongly signify the relation of these entities to one another.

We'll begin by examining the identity trait and leave the identity tag for afterwards:

class a_t
{
public:
};
// the "type indentity trait" for a_t
template <typename type_p>
concept a_tr =
nkr::cpp::is_any_tr<type_p, a_t>;
// should constrain to any qualification of a_t
static_assert(a_tr<a_t> == true);
static_assert(a_tr<const a_t> == true);
static_assert(a_tr<volatile a_t> == true);
static_assert(a_tr<const volatile a_t> == true);
using alias_of_a_t = a_t;
// and naturally any qualification of an alias of a_t
static_assert(a_tr<alias_of_a_t> == true);
static_assert(a_tr<const alias_of_a_t> == true);
static_assert(a_tr<volatile alias_of_a_t> == true);
static_assert(a_tr<const volatile alias_of_a_t> == true);
class b_t
{
public:
};
// but never any qualification of any other type
static_assert(a_tr<b_t> == false);
static_assert(a_tr<const b_t> == false);
static_assert(a_tr<volatile b_t> == false);
static_assert(a_tr<const volatile b_t> == false);

Here we define the identity traits of a template called "a":

template <typename ...parameters_p>
class a_t
{
public:
using parameters_t = nkr::tuple::types_t<parameters_p...>;
};
// the "instantiated type indentity trait" for a_t
template <typename type_p>
concept a_tr =
nkr::cpp::is_any_tr<type_p, typename type_p::parameters_t::template into_t<a_t>>;
// should constrain to any qualification of an instantiated a_t
static_assert(a_tr<a_t<>> == true);
static_assert(a_tr<const a_t<>> == true);
static_assert(a_tr<volatile a_t<>> == true);
static_assert(a_tr<const volatile a_t<>> == true);
template <typename ...parameters_p>
using alias_of_a_t = a_t<parameters_p...>;
// and naturally any qualification of an instantiated alias of a_t
static_assert(a_tr<alias_of_a_t<>> == true);
static_assert(a_tr<const alias_of_a_t<>> == true);
static_assert(a_tr<volatile alias_of_a_t<>> == true);
static_assert(a_tr<const volatile alias_of_a_t<>> == true);
template <typename ...parameters_p>
class b_t
{
public:
using parameters_t = nkr::tuple::types_t<parameters_p...>;
};
// but never any qualification of any other type
static_assert(a_tr<b_t<>> == false);
static_assert(a_tr<const b_t<>> == false);
static_assert(a_tr<volatile b_t<>> == false);
static_assert(a_tr<const volatile b_t<>> == false);
// the "template indentity trait" for a_t
template <template <typename ...> typename template_p>
concept a_ttr =
nkr::cpp::is_any_ttr<template_p, a_t>;
// should constrain to a_t
static_assert(a_ttr<a_t> == true);
// and any alias of a_t
static_assert(a_ttr<alias_of_a_t> == true);
// but never any other template
static_assert(a_ttr<b_t> == false);

And lastly we define the identity traits of a template template called "a":

template <template <typename ...> typename ...parameters_p>
class a_t
{
public:
using parameters_t = nkr::tuple::templates_t<parameters_p...>;
};
// the "instantiated type indentity trait" for a_t
template <typename type_p>
concept a_tr =
nkr::cpp::is_any_tr<type_p, typename type_p::parameters_t::template into_t<a_t>>;
// should constrain to any qualification of an instantiated a_t
static_assert(a_tr<a_t<>> == true);
static_assert(a_tr<const a_t<>> == true);
static_assert(a_tr<volatile a_t<>> == true);
static_assert(a_tr<const volatile a_t<>> == true);
template <template <typename ...> typename ...parameters_p>
using alias_of_a_t = a_t<parameters_p...>;
// and naturally any qualification of an instantiated alias of a_t
static_assert(a_tr<alias_of_a_t<>> == true);
static_assert(a_tr<const alias_of_a_t<>> == true);
static_assert(a_tr<volatile alias_of_a_t<>> == true);
static_assert(a_tr<const volatile alias_of_a_t<>> == true);
template <template <typename ...> typename ...parameters_p>
class b_t
{
public:
using parameters_t = nkr::tuple::templates_t<parameters_p...>;
};
// but never any qualification of any other type
static_assert(a_tr<b_t<>> == false);
static_assert(a_tr<const b_t<>> == false);
static_assert(a_tr<volatile b_t<>> == false);
static_assert(a_tr<const volatile b_t<>> == false);
// the "template template indentity trait" for a_t
template <template <template <typename ...> typename ...> typename template_template_p>
concept a_tttr =
nkr::cpp::is_any_tttr<template_template_p, a_t>;
// should constrain to a_t
static_assert(a_tttr<a_t> == true);
// and any alias of a_t
static_assert(a_tttr<alias_of_a_t> == true);
// but never any other template
static_assert(a_tttr<b_t> == false);

Interface Specialization Indirection

(W.I.P)

Because we are using C++20 concepts, we have to work around a bug that exists in two of the major compilers. In order to use out-of-body class definitions, we take advantage of a pattern of concept partial specialization which indirectly maps onto separate types. An alias of this indirection is used as the primary type. The actual types that make up the specializations should be put in a non-colliding namespace one step interior to the namespace where the primary type lives. No members in the indirection template may be defined out-of-body.

View a small but detailed example of this bug.


Label Postfixes

There are a number of postfixes on various labels throughout the library. They are helpful in avoiding name collisions, in particular with C++ keywords, but primarily they are used to differentiate between different kinds of entities, such as types, traits, and interfaces.

// "_t" is for "type"
class example_t
{
public:
};
// "_tr" is for "trait"
template <typename type_p>
concept example_tr =
true;
// "_i" is for "interface"
template <typename type_p>
class example_i
{
public:
};

You may have noticed that even the template parameter has a postifx, in particular _p. This allows for the easy definition of an alias with the same base name inside the template, a very frequent occurrence in nkr:

// "_p" is for "parameter"
template <typename parameter_p>
class example_t
{
public:
using parameter_t = parameter_p;
};

Importantly, postfixes can indicate strong relationships between several entities. It is extremely frequent to find these related entities declared nearby each other in the same file. This repetition of the primary name in combination with the repetition of the extremely common postfixes allows for easy recall when working with these entities. For example we may have the following:

using namespace nkr;
// a template type is frequently the primary entity in a relationship
template <typename type_p>
class entity_t
{
public:
};
// this "tag" can represent an instantiation of the primary entity
struct entity_tg {};
// whereas this "template tag" can represent the template itself
template <typename ...>
struct entity_ttg {};
// a "trait" can be used to constrain to an instantiated type
template <typename type_p>
concept entity_tr =
true;
// and a "template trait" to constrain to the template proper
template <template <typename ...> typename template_p>
concept entity_ttr =
true;

You may have noticed that template types share the same postfix as a regular type: _t. This is because the meaning of the postfix remains the same with _t referring to an instantiated type, which is the most frequent occurrence of a template type label:

using namespace nkr;
// "_t" is not for "template"!
template <typename type_p>
class template_t
{
public:
};
// "_t" is for "type", as in "instantiated type".
using instantiated_type_t = template_t<int>;

You may have also noticed the distinction between _tg and _ttg as well as _tr and _ttr. While _tg and _tr may be read as tag and trait and both reference a type, _ttg and _ttr may be read as template tag and template trait, both referencing a template. More formally, they may be read as template of type tag and template of type trait. This pattern extends indefinitely, and may be used to define a template of template of type tag and template of template of type trait:

using namespace nkr;
// "_tttg" for "template of template of type tag"
template <template <template <typename ...> typename ...> typename template_template_p>
struct entity_tttg {};
// "_tttr" for "template of template of type trait"
template <template <template <typename ...> typename ...> typename template_template_p>
concept entity_tttr =
true;

nkr::tuple::templates_t is an example of this indefinite postfix pattern coming into play.

Postfixes even have a use in the naming of files, in particular header files. The most common postfixes come in a set of five, and like the various entities in the library proper, these postfixes are used to coordinate various files that have the same base name and imply a distinct relation to one another. These special postfixes used for file name are in addition to the postfix of the primary entity contained in the files:

// The non-postfix header pulls in everything but the "_dox" below. This is what you would normally use in your code.
#include "nkr/pointer/cpp_t.h"
// "_dec" provides the declarations of entities contained in this header-group. The go-to file to know what is available.
#include "nkr/pointer/cpp_t_dec.h"
// "_dec_def" defines the constexpr and other meta entities of "_dec". A helpful technical distinction from "_def".
#include "nkr/pointer/cpp_t_dec_def.h"
// "_def" defines the non-constexpr and non-meta entities of "_dec".
#include "nkr/pointer/cpp_t_def.h"
// "_dox" provides the doxygen comments used to create the documention for the entities of "_dec".
#include "nkr/pointer/cpp_t_dox.h"

The following is a comprehensive list of postfixes and their meanings as found throughout nkr:

List of Label Postfixes

  • _dec declarations
  • _dec_def declaritive definitions
  • _def definitions
  • _dox documents or docs
  • _e enumeration
  • _i interface
  • _lb label
  • _p parameter
  • _t type
  • _tg tag
  • _tr trait
  • _ttg template tag
  • _ttr template trait
  • _tttg template template tag
  • _tttr template template trait
  • _u union

Move Assignment of Volatile Instances

(W.I.P)

In order to avoid an overload resolution ambiguity, we use a template operator to define the move assignment of volatile types. Because templates have a lower precedence than normal operators, this allows for both volatile and non-volatile instances as well as new constructions of the type to be move-assigned properly, and also allows other types that can be converted through a constructor of the type to be properly assigned as expected:

using namespace nkr;
class example_t
{
public:
example_t& operator =(const example_t& other);
volatile example_t& operator =(const example_t& other) volatile;
example_t& operator =(const volatile example_t& other);
volatile example_t& operator =(const volatile example_t& other) volatile;
example_t& operator =(example_t&& other); // may match any rvalue or temp value of example_t or anything implicitly convertible
volatile example_t& operator =(example_t&& other) volatile; // ""
example_t& operator =(tr<just_volatile_tg, t<example_t>> auto&& other); // is only ever resolved to when given a volatile example_t&& and nothing else
volatile example_t& operator =(tr<just_volatile_tg, t<example_t>> auto&& other) volatile; // ""
};

One Hierarchy

(W.I.P)


One Kind of Template Parameter

A few key points need to be understood before expressing this principle:

  1. Templates can take an large variety of entities as parameters including types, other templates, and literal values.
  2. It is desirable to use templates as parameters in concepts.
  3. Templates can have any number of parameters and so parameter packs must be used in the concept.
  4. Parameter packs require one kind of template parameter and thus different entities cannot be mixed.

With these points in mind, it only makes sense to restrict each individual template to accept only one kind of template argument, whatever that may be. Doing so allows us to statically constrain the use of templates in our functions, types, interfaces, and more. For example, nkr::tr requires that every template used in an expression can only take types and nothing else.


Primary Inner Type

Most every template type available in the library, regardless of how many parameters it has, contains a primary inner type. Usually, it's the first provided argument in the parameter list:

using namespace nkr;
template <typename type_p, typename ...maybe_more_types_p>
class template_t
{
public:
using type_t = type_p; // the primary inner type
};

Primary inner types are usually used for the sake of type constraints, particularly through use of an nkr::tr expression with multiple operands:

using namespace nkr;
// we can use any nkr template as long as it has a primary inner type
template <typename type_p>
using template_t = nkr::pointer::cpp_t<type_p>;
static_assert(tr<
template_t<long>, // we provide a primary inner type of "long"
any_tg, tt<template_t>,
of_any_tg, t<long long> // but we're looking for "long long", so it's false
> == false);
static_assert(tr<
template_t<long long>, // we change it to "long long" to get true
any_tg, tt<template_t>,
of_any_tg, t<long long>
> == true);

It should be noted that each template does not need to have the same alias name for the primary inner type, nor does it need to reuse the parameter name in the name of its alias:

using namespace nkr;
template <typename type_p, typename ...maybe_more_types_p>
class template_t
{
public:
using value_t = type_p; // does not use the parameter's name
};

So in order to know what the primary inner type is for a particular template instantiation, we need to use nkr::interface::type_i:

using namespace nkr;
template <typename type_p>
using template_t = nkr::pointer::cpp_t<type_p>;
// here we give our example template a primary inner type of "int"
using type_t = template_t<int>;
using interface_of_type_t = nkr::interface::type_i<type_t>;
// we can access the primary inner type with an alias contained in the interface
using primary_inner_type_of_type_t = interface_of_type_t::of_t;
// it should be equal to what we gave our template_t
static_assert(nkr::cpp::is_tr<primary_inner_type_of_type_t, int>);
using interface_of_interface_of_type_t = nkr::interface::type_i<interface_of_type_t>;
// coincidentally, the interface itself has a primary inner type equal to type_t
static_assert(nkr::cpp::is_tr<interface_of_interface_of_type_t::of_t, type_t>);

Common alias names for a primary inner type are type_t, value_t, and unit_t.


Primary Inner Value

(W.I.P)


Qualification Support

(W.I.P)

This library provides methods available for non-qualified, const, volatile, and const volatile qualifications of as many types as possible. Exceptions only occur when it doesn't make sense for a particular type to have a certain qualification, or for the aliased C++ types that do not define all qualifications.


Type Documentation

(W.I.P)


Type Sections

(W.I.P)