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:
class constexpr_t;
}
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:
Following the above pattern makes all equality operator overloads templates, and with that point in mind, this pattern avoids two very important conflicts:
- 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.
- 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:
class equality_a_t
{
public:
using value_t = long;
public:
value_t value;
public:
constexpr equality_a_t(value_t value) noexcept :
value(value)
{
}
};
class equality_b_t
{
public:
using value_t = long long;
public:
value_t value;
public:
constexpr equality_b_t(value_t value) noexcept :
value(value)
{
}
constexpr equality_b_t(equality_a_t value) noexcept :
value(value.value)
{
}
};
}
inline constexpr nkr::boolean::cpp_t
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
noexcept
{
return operator ==(a, b);
}
inline constexpr nkr::boolean::cpp_t
noexcept
{
return operator ==(a, b);
}
inline constexpr nkr::boolean::cpp_t
noexcept
{
return operator ==(a, b);
}
inline constexpr nkr::boolean::cpp_t
noexcept
{
return !operator ==(a, b);
}
inline constexpr nkr::boolean::cpp_t
noexcept
{
return !operator ==(a, b);
}
inline constexpr nkr::boolean::cpp_t
noexcept
{
return !operator ==(a, b);
}
inline constexpr nkr::boolean::cpp_t
noexcept
{
return !operator ==(a, b);
}
inline constexpr nkr::boolean::cpp_t
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
noexcept
{
return operator ==(a, b);
}
inline constexpr nkr::boolean::cpp_t
noexcept
{
return operator ==(a, b);
}
inline constexpr nkr::boolean::cpp_t
noexcept
{
return operator ==(a, b);
}
inline constexpr nkr::boolean::cpp_t
noexcept
{
return !operator ==(a, b);
}
inline constexpr nkr::boolean::cpp_t
noexcept
{
return !operator ==(a, b);
}
inline constexpr nkr::boolean::cpp_t
noexcept
{
return !operator ==(a, b);
}
inline constexpr nkr::boolean::cpp_t
noexcept
{
return !operator ==(a, b);
}
Now we can fully equate values of these two types in every imaginable way:
static_assert(nkr::equality_a_t(1) == nkr::equality_a_t(1));
static_assert(nkr::equality_a_t(1) == nkr::equality_b_t(1));
static_assert(nkr::equality_b_t(1) == nkr::equality_b_t(1));
static_assert(nkr::equality_b_t(1) == nkr::equality_a_t(1));
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));
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:
};
template <typename type_p>
concept a_tr =
nkr::cpp::is_any_tr<type_p, 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;
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:
};
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...>;
};
template <typename type_p>
concept a_tr =
nkr::cpp::is_any_tr<type_p, typename type_p::parameters_t::template into_t<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...>;
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...>;
};
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);
template <template <typename ...> typename template_p>
concept a_ttr =
nkr::cpp::is_any_ttr<template_p, a_t>;
static_assert(a_ttr<a_t> == true);
static_assert(a_ttr<alias_of_a_t> == true);
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...>;
};
template <typename type_p>
concept a_tr =
nkr::cpp::is_any_tr<type_p, typename type_p::parameters_t::template into_t<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...>;
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...>;
};
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);
template <template <template <typename ...> typename ...> typename template_template_p>
concept a_tttr =
nkr::cpp::is_any_tttr<template_template_p, a_t>;
static_assert(a_tttr<a_t> == true);
static_assert(a_tttr<alias_of_a_t> == true);
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.
class example_t
{
public:
};
template <typename type_p>
concept example_tr =
true;
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:
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:
template <typename type_p>
class entity_t
{
public:
};
struct entity_tg {};
template <typename ...>
struct entity_ttg {};
template <typename type_p>
concept entity_tr =
true;
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:
template <typename type_p>
class template_t
{
public:
};
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:
template <template <template <typename ...> typename ...> typename template_template_p>
struct entity_tttg {};
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:
#include "nkr/pointer/cpp_t.h"
#include "nkr/pointer/cpp_t_dec.h"
#include "nkr/pointer/cpp_t_dec_def.h"
#include "nkr/pointer/cpp_t_def.h"
#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:
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);
volatile example_t& operator =(example_t&& other) volatile;
example_t& operator =(tr<just_volatile_tg, t<example_t>> auto&& other);
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:
- Templates can take an large variety of entities as parameters including types, other templates, and literal values.
- It is desirable to use templates as parameters in concepts.
- Templates can have any number of parameters and so parameter packs must be used in the concept.
- 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:
template <typename type_p, typename ...maybe_more_types_p>
class template_t
{
public:
using type_t = type_p;
};
Primary inner types are usually used for the sake of type constraints, particularly through use of an nkr::tr expression with multiple operands:
template <typename type_p>
using template_t = nkr::pointer::cpp_t<type_p>;
static_assert(tr<
template_t<long>,
any_tg, tt<template_t>,
of_any_tg, t<long long>
> == false);
static_assert(tr<
template_t<long long>,
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:
template <typename type_p, typename ...maybe_more_types_p>
class template_t
{
public:
using value_t = type_p;
};
So in order to know what the primary inner type is for a particular template instantiation, we need to use nkr::interface::type_i:
template <typename type_p>
using template_t = nkr::pointer::cpp_t<type_p>;
using type_t = template_t<int>;
using interface_of_type_t = nkr::interface::type_i<type_t>;
using primary_inner_type_of_type_t = interface_of_type_t::of_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>;
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)