This is an attempt to design "fully generic references", i.e., a way to manipulate a reference to any C++ type, including void and references.
The Problem
In vanilla C++, all types are in one of four categories:
Objects: int, MyClass, int*, int[4], int(*)(), etc.
References: int&, int&&.
Functions: int(), void(int) const&.
void.
Of these, objects and void can be cv-qualified. Note that the const in int() const is not cv-qualification: adding const to a function type leaves it unchanged. But, functions are always constants, and so always immutable, so we can treat all functions as always being const.
Objects and tame functions[1] can be the pointees of references. Only objects that are not an unbounded array type can be the element type of an array. Further, the built-in array types, int[4] and int[] are weird because they cannot appear as parameters and arguments to functions (although references to them can).
[1]: A tame function is one that does not have this qualification. int() is tame, but int() const is "abominable".
Type Constructors We Care About
In generic code, we want to require best::ref<T> instead of T, which is T& exactly when that is a valid type. However, best::ref<T&> already presents problems: we cannot write down a constructor for it because we can only take references as function arguments "by value". This necessitates that we add our own custom types for every "storage type constructor":
best::value<T>, a T held by-value. This type can be dereferenced to get any of the pointer types below. (This is best::object<T> today).
best::box<T>, which is like best::value<T> but the value is on the heap.
best::array<T, n> coincides with either T[n] or T[] when they are well-formed.
best::{ref, rref}<T>. These are the four kinds of references (notably, we are going to pretend volatile does not exist).
best::raw_ptr<T>, a T held by raw C++ pointer. This coincides with T* when that is a valid type. Notably, void* is valid, but void& is not.
best::ptr<T>, which already exists. This is an enhanced pointer type.
best::as_const<T>, best::as_mut<T>. This produces the const (resp non-const) version of a type.
There are also concepts for identifying pointers, references, and arrays.
These define the following operations:
best::ptr<T> defines the canonical constructors for T, via the construct() and assign() member functions.
best::value<T> (and best::box<T>) construct themselves via best::ptr<T>.
best::ptr<T> derefs to best::ref<T> in the expected way.
best::addr() and converts best::ref<T> into a `best::ptr.
There's a few small problems with this formulation. First, we need to invent opaque class types to fill in for types like best::ref<T&>. In particular, we need to invent opaque types for the following:
best::ref<T&> (and other ref-ref combinations).
best::ref<void>
best::ref<int() const>
best::raw_ptr<T&>
best::raw_ptr<int() const>
best::as_const<T&>
best::array<T&, n>
best::array<void, n>
best::array<int(), n> (All functions, including tame ones.)
best::array<T, 0> for all types T unless T[0] is well-formed.
Note that we do not need to define best::as_const<int()> because we have declared all function types as intrinsically const. Also, the array types suck, so we should discourage their use in generic code. Thus, best::array<T&, n> and other invented array types will be non-constructable and non-destructable. Instead, truly general arrays must go through best::value. The same goes for best::as_const<T&>.
Also, type classification traits need to not identify these as class types! Instead, they must respect.
Fat Pointer Types
However, this does not cover everything. First, T(&)[] is a useless type, we want to use best::span instead. Also, we want to have a mechanism for attaching metadata to pointers, a la &[T] and &dyn Tr in Rust. So, a best::box<best::dyn<Blah>> would want to be a void* plus a vtable, and derefing it needs to produce a Blahby value.
This suggests that we need to define an additional best::view<T> type, which may be the same as best::ref<T> but will be something else for so-called "fat pointer" types like arrays and dyns.
We call a type "thin" if best::ptr<T> has no metadata (formally T is NOT thin, if T is an array type or it is a class type satisfying requires { typename T::BestPtrMetadata; }. Otherwise, T is fat. Today, every type specifies a "metadata class":
struct my_meta {
using pointee; // `best::raw_ptr<T>` is `pointee*`
using metadata; // The public metadata type exposed to users. `my_meta` converts to/from it.
best::layout layout() const; // Returns the true layout of a particular value, given its pointer meta.
auto deref(pointee*) const; // Dereferences the pointer to obtain its corresponding view type.
// Other requires functions, see `best::is_ptr_metadata` in ptr.h.
}
deref() must return either a pointer U* or a class type V. This defines the view type for T: if deref() returns a pointer, the view type is U&; otherwise, it is V (this is `best::view). The following are the view types for all types T:
If T is an "ordinary" object type, T&.
If T is a reference best::ref<T> or best::rref<T>, the view type is T's view type.
If T is a function type T, the view type is best::tame<T>&.
If T is an array type, the view type is best::span<T> or best::span<T, n>
If T is void, the view type is void.
Note that view types are always "lvalue flavored". There is no such thing as an rvalue view. Also, ptr -> view is a lossy operation. Given a view type, it is not possible to obtain a unique pointer type for it.
Then, what makes sense to do is as follows:
best::ref<T> is either a reference when T is thin, or a wrapper over best::ptr<T>. This is the "uniform reference type". This means references always carry metadata.
best::raw_ptr<T> is always best::ptr<T>::pointee*. This value is returned by best::ptr::as_raw().
best::ptr<T>'s operator* and operator-> return the view type, NOT the reference type. To obtain a reference, use best::ptr::as_ref().
best::as_view() converts any type into its corresponding view.
What's nice about this is that things like best::ptr<T[]> p; p->size(); Just Work!
Example Generic Code
// Write to a reference reference.
template <typename T>
void store_ref(best::ref<T&> dst, T& src) {
dst.assign(src);
}
// Load a generic reference to reference, which may itself point to a reference.
// Note that function template deduction won't work here, and it must be called
// as `load_refref<MyType>(my_ref)`
template <typename T>
best::ref<T> load_refref(best::ref<best::ref<T>> r) {
return r; // Implicit conversion, just like T& -> T.
}
Corner cases
Some ordinary C++ reference types exist that we are going to pretend did not exist:
int(&)[], basically a crappy pointer.
best::ref<int>&, and other references of best::ref and friends.
For this, we'll introduce best::is_valid<T>, which rejects all types like the above. All generic Best types will include a call to this concept to stop types like best::option<best::ref<int>&> existing and causing shenanigans.
This is an attempt to design "fully generic references", i.e., a way to manipulate a reference to any C++ type, including void and references.
The Problem
In vanilla C++, all types are in one of four categories:
int
,MyClass
,int*
,int[4]
,int(*)()
, etc.int&
,int&&
.int()
,void(int) const&
.void
.Of these, objects and
void
can be cv-qualified. Note that theconst
inint() const
is not cv-qualification: addingconst
to a function type leaves it unchanged. But, functions are always constants, and so always immutable, so we can treat all functions as always beingconst
.Objects and tame functions[1] can be the pointees of references. Only objects that are not an unbounded array type can be the element type of an array. Further, the built-in array types,
int[4]
andint[]
are weird because they cannot appear as parameters and arguments to functions (although references to them can).[1]: A tame function is one that does not have
this
qualification.int()
is tame, butint() const
is "abominable".Type Constructors We Care About
In generic code, we want to require
best::ref<T>
instead ofT
, which isT&
exactly when that is a valid type. However,best::ref<T&>
already presents problems: we cannot write down a constructor for it because we can only take references as function arguments "by value". This necessitates that we add our own custom types for every "storage type constructor":best::value<T>
, aT
held by-value. This type can be dereferenced to get any of the pointer types below. (This isbest::object<T>
today).best::box<T>
, which is likebest::value<T>
but the value is on the heap.best::array<T, n>
coincides with eitherT[n]
orT[]
when they are well-formed.best::{ref, rref}<T>
. These are the four kinds of references (notably, we are going to pretendvolatile
does not exist).best::raw_ptr<T>
, aT
held by raw C++ pointer. This coincides withT*
when that is a valid type. Notably,void*
is valid, butvoid&
is not.best::ptr<T>
, which already exists. This is an enhanced pointer type.best::as_const<T>
,best::as_mut<T>
. This produces theconst
(resp non-const
) version of a type.There are also concepts for identifying pointers, references, and arrays.
These define the following operations:
best::ptr<T>
defines the canonical constructors forT
, via theconstruct()
andassign()
member functions.best::value<T>
(andbest::box<T>
) construct themselves viabest::ptr<T>
.best::ptr<T>
derefs tobest::ref<T>
in the expected way.best::addr()
and convertsbest::ref<T>
into a `best::ptrThere's a few small problems with this formulation. First, we need to invent opaque class types to fill in for types like
best::ref<T&>
. In particular, we need to invent opaque types for the following:best::ref<T&>
(and other ref-ref combinations).best::ref<void>
best::ref<int() const>
best::raw_ptr<T&>
best::raw_ptr<int() const>
best::as_const<T&>
best::array<T&, n>
best::array<void, n>
best::array<int(), n>
(All functions, including tame ones.)best::array<T, 0>
for all typesT
unlessT[0]
is well-formed.Note that we do not need to define
best::as_const<int()>
because we have declared all function types as intrinsicallyconst
. Also, the array types suck, so we should discourage their use in generic code. Thus,best::array<T&, n>
and other invented array types will be non-constructable and non-destructable. Instead, truly general arrays must go throughbest::value
. The same goes forbest::as_const<T&>
.Also, type classification traits need to not identify these as class types! Instead, they must respect.
Fat Pointer Types
However, this does not cover everything. First,
T(&)[]
is a useless type, we want to usebest::span
instead. Also, we want to have a mechanism for attaching metadata to pointers, a la&[T]
and&dyn Tr
in Rust. So, abest::box<best::dyn<Blah>>
would want to be avoid*
plus a vtable, and derefing it needs to produce aBlah
by value.This suggests that we need to define an additional
best::view<T>
type, which may be the same asbest::ref<T>
but will be something else for so-called "fat pointer" types like arrays and dyns.We call a type "thin" if
best::ptr<T>
has no metadata (formallyT
is NOT thin, ifT
is an array type or it is a class type satisfyingrequires { typename T::BestPtrMetadata; }
. Otherwise,T
is fat. Today, every type specifies a "metadata class":deref()
must return either a pointerU*
or a class typeV
. This defines the view type forT
: ifderef()
returns a pointer, the view type isU&
; otherwise, it isV
(this is `best::viewT
is an "ordinary" object type,T&
.T
is a referencebest::ref<T>
orbest::rref<T>
, the view type isT
's view type.T
is a function typeT
, the view type isbest::tame<T>&
.T
is an array type, the view type isbest::span<T>
orbest::span<T, n>
T
is void, the view type isvoid
.Note that view types are always "lvalue flavored". There is no such thing as an rvalue view. Also,
ptr -> view
is a lossy operation. Given a view type, it is not possible to obtain a unique pointer type for it.Then, what makes sense to do is as follows:
best::ref<T>
is either a reference whenT
is thin, or a wrapper overbest::ptr<T>
. This is the "uniform reference type". This means references always carry metadata.best::raw_ptr<T>
is alwaysbest::ptr<T>::pointee*
. This value is returned bybest::ptr::as_raw()
.best::ptr<T>
'soperator*
andoperator->
return the view type, NOT the reference type. To obtain a reference, usebest::ptr::as_ref()
.best::as_view()
converts any type into its corresponding view.What's nice about this is that things like
best::ptr<T[]> p; p->size();
Just Work!Example Generic Code
Corner cases
Some ordinary C++ reference types exist that we are going to pretend did not exist:
int(&)[]
, basically a crappy pointer.best::ref<int>&
, and other references ofbest::ref
and friends.For this, we'll introduce
best::is_valid<T>
, which rejects all types like the above. All generic Best types will include a call to this concept to stop types likebest::option<best::ref<int>&>
existing and causing shenanigans.