Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fully generic references #32

Open
mcy opened this issue Aug 1, 2024 · 0 comments
Open

Fully generic references #32

mcy opened this issue Aug 1, 2024 · 0 comments

Comments

@mcy
Copy link
Owner

mcy commented Aug 1, 2024

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 Blah by 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant