A C++20 header-only library to specify constants and value parameters
via template arguments. Depends only on std <concepts>.
Distributed under the Boost Software License, V1.0
Copyright © 2024 The Lemuriad
Permission is hereby granted, free of charge, to any person or organization
obtaining a copy of the software and accompanying documentation covered by
this license (the "Software") to use, reproduce, display, distribute,
execute, and transmit the Software, and to prepare derivative works of the
Software, and to permit third-parties to whom the Software is furnished to
do so, all subject to the following:
The copyright notices in the Software and this entire statement, including
the above license grant, this restriction and the following disclaimer,
must be included in all copies of the Software, in whole or in part, and
all derivative works of the Software, unless such copies or derivative
works are solely in the form of machine-executable object code generated by
a source language processor.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
for compiler requirements see platform notes.
Click to expand contents
- Parameters, parameterize, parametrize
- Rationale: Motivation
- Concepts:
parameta_traits.hpp - Types:
parameta.hpp- Class templates,
- Type meta parameter
type_ - Value meta parameters
dynamic_,static_- Meta value API -
dynamic_,static_ dynamic_deduction guide- Maker functions
makestatic - Metadata access
metasize,metaget
- Meta value API -
- Example: Usage
- Appendices:
- Platform notes
- Array value type TL/DR avoid array value_type
- Function value type
- Parameters:
- Model variables that a modeler chooses to 'freeze'.
- Parameterize:
- Choose a set of variables to act as parameters.
- Parametrize:
- Select values for the chosen set of parameters.
Here, we've drawn a distinction between redundant spellings;
(1) choosing variables to freeze and (2) binding values to them.
Parameters are viewed as constants during analysis and design
'Parameter', used alone, implies a value and values are the focus here.
Classic C++ metaprogramming is based on type parameters
Template parameters in C++ are normally taken to be types.
C++ always had non-type
template parameters - NTTPs.
The awkward negative definition
Let's just call them 'value parameters'. Formerly limited to integer-like constants, including pointers and references to static variables, C++20 admitted floating point value parameters and values of class type - a game changer.
Parameter tuning, as a process that modifies values, appears incompatible with C++ template parameters as immutable constants frozen at compile time. However, in the case of pointers or references to static variables, the value of the referent variable itself may be mutable - only its 'id' is static. This can be exploited to allow a degree of runtime parameter tuning, with no impact on class size. Parameter consumers hold a reference and share read-only access to the static value while only the owner has runtime write access.
Even more flexibly, the template argument can act as an instruction to instance a non-static data member to be dynamic-initialized at runtime. This clearly adds to class size, with redundancy if maintained as a class-invariant 'parameter'. Otherwise, per-object state is useful for dynamic sizes. This is also appropriate for interface types, prototyping, larger objects, JIT transition, and for interaction with dynamic languages.
Flexible model development calls for freedom to deal with parameters dynamically during design, and then switch to statically compiled values to be constant-folded into code for release. Going meta facilitates such 'generic staticity'. The additional 'meta' level of indirection allows more uniform and expressive template APIs with flexibility to tune parameters during development.
Skip straight to Usage examples to see the ideas in action, or continue on through concepts and types.
parameta is a header-only library of types and concepts
to support a generic approach to parameterization;
specifying value parameters in templates
either as static constants or as dynamically-determined values.
Use cases are number systems and multidimensional arrays, with parameters specifying bit-widths, biases, bases, array bounds, layout and indexing.
Compile-time constant parameters are most simply and directly specified as NTTP non-type template parameters.
Dynamically-initialized parameters can only be indirectly specified in a template signature, either by refering to a static variable
can only signal that a runtime value parameter is coming, to be determined at runtime.
that tackles difficulties in the design of templates that
The core idea is to parameterize using meta value types,
i.e. types that represent values,
with API based on std::integral_constant
but representing values of any type
and generalized to include 'dynamic' runtime-determined values,
not necessarily constant.
Three concepts, metavalue, metastatic and metaconst,
capture the hierarchy of increasing static and constexpr constraint
on meta value parameters and their storage duration.
Three maker functions, makevalue, makestatic and makeconst,
help to construct meta values based on
two types, dynamic_ and static_,
that model non-static and static-or-constexpr meta values.
A metatype concept and type_ type
are defined for completeness,
along with a universal metapara concept.
All types can carry arbitrary metadata, to specify usage say.
Meta parameterization also crosses over into 'meta implementation' if the meta parameter types are 'injected' as members into the parameterized class. Then, flexible and generic implementations are achieved without explicit template specializations and with reduced need for meta-programming. Some initial usage patterns are sketched; more experience is needed to evolve idioms of use.
parameta is a header-only C++20 library.
The main header,
parameta.hppmeta types that model the meta parameter concepts
includes
parameta_traits.hppconcepts and traits for meta parameter types
Depends on <type_traits>
This header provides concepts for 'meta parameterization' of template signatures:
$\text{metatype }$ $\text{metavalue} < \text{metastatic} < \text{metaconst }$
The idea is to admit meta parameter types instead of type or value parameters directly:
template <typename T, int N> struct array;
template <metatype t, metavalue<int> n> struct meta_array;Here, t and n are both types but represent a type and a value.
If N is called a 'value parameter' then
n is the corresponding 'meta value parameter'.
The meta value concepts support generic staticity
in that n may be a compile time constant
or simply a 'herald' for a dynamic extent to be determined at runtime.
metatype<Q>concept matches typesQthat represent types
A meta type has the properties of a type trait like type_identity:
- An empty class with a member typedef
type - No call
operator()
(to exclude types likeintegral_constant, see below)
The meta value concepts form a subsumption hierarchy that can also be seen to sort types into nested sets (using set notation directly on concepts for exposition):
-
metavalue$<$ metastatic$<$ metaconst -
metavalue$\supset$ metastatic$\supset$ metaconst
""
flowchart
subgraph metavalue
subgraph metastatic
subgraph metaconst
c(constmeta)
end
end
end
Each more constrained concept, shown more deeply metaconst atop
(constmeta is a metaconst type).
metavalue<Q,VT>concept matches typesQthat represent values
The secondary VT parameter defaults to the return type of Q::operator()()const,
if it exists, otherwise void and the concept is false
(c.f. value type constraint).
A meta value type Q has at least the access API of
integral_constant:
- A
value_typemember typedef (may be a reference type) - A
valuemember variable ofvalue_type(may be static or const qualified) - A no-arg call
operator()()const -> value_typereturningvalue - An implicit conversion
operator value_type()constreturningvalue
Importantly, there's no requirement that
value is a static or a constant expression
(there's also no requirement on a member typedef type definition).
metastatic<Q,VT>concept: ametavaluewith constexpr value or static lvalue idmetaconst<Q,VT>concept: ametavaluewith constexpr value
Q is now required to be an empty class type,
so the value has to be encoded in the type.
As such, value is required to be usable as a template argument:
-
metastaticrequiresvalueinitializes a template<decltype(auto)>
placeholder that accepts any valid NTTP including static lvalues by reference
(not necessarily of constexpr or const value, or of structural value type). -
metaconstrequiresvalue$^\dagger$ initializes a template<auto> placeholder
that accepts only constexpr values of structural value type.
$^\dagger$ for an array, its initial elementvalue[0], recursed down to non-array.
So metaconst guarantees constexpr value of structural value_type
while metastatic may further refer to a static lvalue of any object value_type
A 'structural type' is any type acceptable for a non-type template argument;
a scalar type, a (restricted) literal class type or an lvalue reference type.
Here, we emphasize 'structural value type' to exclude reference types.
Array value type is not classified as a structural type, yet.
Exceptionally, a non-metaconst meta value type can still have
constexpr value
because a value rejected by metaconst for not having
structural type can be accepted as metastatic
if it is a static object
(concepts cannot check for constexpr value;
'can initialize template<auto>' is imprecise).
The 'const' in metaconst means constexpr 'compile-time constant'.
The 'static' in metastatic comes from 'compile-time static',
the union of 'static storage duration' id and 'constexpr value'.
std::integral_constant models metastatic,
and its constexpr subset metaconst.
'
integral_constant' was always a misnomer, moreso since C++20:
- It needn't be integral and its value isn't necessarily constant !
- Any valid non-type template parameter type is allowed
including the reference 'id' of a static-storage variable, const or not.- '
structural_constant_or_static_lvalue_reference' ?
This library provides a static_<v> class template
with a static_t<T,v> explicit-typed alias
that implements the full static API of integral_constant.
A meta value concept can be used to constrain a variable declaration:
metavalue auto chx4 = char{4}; // FAIL; char isn't a metavalue
metavalue auto chr4 = static_t<char,4>{}; // a metavalueand can also constrain to a given value type, say char:
metavalue<char> auto charchar = static_<'0'>{}; // ok; char==char
metavalue<char> auto mismatch = static_<0>{}; // FAIL; char!=intwith the secondary type template parameter of the meta value concept
specified as <char>.
Unfortunately, there's no direct way to constrain
the value type to a concept such as std::integral.
To do that, you have to define a new named concept,
or use ad hoc constraints.
Concepts metavalue metastatic metaconst
accept increasingly constrained meta value types:
-
metavalueis least constrained, so most inclusive. It accepts class wrappers that hold a non-static data member value, as well as accepting metastatic types (that include metaconst types). -
metastaticrequires an empty class so rejects class wrappers. The value must be a valid NTTP. It is more inclusive thanmetaconstbecause it also accepts references to general static objects. -
metaconstis most constrained; it has a guaranteed constexpr value of structural type. It rejects all else. The value is necessarily compile-time initialized, never dynamic initialized.
The concepts segregate types into disjoint sets between each concept boundary.
A metavalue type satisfies exactly one of:
$$ ! \sf{metastatic}\space\sf{ metavalue} \text{ | } ! \sf{metaconst}\space\sf{metastatic} \text{ | } \sf{metaconst } $$
metaconst stands on its own as a compile-time constant value.
Non-metaconst types are collectively called
$$ \underbrace{ \text{metadyna} \text{ | } \text{metadynst} } \text{ | } \sf{metaconst} $$
$$ \underbrace{ \text{metadyn} \text{ | } \sf{metaconst} } $$
$$ \sf{metavalue} $$
Decomposition via metastatic is more useful:
$$ \text{metadyna} \text{ | } \underbrace{ \text{metadynst} \text{ | } \sf{metaconst} } $$
$$ \underbrace{ \text{metadyna} \text{ | } \sf{metastatic} } $$
$$ \sf{metavalue} $$
This isolates the disjoint dynamic_ type.
The
metadyn concept taxonomy
The metavalue types that aren't metaconst are presumed
dynamic-initialized meta values.
These metastatic:
$\text{metadyn} = \text{metadyna} \sqcup \text{metadynst}$
""
flowchart
subgraph metadyn
subgraph metadyna
A(dynamic_)
end
subgraph metadynst
D(dynstmeta)
end
end
metavalue types are the total disjoint union of
metaconst types.
This is shown below in two ways, composing via metastatic.
-
metavalue$=$ $\text{metadyn}$ $\sqcup$ metaconst
""
flowchart
subgraph meta[metavalue]
subgraph metadyn
A(dynamic_)
D(dynstmeta)
end
subgraph metaconst
C(constmeta)
end
end
-
metavalue$=$ $\text{metadyna}$ $\sqcup$ metastatic
""
flowchart
subgraph metavalue
subgraph metadyna
a(dynamic_)
end
subgraph metastatic
direction LR
d(dynstmeta)
c(constmeta)
end
end
metadyn
Importantly, metavalue and metastatic do not require
a 'pure' constexpr value.
metavalue that isn't metaconst; a 'dynamic' meta value:
-
$\text{metadyn}$ $=$ $!$ metaconst metavalue
'Dynamic' here means 'dynamic initialization',
as opposed to constinit constant initialization.
metaconst,
so it is proscriptive as a constraint.
For testing if a known metavalue type is !metaconst.
metastatic:
-
$\text{metadynst}$ $=$ metastatic$\text{metadyn}$ : 'dynamic-static' (or non-structural) meta value -
$\text{metadyna}$ $=$ $!$ metastatic$\text{metadyn}$ : 'dynamic-automatic' meta value
metadynst
-
$\text{metadynst}$ $=$ $!$ metaconst metastatic
$\text{metadynst}$ $\sqcup$ metaconst$=$ metastatic
metastatic that isn't metaconst.
The
metadyna
-
$\text{metadyna}$ $=$ $!$ metastatic metavalue
metavalue that isn't metastatic.
The
Dynamic heralds
Like std::dynamic_extent,
in intent,
Constraining to
Climbing down the ladder of meta value concepts:
- A
metaconsttypecrepresents a pure constexpr value, guaranteed.
e.g.static_<42>, equivalent tointegral_constant<int,42>.
The value is accessed by c::value, or c{}.value, or c{}()
or by implicit conversion, typename c::value_type{c{}}
- A
metastatictype represents either a constexpr value (above)
or refers to a static variable (the next paragraph below).
A metastatic type that is not metaconst is a
It refers to an object of static storage duration with value presumed
to be runtime-determined,
i.e. dynamically initialized during static init.
e.g. static_<(s)>, equivalent to integral_constant<int&,s>
The value of an instance d is accessed at runtime
by d() or by d.value
or by implicit conversion.
Use read-only access to ensure substitutability.
Non-metaconst types are
or non-static, i.e. automatic (the next paragraph below).
A metavalue type that is not metastatic is a
It represents a runtime-determined value to be laid out
in automatic storage
for dynamic initialization during a program run
(or a 'herald' of a runtime value).
E.g. dynamic_<int> (defined below) (there's no std equivalent type).
- A
metavalueis any of the above, no more no less.
It has at least the non-static access API ofintegral_constant
Meta value types are always treated as constants in generic code, with 'dynamic' parameters viewed as runtime-determined constants. Any tuning or mutation is carefully confined to separate code.
APIs should accept by metavalue if possible,
by metastatic if zero-size is needed at runtime,
or by metaconst if a guaranteed constant is absolutely required.
This allows implementations to be as generic as possible
in their level of staticity,
using if constexpr as needed to select more constrained code.
Depends on "parameta_traits.hpp" which depends on <type_traits>
This header provides class templates that model the meta parameter concepts, along with helpers.
-
type_meta type, generalizestype_identity$\[1ex]$ -
dynamic_meta value, with API ofintegral_constant -
static_meta value, generalizesintegral_constant dynamic_deduction guide
-
Explicit type alias :
static_t<T,v>$\rightarrow$ static_<v>
- Maker functions :
makestatic - Metadata access
static member functions
metasize,metaget
type_<T>satisfiesmetatype, with optional metadatatype_<T,x...>
The metadata x... is intended for general specification of how T is to be used.
template <typename Type, decltype(auto)...x>
struct type_
{
using type = Type;
...Equivalent to std::type_identity, with metadata,
this type meta parameter is provided for completeness;
value meta parameters, i.e. non-type meta parameters, are the focus here.
The meta value types form a hierarchy
of increasing static and constexpr constraint.
E.g. given object type T, constexpr int c = 1 and static int s = 1:
dynamic_<T>satisfiesmetavaluestatic_<(s)>satisfiesmetavalue&&metastaticstatic_<c>satisfiesmetavalue&&metastatic&&metaconst
dynamic_ simply wraps some type T.
The dynamic_ type itself doesn't encode a value.
static_ is an empty type that carries a value
via an NTTP in its type's template signature,
either directly as a structural constant, metaconst,
or indirectly as a handle to a static object
of any type and mutability, metastatic value kind.
dynamic_<int>{1}; // metavalue, not metastatic, initialized to 1
static_<(s)>{}; // metastatic like integral_constant<int&,d>{}
static_<1>{}; // metaconst, like integral_constant<int,1>{}Optional metadata x... is also admitted; dynamic_<T,x...>, static_<v,x...>
As required by the metavalue concept,
static_ and dynamic_ have the access API of
integral_constant<T,v>,
but their template signatures split T and v:
dynamic_<T>takes only the type template parameterstatic_<v>takes only the value parameter and deduces its value type asdecltype(v)
dynamic_<typename, x...> :
a class wrapping a value of the given parameter value type:
template <typename ValueType, decltype(auto)...x>
struct dynamic_
{
using value_type = ValueType;
value_type value;
// ... integral_constant access APIstatic_<value, x...> : an empty class carrying a generic NTTP parameter:
template <decltype(auto) Value, decltype(auto)...x>
struct static_
{
using value_type = decltype(Value);
static constexpr value_type value = Value;
// ... integral_constant access APIThe remaining access API in both cases is the same as
std::integral_constant:
template <typename ValueType, ValueType Value>
struct integral_constant
{
using value_type = ValueType;
static constexpr value_type value = Value;
using type = integral_constant;
constexpr operator value_type() const noexcept { return value; }
constexpr value_type operator()() const noexcept { return value; }
};Now integral_constant
can be fully implemented as an alias of static_:
template <typename T, T v> using integral_constant = static_<v>;There's no std type wrapper equivalent to dynamic_,
a wrapped type substitutable in read-only use
with statically constrained meta values.
Constructor Template Argument Deduction
is supported and works for dynamic_ in unsuprising ways:
dynamic_{1} // CTAD -> dynamic_<int> initialized to 1
dynamic_<float>{1} // No CTAD; explicit type given, arg converted
dynamic_{float{1}} // Convert arg type explicitly before CTADUse braced initialization;
dynamic_ is an aggregate so braces ban narrowing conversions.
CTAD is a convenient way to construct a dynamic_
from a single value of obvious type
but is limited and implicit.
Providing an explicit type is often better, or necessary.
Maker functions do more powerful deduction
beyond the capability of CTAD and are more generic.
A CTAD guide disables default decay-copy of array and function values
In C++20, aggregate classes like dynamic_
gain an implicit aggregate guide
corresponding to auto deduction
of the wrapped type.
Unfortunately, this forces decay-copy of the argument value,
so array and function values auto-decay to pointers.
A deduction guide is added to bar the implicit decay and to support C++17 CTAD:
template <typename T> dynamic_(T const&)
-> dynamic_<std::conditional_t<
std::is_function_v<T>, T&, T >>;See also appendices
Array value_type
and Function value_type.
dynamic_, as a meta value, requires access functions
that return by value_type so fails to instantiate
for arrays - a forbidden return type.
- Array values deduce as
dynamic_<T[N]>(which then fail instantiation) - Functions are deduced and initialized by reference, not by pointer.
static_t<T,v> static_<v>
This alias gives an explicit-typed template signature
fully equivalent to integral_constant<T,v>:
template <typename T, T v> using static_t = static_<v>;The explicit specification of T = value_type
then effectively performs a static-cast of v to that type.
On its own, static_<v> deduces v's value category implicitly
by decltype(auto) rules which can conflate value
with value type in subtle and sometimes surprising ways.
Pitfalls of decltype(auto)
Initialization of generic decltype(auto) placeholder.
This topic is hard to summarize.
For background see
value categories
and decltype(auto)
and David Mazières 2021 blog
C++ value categories and decltype demystified.
When used as a template placeholder parameter, decltype(auto)
deduction is also constrained by NTTP validity.
Unlike variables but like function parameters,
template value parameters
with array or function type
are 'adjusted' to pointers after deduction,
effectively resulting in auto deduction
with decay-copy semantics -
counter to the no-decay semantics of decltype(auto).
A decltype(auto) parameter has only one shot at deduction.
Based on its deduced value category for the argument,
it may incorrectly accept or unhelpfully reject potential
other-category matches.
Three cases that call for value category conversion:
using std::cout; // Obviously non-const non-structural global var
//static_<cout> // REJECT ok? value not constexpr or structural
static_<(cout)> // ACCEPT ok? binds a mutable reference
static_<as_const(cout)> // Binds a const reference; safer
using std::numbers::pi; // Obviously constexpr structural global
static_<pi> // by-value, good
static_<(pi)> // by-reference, unintended?
constexpr int a[4]{}; // a[i] is an lvalue of constexpr value
static_<a[0]> // lvalue result so binds a reference, unintended?
static_<auto(a[0])> // by-value via auto(expr) decay-copy, c++23The user has to take control of deduction.
- A static variable id is an rvalue so may be rejected by-value even though acceptable as an lvalue.
- Vice versa, a static lvalue is accepted by-reference even when it is acceptable by-value,
- or accepted as a non-const reference when const was wanted.
Given these pitfalls, why use decltype(auto)?
It's the correct choice for a generic list of values.
It's used as the generic static value parameter,
as well as all metadata parameters.
Can type aliases dodge decltype(auto) difficulties?
Separate types, or type-aliases that forward to a generic type,
sacrifice genericity for explicit specificity,
cannot consider the other category,
add their own edge cases
and, currently, have portability issues.
The solution is to use template 'maker' function overloads...
Function template overloads provide
a sane way to initialize a decltype(auto) parameter, e.g.:
-
makestatic<X>()$\rightarrow$ static_<X>() -
makestatic<X>()$\rightarrow$ static_t<typeof(X)const&, X>()
(1) is selected ifXis valid by-value, otherwise (2) by-reference, else fail.
The overloads free users from any need to consider argument value category conversions.
makestatic<X>() vs static_<X>() ?
or static_<(X)>() ?
or static_<auto(X)>() ?
or static_t<typeof(X),X>() ?
or static_<std::as_const(X)>() ?
or static_t<typeof(X)const&, X>() ?decltype(auto) deduction rules are sensitive to value category
so an initializer may have to be converted prior to deduction;
an id expression is converted to lvalue with parens (id),
an lvalue is const-qualified by static_t or as_const(lval),
or an lvalue is converted to rvalue by a static_t
or auto{lval} in C++23.
Examples : makestatic vs static_
Just plug in the argument directly - no need for conversions; Yay.
// Simple case: constexpr value of structural value type:
static_<42>();
makestatic<42>(); // Yay; identical
constexpr int constv = 42; // constexpr variable
static_<constv>(); // -> static_<42>()
makestatic<constv>(); // Yay; same
// Non-constexpr variable (irrelevant if structural or not)
int staticm; // static mutable variable; non-constexpr
//static_<staticm>(); // FAIL: not constexpr
static_<(staticm)>(); // lvalue conversion, non-const : int&
static_<as_const(staticm)>(); // const-lvalue conversion
makestatic<staticm>(); // Yay; deduces const-lvalue : int const&
// Array arguments can decay to pointer
constexpr int arrayc[]{21,42}; // constexpr array, structural elem
static_<arrayc>(); // Oops: decay-to-pointer : int const*
static_<(arrayc)>(); // lvalue conversion requires parens
makestatic<arrayc>(); // Yay; no decay, deduces : int const(&)[2]
// lvalue arguments don't auto-convert to rvalue when it's possible
// (i.e. when constexpr value of structural value type)
static_<arrayc[1]>(); // Oops: binds an lvalue : int const&
static_<auto{arrayc[1]}>(); // rvalue conversion (C++23)
makestatic<arrayc[1]>(); // Yay; static_<42>{} value_type : int
int arraym[2] = {}; // static array, mutable; non-constexpr
static_<arraym>(); // Oops: decay-to-non-const-pointer : int*
static_<(arraym)>(); // Convert to non-const lvalue : int(&)[2]
static_<as_const(arraym)>(); // Convert to const-lvalue
makestatic<arraym>(); // Yay; no decay - deduces : int const(&)[2]
void funct(){} // function definition
static_<funct>(); // Oops, decayed to pointer -> void(*)()
makestatic<funct>(); // Yay; no decay : reference -> void(&)()Simply put; pass by-value beats pass by-reference, and there's no decay:
- If argument
Xcan be accepted by value$^\dagger$ then it is accepted by value - else if
Xis a static lvalue then it's accepted by const reference.
$^\dagger$ Arrays and functions pass by-reference, not via decay-copy.
A by-reference result is always const-qualified for its intended usage as a read-only parameter.
Function overload pair for generic parameter value category deduction.
Two function overloads consider both
by-value <auto> and by-reference <auto const&> potential matches
independently and then choose the most appropriate by overload resolution.
Contrast with 'one-shot' <decltype(auto)> deduction that conflates
value category with value
so may inappropriately accept by-reference,
or unhelpfully reject by-value, potential matches
via the other category.
E.g. makestatic overloads; auto by-value and auto const& by-reference:
template <auto val> // by-value overload
constexpr static_<val> makestatic(); // (want worse-match)
template <auto const& ref> // by-reference overload
constexpr static_<ref> makestatic() // (want best-match)
requires( // enable if:
! requires{makestatic<ref>;} // value overload fails
|| is_function_v<typeof(ref)> // or if function type
|| is_array_v<typeof(ref)> // or if array type
);Only function overloads can do this tailored deduction of value category by considering each potential parameter match independently.
The 'val' overload accepts via auto
while the 'ref' overload accepts via auto const&
and is enabled if the 'val' overload fails,
or if the referenced type is an array or function type...
...in which case ... for static arrays and functions ...
the overloads are ambiguous because
the 'val' overload accepts them by decay-copy.
If the ambiguity is resolved in favour of the 'ref' overload
then decay is avoided, with arrays and functions binding by reference.
To prioritize the 'ref' overload,
without adding a user-provided argument,
a variadic deduced type pack is added to the 'val' overload,
making it a worse match
(on MSVC this technique currently only works
exactly for the array-or-function edge case for which it is needed here):
template <auto val, typename...W> // by-value overload
constexpr static_<val> makestatic(W...); // Worse-matchThe effect is that the first overload accepts any
constexpr value of structural type,
even lvalues that would reference-bind to decltype(auto),
while the second accepts the rejects
(static objects of non-constexpr value or of non-structural value type)
as well as static arrays and functions.
To get the type, macros can work:
#define METASTATIC(X) decltype(makestatic<X>())
template <metaconst c> c checkonst(c);
#define MAKECONST(X) decltype(checkonst(makestatic<X>()))Downsides of 'doubled up' deduction.
The 'doubled up' deduction can't be aliased; only macros can 'forward' an argument to the overload set.
The 'doubled up' deduction doubles the number of overloads required for any additional parameters and complicates overload resolution. The technique is practially limited to small numbers of parameters.
Users should write maker functions specific to the constraints of their use cases.
Due to the impossibility of 'forwarding' double-deduction, this library can't abstract away the overload mechanics with generic helpers (without resorting to macros for code generation). The library-provided maker functions are 'templates' to follow. The library concepts and documentation are intended to help. More examples from usage experience will be added in time.
metasize(), metaget()
Common accessors for metadata x... in any meta parameter type
Q static_, dynamic_ or type_,
implemented as static member functions:
-
Q::metasize()$\rightarrow$ sizeof...(x) -
Q::metaget()$\rightarrow$ static_<x...>
Q::metaget<I...>()$\rightarrow$ static_<xI...>
where xI is the Ith x... value.
If there's no metadata metaget emits a static_assert message.
Note that single-index metaget<I>() returns static_<xI>,
wrapped, same as for multi-indices.
In principle, there's no need for in-class 'intrusive' access functions, but a minimal static API is convenient (and currently neccessary for Clang support).
The metadata API is controlled by preprocessor expansion;
it can be disabled or switched without editing "parameta.hpp".
Consider a generic array type, ray, in which the array Extent
is parameterized by a meta value type, required to be of integral type,
and its data Storage by a type parameter,
constrained to have array-like access via the subscript operator[]:
template <typename Storage, metavalue Extent>
requires (integral<typename Extent::value_type>
&& requires (Storage a) {a[0];})
struct ray {
[[no_unique_address]] Storage data;
[[no_unique_address]] Extent extent;
};The
[[no_unique_address]]
annotation ensures that
empty member types occupy no storage.
Parametrize with a C array member and a metaconst extent:
template <typename T, int N> using array = ray<T[N], CONST(N)>;
array<int,2> i2 {{4,2}}; // ray<int[2],metastatic<2>>
array<char,4> c4 {"str"}; // ray<char[4],metastatic<4>>
static_assert( sizeof c4 == 4 && c4.extent == 4 );Like std::array, storage is in-class 'intrinsic',
and there's no size overhead for holding
the extent as a trailing data member
as it's an empty class type.
Parametrize as a 'dynamic span' type
with 'extrinsic' data of dynamic size
with data Storage type P that
should be constrained to satisfy pointer traits.
template <typename P> using span = ray<P,dynamic_<int>>;
char buffer[4];
span<char*> ps{buffer,{4}};This is a 'pointer and size' aggregate,
in which the size is accessed as a metavalue member extent.
The buffer ownership can be 'injected'
by parametrizing with a unique_ptr:
span<unique_ptr<char[]>> up{make_unique<char[]>(4),{4}};Parametrizing Extent as metaconst saves size,
and parametrizing Storage as a metastatic reference
to a static buffer shrinks the class size to the minimum:
ray<char(&)[4], static_<4>> sp{buffer};
ray<static_<buffer>, static_<4>> sb{};Here's a summary of the layouts and resulting sizes:
static_assert( sizeof ps == 16 ); // pointer 8 + size 4 (4 byte pad)
static_assert( sizeof up == 16 ); // unique_ptr 8 + int (4 byte pad)
static_assert( sizeof sp == 8 ); // pointer 8 + static size 0
static_assert( sizeof c4 == 4 ); // array char[4] + static size 0
static_assert( sizeof sb == 1 ); // static ref 0 + static size 0ToDo: Discuss API design beyond basic layout parameterization.
Compare with std::mdspan API.
Add a number system representation example.
Requires a recent compiler
g++10 -std=c++20and up
g++9 -std=c++2a -fconcepts
g++9 -std=c++17and upclang++12 -std=c++20and up
clang++12 -std=c++17and upcl /std:c++20v19.30 up
cl /std:c++17v19.26 up
The library is experimental
but intended to be production ready.
It builds on C++17 NTTP placeholder parameters,
generic decltype(auto) in particular.
The C++17 support is natural and allows API compatibility,
without C++20 extended NTTPs and concepts.
std library <concepts> are not used.
Some c++23 features are used, conditionally.
This will cause warnings when compiled c++20 mode.
To supress the warnings on gcc and clang use -Wno-c++2b-extensions.
MSVC v19.34 has issues with non-type template parameter
value category deduction.
The makestatic function decays constexpr functions.
Macros can be used to help keep code portable,
see the #define's in the library headers.
Clang 15 does not yet implement some C++20 updates to NTTPs; it doens't yet admit floating point values, subobject lvalues, or direct braced initialization syntax (e.g. for aggregate CTAD).
Clang bug
template decltype(auto) substitution failure
necessitates an intrusive in-class API for metadata access,
i.e. metasize and metaget had to be made
static member functions for clang
(hidden friends also work).
For this reason the access API is made removable or configurable.
Array and function value types are incompatible as meta value types.
Static arrays and functions are best handled by reference,
to retain value-like semantics.
'Doubled up' deduction of
'maker' function overloads helps
by protecting against decay
(template <decltype(auto)> placeholder parameters
actually perform <auto> decay-copy).
Array decay loses extent information,
changes value category from lvalue to rvalue
and breaks the metaconst concept
(the pointer value is checked rather than the pointed-to value).
Array classes like std::array
with proper value semantics are generally a better choice.
Array value types should generally be avoided...
...at least until C++ makes array a regular type.
Skip this section unless you're an irregular type.
Array value type is forbidden as a function return type.
The metavalue access functions are required
to return by value type.
Therefore, meta value types cannot be instantiated with array value type:
using char2 = dynamic_<char[2]>; // OK to alias, uninstantiated
char2 X; // FAIL instantiation
dynamic_{"X"} // FAIL instantiation of deduced dynamic_<char[2]>(The dynamic_ deduction guide could be modified
to accept array by-reference, or by-pointer with decay,
but that hack would preclude by-value array working in
some future C++ with a language fix.)
Note that dynamic_ has no explicit constraint on its value_type
so the class type can 'carry' an array value type, dynamic_<T[N]>,
as long as it's never instantiated itself.
On the other hand, static_ shouldn't accept array value arguments at all.
Array types are not classed as structural types so a decltype(auto)-
deduced array value type should cause an instantiation failure.
Here, though, it appears to work:
static constexpr char dk[] = "decay";
decltype(auto) nodk = dk; // Reject; deduced as const char[6]
// then error: array initializer
static_<dk> // Accept !! (via ADJUSTMENT and DECAY-COPY)
// as const char* value_type, not char[6]What happens here is an unfortunate series of events;
you are advised to look away.
The semantics of decltype(auto) is silently reversed to mean auto.
static_<dk> deduces array value type const char[6]
via its template decltype(auto) placeholder parameter.
Next, the array type is silently 'adjusted' to pointer type const char*
(see below).
This then forces decay on the initializing array argument value.
The pointer value is then accepted because it's a static object id.
This ancient C rule, 'adjustment' of formal function parameters of array type to pointer, is seen as C's Biggest Mistake. Here it is perpetuated in an entirely modern C++ context of generic template parameters.
All direct attempts to use a string-literal fail (sadly, as a much requested use case):
static_<"X"> // FAIL; string literal NTTP reference forbidden
static_<&"X"> // FAIL; string literal NTTP pointer forbidden
using chars = char[];
static_<chars{"X"}> // FAIL; not a static object lvalueIn short...
- Meta value types of C array value type can't be instantiated.
- Static meta value types can't be formed with C array value type.
...this generic library strives to support array, even if an awkward type.
Functions have non-value type, so are totally no-go by-value.
(Arrays have value type, albeit irregular non-copyable.)
Despite this, functions are fairly easy to work with.
Functions kind of work...
Using std::puts from <cstdio> as an example:
dynamic_<int(&)(const char*)>{puts}("Hi"); // Explicit reference
dynamic_{puts}; // The same; function-specific CTAD -> reference
dynamic_{&puts}("Bye"); // & takes address for CTAD -> pointer
static_<(puts)>{}("Hello, world!"); // Static function reference
static_<&puts>{}("Goodbye."); // & -> static function pointer
static_<puts>{}("Goodbye, null world?"); // Silent decay to ptr(Note that it's undefined behavior to refer to
functions defined in the std library.)
The captured function can be called directly
because the implicit conversion operator
returns the function reference or pointer.
However, for functions taking no parameters
the empty parens will invoke the call operator() instead
so generic code shouldn't rely on implicit conversion.
As explained in the previous section on array value type,
the last line above should fail but instead silently decays the
argument to a pointer, so defeating the purpose of decltype(auto).
Function pointer decay is not as problematic as array decay, as no information is lost, but retaining a reference is best as pointer nullability necessitates null-checks before use.
Function objects are usually more appropriate; consider them as well, or instead.