Giter VIP home page Giter VIP logo

chromium / subspace Goto Github PK

View Code? Open in Web Editor NEW
81.0 7.0 16.0 6.72 MB

A concept-centered standard library for C++20, enabling safer and more reliable products and a more modern feel for C++ code.; Also home of Subdoc the code-documentation generator.

Home Page: https://suslib.cc

License: Apache License 2.0

C 0.84% C++ 84.58% CMake 0.48% CSS 0.62% JavaScript 0.06% Python 0.01% HTML 13.29% Batchfile 0.03% Shell 0.09%
cxx cxx20 standard-library functional-programming safety

subspace's Introduction

CI docs

Subspace Library

An experimental take on a safer, simpler C++ standard library.

Please don't use this library. This is an experiment and we don't yet know where it will take us. There will be breaking changes without warning, and there is no stable version.

  1. See BUILD.md for instructions on building Subspace and Subdoc and running their tests.
  2. See USAGE.md for instructions on integrating the Subspace library into your project.
  3. See PRINCIPLES.md for the principles behind design choices in the Subspace library.
  4. See STYLE.md for evolving guidance and best practices for developers of the Subspace library.

Subdoc

Subdoc is a documentation generator from inline C++ documentation in the spirit of Rustdoc.

The comments in the Subspace library use markdown syntax with additional Subdoc attributes, and are designed to be consumed by Subdoc in order to generate an HTML website.

Subdoc is built on top of Subspace, giving the developers a chance to see the Subspace library in action and test the ergonomics and features of the library.

Status: Subdoc is still very much a work-in-progress, but it is being used to generate Subspace documentation on each commit.

Compiler Support

Subspace is a concept-first library and requires C++20. Compiler support for C++20 varies, and with active development ongoing, bugs in their implementations still appear and disappear regularly. When in doubt, check out which compiler versions are used by the CI bots.

Compiler Version
Clang: 16 and up
GCC: 13 and up
MSVC: VS2022 17.8.1 (Build 17.8.34316.72) and up

We attempt to work around bugs when reasonable, to widen compiler version support. See compiler_bugs.h for the set of bugs we are aware of and currently work around.

subspace's People

Contributors

danakj avatar dj2 avatar veluca93 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

subspace's Issues

Figure out the constructor story

Rough thoughts atm:

There are four types of construction

1 Default - replaces std::is_default_constructible. We have MakeDefault now, but we should drop the with_default() stuff, it duplicates existing language stuff. And a default constructor doesn't need to overload other (non-copy/move) constructors if we don't use those.

?? DefaultTrivial ?? - probably

2 Copy/Move is done, we expose a simpler concept than type traits. Have to use copy/move constructors unfortunately that's just how the language works today.

3 With context/data. Not a conversion! Static method!!

4 Conversion. Use From/from() static method

Ban is_convertible/is_constructible. It’s ambiguous. We currently use it to deal with operator T() conversions, which are no better than conversion constructors.

  • where we use it, we should be using Into??
  • we use it for conversion OPERATOR from IntoRef
  • how to avoid it
  • this could allow removing operator Ts on u8 etc? Should be saying into(). But need to provide some Into method on u8 etc and on IntoRef instead of the operators.

Can Choice construction be constexpr?

When the value being constructed in the choice is trivially-default-constructible, its lifetime can begin by being assigned to (11.5.6.1 of C++20 spec).

So we can provide a constexpr path in this case for Choice<...>::with<X>(y)?

Then we can also do the same for Choice<...>::set<X>(y)?

Consider rename Union to Choice

union being a keyword is awkward, so we have a union_type namespace. Carbon uses Choice for their tagged union, probably for similar reasons. Consider matching that?

Configurable behaviour of operator+= etc for integers

Currently the behaviour of operator+= is to check() on overflow.

We could provide a compile-time choice to wrap on overflow instead, which would be cheaper perf-wise (no introduction of branches). This is likely to be required for use in place of primitives in a large project. OTOH then you don't eliminate integer overflow bugs, so may not actually be warranted, though maybe you want different behaviour in debug vs release.

Provide instructions for using Subspace in a CMake-based project

We have some instructions for checking out and building Subspace, but no integration instructions.

Presumably this is easy for CMake pros, but some steps would be good, that include disabling CIR/Subdoc. And that include enabling the clang-tidy! Once it's merged.

Possibly we should consider vcpkg or something?

@dj2 From your past submissions it seems like you were optimizing this path, do you have any opinions or help you could lend?

Remove custom clang-format

The Clang 16 release will format Subspace correctly now, modulo bugs (of which there are a bunch but the custom bin won't do any better really).

So we can remove that extra infra and documentation.

Debug/Display for numeric types

We need to be able to print numerics without casting them to primitives every time.

Some basic options exist:

  1. Just for Gtest, add a PrintTo method for them, in the subspace/test/ code.
  2. Add operator<< for them. This means STREAMS. This is bad for compile time.
  3. Add a ToString() method, and implement PrintTo on top of it.

Modeling Debug/Display traits as concepts instead we can do it differently, but something like (2) or (3) above.

This version will look up write_debug() through ADL:

template <class T>
concept sus::fmt::Debug = requires (const T& t) {
  write_debug(t, sus::fmt::Formatter&);
};

It can be implemented as a friend function:

namespace num {
struct i32 {
    friend void write_debug(i32, fmt::Formatter&);
};

Or a free function:

template <std::same_as<i32> T>
void write_debug(T, fmt::Formatter&);

This version will look for the method on the class. It can't be added types you don't control.

template <class T>
concept Debug = requires(const T& t, fmt::Formatter& fmt) { 
    { t.write_debug(fmt) } -> std::same_as<void>; };

And the method has to clutter the type's public API, even though it should not be called directly.

I think we should go with the first option.

Then we have two more choices:

  1. Add PrintTo() for GTest based on write_debug.
  2. Implement write_display() for these types as well, and provide operator<< in some non-default header for them, which tests include.
  3. Do both, as tests would use the write_debug in place of write_display.

As there's much to be done for how the Formatter would work, the Formatter should not be stabilized along with numeric types, but the use of them through PrintTo() or << depending on the above, is fine.

Make all Iterator types in the library Clone

  • If they are over references, like iter(), or slice iterators.
  • Cloning mutable references is fraught. Do we... support that?
  • If they are over values, like into_iter() if the values are Clone.

AsRef<T> and AsSlice<T>

AsRef being singular and AsSlice being plural.
Need Mut siblings as well.

Many things can produce a ref/slice without being a ref/slice. Vec is a Slice, has Slice methods. But is a string? Is an unordered map?

But what if you want to receive anything that can give a ref/slice?

Rust has AsRef, but omits AsSlice though many types have .as_slice() which means the caller must do the production.

We could have fn(AsSlice const auto& s) { s.as_slice()…. }

https://youtu.be/Tz5drzXREW0 talks about this desire to write generic code over slice-able things.

Conversion from pointer to integer

Right now you have to reinterpret_cast to a primitive value then construct a usize. Yuck.

Instead template <class T> usize::from(T*)? But usize has size equal to size_t. Should we now introduce uptr before things get out of hand with usize holding pointers?

I think it's clear from reading the Rusty stuff below there's regret they didn't do this sooner, they would use uptr as a name, and they would have it be the size of a pointer, not of ptraddr_t which is too theoretical to need to support in the stdlib.

TODO:

  • Update docs on isize/usize to say they are address-sized instead of pointer-sized. This is a no-brainer for now.
  • Add a uptr type. (No need for iptr.) #284
    • Differentiate it from u* as laid out below.

Rust project thoughts

Here are Rusty thoughts on it: rust-lang/rust#95228
Highlights:

  • A secondary goal of this project is to try to disambiguate the many meanings of ptr as usize, in the hopes that it might make it plausible/tolerable to allow usize to be redefined to be an address-sized integer instead of a pointer-sized integer.
  • This would... effectively redefine usize from intptr_t to size_t/ptrdiff_t/ptraddr_t (it would still generally conflate those concepts, absent a motivation to do otherwise).
  • size_t != uintptr_t != ptraddr_t Zulip thread
    • On CHERI, these are both true:
      sizeof(void *) == 16
      UINTPTR_MAX == UINT64_MAX

Rust stdlib docs on it: https://doc.rust-lang.org/std/ptr/#pointer-vs-addresses

Divergence from u* integer types.

MAX

Of interest here is that if uintptr_t holds 128 bits but UINTPTR_MAX reports (2^64-1), we will need to either make uptr::MAX not self-consistent (ouch) or be very clear in the docs that uptr::MAX is not the same as UINTPTR_MAX, or just not have uptr::MAX at all. If uptr::MAX is self-consistent and returns (2^128-1), any rewriting of UINTPTR_MAX into a Subspace constant should be rewritten to usize::MAX, which will convert up to the larger uptr::MAX if needed, unless the authors were sure they wanted to include the non-address part of the pointer in their max. Having to go through usize seems like the wrong path though, there should be a more clear well-lit Good Path.

Another possibility here would be to have two max values on uptr instead, like

  • uptr::MAX_ADDR // 2^64-1 on CHERI
  • uptr::MAX_BIT_PATTERN // 2^128-1 on CHERI
    This would break the use of uptr in ducktyped generic code that rely on T::MAX, though perhaps in a good way. If MAX == MAX_ADDR it would be incorrect to use in generic code that wants to do bit masking with it as it would miss half the bits. If MAX == MAX_BIT_PATTERN it would be incorrect to use in generic code that wants to write MAX into the type, as it would be writing non-zeros into the capabilities and then produce Bad Things in CHERI.

FWIW usize::MAX is not really a valid value for usize for the things it is intended for which is offsets into arrays, as the max size of any allocation/array is isize::MAX or a 31-bit number. But it can also just be used as "an integer" so MAX is not (2^31-1). That said uptr::MAX being uptr::MAX_BIT_PATTERN feels much worse as it will directly cause bad CHERI things to occur when converted back to a pointer, whereas a x_usize >= 2^31 can be caught by bounds checks (or compiler checks for Array sizes) if misused. So I don't see this as strong argument for having uptr::MAX == MAX_BIT_PATTERN.

Proposal

Let's start with MAX_ADDR and MAX_BIT_PATTERN and drop MAX for now (and also have MAX_ADDR_PRIMITIVE and MAX_BIT_PATTERN_PRIMITIVE).

Conversions

  • uptr should be implicitly constructible from any pointer, unlike other u*.
  • uptr should not be implicitly constructible from smaller integer types, unlike other u*.
  • uptr should not be explicitly convertible to primitive integer types except uintptr_t. Up for consideration if this should be through a method call to help avoid code compiling and relying on it converting to other types due to type aliases and then breaking under CHERI, but a method call would give an explicit name/intend to callers but would be less type-safe than an operator std::same_as<uintptr_t>().
  • To convert to a usize, it should provide .addr() (like Rust's proposal) which returns usize.
  • uptr can be constructible from a usize but explicitly through means that specify the upper bits.
    • uptr.with_addr(usize) (like the the Rust proposal) can copy the high bits from *this to the new pointer. e.g. uptr(actual_pointer).with_addr(a_different_address).
    • I considered a static uptr::from_ptr_and_addr(T*, usize) but it's basically a syntactic sugar for with_addr. It's not clear if it would be painful to construct a uptr and call .with_addr such that it's worth adding this shorter path.
  • uptr should not be From<u*> for explicit conversions, as it should instead be constructed through with_addr() through another pointer.
  • uptr should also not be Into<u*> (or IOW u* should not be From<uptr>). If size_of<uptr>() > size_of<usize>() then basically every conversion from uptr to usize will be out of bounds due to the capabilities in the high bits, and thus .addr() would need to be used to not panic.

What to call Option::and() / Option::or()

It's currently Option::and_opt() and Option::or_opt() but that's kinda awful.

It can't be just and() / or() because C++ made them into the binary && and || operators for some reason.

Ideas:

  • Option::and_if(Option)
  • Option::and_(Option)
  • Option::and_with(Option)
  • Option::and_op(Option) (as in operation)
  • ??

References + Copy/Move/Clone

Copy / Move

References are Copy/Move, because T& can be copied to a T&.

However it's easy to write broken things then

auto f(Copy x) {
  return x;  // Copy!
}

This won't compile if x is a reference T& and the type T isn't Copy, since it's returning auto which is not a reference.

We should be more clear about CopyToObjectType vs CopyToExactSameType, and same for clone()/move()?

  • copying makes an object type unless you force it not to by declaring the output type to be a reference (e.g. auto&)
  • clone() should return an object type.
  • move() returns an rvalue-reference, not the input reference type.

Can we allow specifying the output type of clone() and move() so that they can preserve references (explicitly) like forward does? This would basically collapse move() and forward() but by default move() would infer the output as std::remove_reference_t<T>&&

Clone

Because references are Copy they are Clone, but we need clone() to return an object type. This makes it break in exactly the above case, when passed a reference type.

The problem is that by the time you get into a universal-reference function parameter, the type is always a reference, since non-reference types get promoted to references. There's no way to tell apart an input lvalue object and lvalue reference:

template <class T>
void f(T&&);

int i = 1;
int& r = i;
f(i);  // receives int&
f(r);  // receives int&

And overloading on T and T& is ambiguous for the same reason.

Separate concepts for references

Most C++ templates are not generic over references, outside of perfect forwarding into assignment, because it's very tricky to do so. However we want to use them as such to avoid nullable pointers.

References should not be Copy/Move/Clone, they should strip the reference from the type.

We can introduce helpers for writing generic code that supports reference types?

  • CopyOrRef
  • MoveOrRef
  • CloneOrRef

Using these concepts implies the author has a burden to verify that if the type is a reference, it is the reference that is acted on, and not the underlying type.

Testing

We should test all T<U&> templates with a type that is not Copy or Move so that we can verify the underlying U is never copied/moved instead of the reference itself.

Choice needs to have try_get_ref() or similar to return Option

Like Vec::get_ref() which returns an Option, Choice::get_ref() should also return an option.

But then it needs an API to grab the current value and panic() in case of error, like Vec::operator[].

Consistency

  • Tuple::get_ref() couldn't be named get() cuz then structured bindings would try to use it (and it's not overloaded for mutable access). But Tuple::get_ref() returns the naked type.
  • I renamed Vec::get() to get_ref() to match. (Tuple and Vec both had get_mut() as well before this change.) But now they return different types, Option and T respectively.
  • Choice::get_ref() should be more like Vec's not Tuple's.

Some ideas

  • operator() and operator*() can't be templated. RIP.

  • If we get rid of the template #124, then operator() is possible, but not pretty.

Choice u;
return u(2);
  • Tuple get_ref(), get_mut() could instead be something more Tuple-specific, since they behave differently.
tuple.get_n<2>();
tuple.get_n_mut<2>();
tuple.at<2>();
tuple.at_mut<2>();
  • Then Vec and Choice can go back to get(), get_mut() returning an Option.
  • Does Choice::at() give the naked type or panic then?
choice.at<tag>();  // can panic
choice.at_mut<tag>();  // can panic
choice.get<tag>();  // return Option
choice.get_mut<tag>();  // return Option

Performance improvements

Use google benchmark or other performance/profiling tools to make better choices about inlining, branch ordering, [[likely]]/[[unlikely]] or other code gen in critical paths to improve performance.

Result<T, E> should support reference-type T and E

This requires using storage as a pointer (like Option's StoragePointer) and a lot of care to preserve references instead of copying/moving the underlying types.

It's plausible to not support reference types in the E parameter. But returning a static string reference should also be okay?

Slice::select_nth_unstable_by

This method is not implemented yet. It requires us to implement sorting ourselves, which depends on Iterator::max_by(), min_by() in the Rust impl.

Iterator::collect_vec()

Would eliminate the need to write the type in collect() for the most common case.

Vec<i32> = it.collect_vec();

Export everything in a sus module.

It's a pain point to have to include the iterator.h header yourself when using iterators on a type.

Granted there's circular references Option -> Iterator -> Option. Can we use template types to break the cycle in the code like we did for integers?

Opaque<T> that can be converted to/from Vec<T>

From @noncombatant:

An opaque sequence type. For example, we often see IPC interfaces where the type is vector, and we say, "Whoa, are you going to parse this?" And the developer says, "No, we never look at it. We just pass it to someone else." Well, it'd be nice to encode that in a type, maybe? E.g. a vector-like thing that has only size method, no operator[]. So you can't parse it, you really can only pass it to someone else (and they can have a Foo::FromOpaque(T&) function)

IOStreams

Lots of stuff expects C++ types to be streamable. But stream headers are also the worst for compile times.

Right now, to print a numeric you need to cast it to a primitive:

    for (auto [pos, val]: it) {
      std::cerr << "(" << size_t{pos} << ", " << int{val} << ")\n";
    }

We could:

  • Add operator<< to them, and put the impls into .cc files so we can limit to iosfwd.h. This is doable as they aren't templates.
  • Add a ToString() method to them, which returns a sus::String, which itself needs to be streamable. We still end up with iosfwd.h at least.
  • Implement Display/Debug concepts which gives them .debug() and .display() methods instead, which take as input a thing to write to. These can also go into a .cc file if helpful. Then also write our own stuff like fmt that uses {} to call .display() and {:?} to call .debug().

We need a better story to print numerics though before they are stabilized, whatever we pick.

Strings and Chars

CStrings, OSStrings, Strings, string constants. Char (aka unicode codepoints).

There's a lot of possible ways to take this design space-wise.

What's important is that C++ strings full of not-utf8 have somewhere to go that won't crash. So probably String isn't utf8. Maybe UString or something.

Also string constants should not need relocation, so if we can't do that with a type then we can't and they should stay as chars but I think we can. string_view does it now? constexpr? consteval?

Remove Mref

I think it's a cool idea but the mref() annotation should be done through a static analysis check instead of in the type system.

By doing it in the type system, it forces function/method overloads when receiving const T& or T& in a template where the distinction matters. Overloading is against the principles. For example in Option:

  constexpr T& insert(T t) & noexcept
    requires(sus::mem::Move<T> &&
             !(std::is_reference_v<T> &&
               !std::is_const_v<std::remove_reference_t<T>>))
  {
    t_.set_some(move_to_storage(t));
    return t_.val_;
  }

  constexpr T& insert(Mref<std::remove_reference_t<T>> t) & noexcept
    requires(std::is_reference_v<T> &&
             !std::is_const_v<std::remove_reference_t<T>>)
  {
    t_.set_some(move_to_storage(t));
    return t_.val_;
  }

Keep mref()s

In anticipation of a static check, keep the mref() annotations that we have, but make it a function

template <class T>
decltype(auto) mref(T&& t) { return static_cast<T&&>(t); }

Option<T>::foo needs const& overloads when T is Copy

For all &&-qualified methods we should provide a const&-qualified overload when T is Copy.

We can use deducing this (#176) to do this without overloads in the future.

This should block marking Option as stabilized, even though it would not be a breaking change?

Chaining iterators ends up heap allocating sometimes

When A chains from B, A contains B. But in order to be able to move A, B has to move to the heap if it's not trivially-relocatable (and because it is type-erased to avoid template explosions).

This means iterator.filter(x) is a heap allocation when iterator is not trivially-relocatable but is not otherwise.

This presents a hidden performance cost, which is contrary to the principles.

To avoid this we could

  • Require all iterators to be trivially-relocatable (assert this in sus::iter::Iterator<I>). But that does mean the IntoIter methods (T::into_iter()) must do a heap allocation if T is not trivially-relocatable. For Option<U> it means Option<U>::into_iter() is not a heap allocation is U is not trivially-relocatable, but it is a heap allocation if it's not. Again, a performance cliff, but this time closer to the types. πŸ‘Ž
  • Ban IntoIter on non-trivially-relocatable types. We do really want to steer toward data being trivially relocatable, but this is a strong requirement. We should be sure this is the best choice. πŸ₯ˆ

Is there another path?

Or can we expose the perf cliff explicitly somehow?

  • A diff Iterator<> class for non-trivially-relocatable iterators? But that means we need a diff IntoIter concept too since classes return iterators? πŸ‘Ž
  • Chaining functions require trivially-relocatable, and we have a separate .box() method that lifts the iterator into the heap so it can be chained? e.g. instead of iterator.filter(x) if iterator is not trivially-relocatable, move(iterator).box().filter(x). The box() method simply chains the iterator with an iterator that puts the inner one onto the heap and forwards next() to it. πŸ₯‡
    • If we do this, use RelocatableStorage for the inner iterator or remove the RelocatableStorage code.

Fill out the Result API

https://doc.rust-lang.org/stable/std/result/enum.Result.html

There is a TODO in the result.h header:

// TODO: Result<void, E>
// TODO: Result<T&, E&>
// TODO: and, and_then, as_mut, as_ref, cloned, copied, expect, expect_err,
// etc...
// https://doc.rust-lang.org/std/result/enum.Result.html

Like Option, we want to clone the full API of Result.

We also need a way to access (at least) the result without turning the Result into an rvalue, like operator* and operator-> on Option.

How do we mark things as unstable?

We want to stabilize numerics, which come with Option. But not stabilize iterators and tuples yet which are found in the Option API.

We need to mark those methods as unstable somehow so they won't be used by accident?

  • [[deprecated]]?
  • A marker unstable_fn type?
  • [[clang::annotate]]?

We also want to mark concepts (e.g. NeverValueField) or types (e.g. Iterator) as unstable.

[[clang::annotate("sus::unstable")]] seems like the most clear choice except it wouldn't do anything. We'd need to just say you can't use them, and we can write a clang-tidy pass that errors on their use?

The use of a clang-only attribute is unfortunate. Not sure how to address this in a cross-compiler way. This would be enough for some projects like Chromium.

The `template` keyword is needed to call tuple.get_ref() and union.get_ref() etc

This is awkward...

auto tuple = ...;
tuple.get_ref<0>();  // no :(
tuple.template get_ref<0>();  // yes :(

Not only this but the compiler errors on MSVC and GCC complain about the () missing arguments (cuz we have get_ref() methods on some types that take an argument?) instead of complaining about missing template. Only Clang gets this right..

I wish we could pass the index as a parameter but I don't think that we can, we have to walk up N base classes in tuple, and the methods can't be consteval or anything like that.

Do we move to a global get_ref? :/ That's really unlucky if so. And it's not needed for vec/array/slice/indexing things.

ExactSizeIterator and DoubleEndedIterator concepts

ExactSizeIterator concept

Similar to https://doc.rust-lang.org/core/iter/trait.ExactSizeIterator.html, it says that the size_hint is exact.

Q: How does a type opt into being ExactSizeIterator? Does it have a different exact_size_hint() method?

Then use this for collect/from_iter implementations.

DoubleEndedIterator

Similar to https://doc.rust-lang.org/core/iter/trait.DoubleEndedIterator.html, it has a next_back() method.

Presence of next_back() -> Option<ItemT> opts a type into being DoubleEndedIterator.

[C++23] Use deducing this to write &&-only-qualified methods

Compiler Status

Compiler support:
βœ… MSVC has "deducing this" support today.
β›” GCC does not have any support yet. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=102609
🚧 Clang does not have support but cor3ntin is working on it: llvm/llvm-project#59619

Overview

Today we write methods that consume this as &&-qualified. For example:

  constexpr T Option::unwrap() && noexcept {
    return ::sus::move(*this).unwrap_unchecked(::sus::marker::unsafe_fn);
  }

If the T in Option<T> is not sus::mem::CopyOrRef, then this is the right thing to do. To call this method you must have an rvalue, either by construction or by sus::move().

However if the T is CopyOrRef, then Option<T> is Copy. And in that case, it would be nice to do as Rust would do. Calling a method that consumes this would first do a copy, and call the method on that copy. In Rust you'd write such a thing like

fn unwrap(self) -> T {
  unsafe { self.unwrap_unchecked() }
}

as there are no rvalue references in Rust.

In option.h we have a TODO which points out we can do this in C++ by writing a 2nd copy of every &&-qualified method as a const&-qualified method which requires sus::mem::CopyOrRef<T>, as that is the condition that makes Option<T> itself become Copy.

However this means a 2nd copy of every method on Option more or less. Overloads suck.

Good news is C++23 introduces deducing this, which allows us to combined &, const&, &&, const&& qualified overloads into a single method.

  template <class Self>
  constexpr T Option::unwrap(this Self&& self) noexcept {
    return ::sus::forward<Self>(self).unwrap_unchecked(::sus::marker::unsafe_fn);
  }

The forward will preserve the reference type of self and we have 1 method for all qualifiers. However this is not really what we want at all. We only want a &&-qualified method (and a const&-qualified one if CopyOrRef<T>).

But there's another formulation of deducing this that received self by value.

  constexpr T Option::unwrap(this auto self) noexcept {
    return ::sus::move(self).unwrap_unchecked(::sus::marker::unsafe_fn);
  }

Here we would receive a new self move-constructed from an rvalue, if the caller was calling a method on an rvalue. And if the caller was an lvalue, it will have to copy-constructed self. This does exactly what we want, as it copies if Option was Copy.

The only downside here is the errors. We can't eliminate this method from attempting to be called on an lvalue when self is not Copy. In fact it will instead fall back to C++'s internal traits of std::is_copy_constructible<Option<T>>. Since Option makes a copy constructor iff it also makes a copy assignment operator, this ends up being equivalent, you just get an error about failure to copy if you did it wrong due to deleted copy constructor in Option instead of the nicer "this type is not Copy" that you can get from requiring sus::mem::CopyOrRef.

Plan

So all this is to say, once all three compilers have deducing this, I think we should:

  • In all cases that we have a &&-qualified method and we don't explicitly delete the const&-qualified overload (I don't think we have any of that),
  • And when the class itself is or can be Copy,
  • Convert the &&-qualified method to a this auto self.

If the class can not be Copy, or has a deleted const&-qualified overload, then convert &&-qualified methods to this Self&& self as it's just a nicer syntax.
Similarly, convert all other qualified method, perhaps all methods in totality, over to their equivalent deducing this version. Be careful to not use auto&& as the type of this when you don't want to support all qualifiers.

Fn support for templated lambda (and static [C++23] operator()?)

Currently Fn/FnMut/FnOnce requires as input a lambda with a non-templated non-static method operator().

Templates

Calling a templated operator() is typically invisible as the types are deduced. For instance the parameter can simply be typed auto. Fn should support this. The reason it does not is (I believe, only) because the concepts for checking if the object is callable in sus::fn::callable don't know what to do with a template. I'd like to error correctly at the construction of Fn rather than waiting until running it to fail, but it's not obvious if it can check for the presence of operator() if it's templated. Given Fn knows the types of the arguments it will receive, it should be able to use those to figure out if operator() works.

static operator()

C++23 adds "static operator()" so that it's possible to have a type provide operator() without a this pointer.

Should the Fn/FnMut/FnOnce types be able to consume such types, and what would it look like? Right now it consumes either:

  • A function pointer
  • A callable object

Both of these are objects.

To act on a static operator(), it would need to convert the type into a function pointer, but what would that API look like?

Fn Fn::with_type<MyType>() { return Fn::with_fn_ptr(&MyType::operator()); }

This is a weird API, and it's not clear what if anything would need to use it. The caller can simply take the address itself, so I think probably this isn't needed? I think it's worth keeping it in mind in case there are some interesting use cases that appear.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.