chromium / subspace

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.
https://suslib.cc
Apache License 2.0
89 stars 15 forks source link

Tuple and Option markers/conversion construction #384

Closed danakj closed 1 year ago

danakj commented 1 year ago

Rust

let mut v = vec![1, 2, 3, 4, 5, 6];
v.sort_by_key(|&num| (num > 3, Reverse(num)));
assert_eq!(v, vec![3, 2, 1, 6, 5, 4]);

C++

auto v = sus::Vec<i32>(1, 2, 3, 4, 5, 6);
v.sort_by_key([](i32 num) {
  return sus::Tuple<bool, Reverse<i32>>(num > 3, Reverse(num));
});
sus::check(v == sus::Vec<i32>(3, 2, 1, 6, 5, 4));

This would be nicer

auto v = sus::Vec<i32>(1, 2, 3, 4, 5, 6);
v.sort_by_key([](i32 num) { return sus::tuple(num > 3, Reverse(num)); });
sus::check(v == sus::Vec<i32>(3, 2, 1, 6, 5, 4));

but tuple() returns a marker that deduces types for vec() this lets you pass it heterogenous types and it figures out the actual thing later to convert them all to but constructing a Tuple idk maybe you want to use the types you have. without... writing them all out 😐 tbh i never write vec(...) even. it's very nice for some() and none() but for Vec just writing it out seems.. fine maybe i should kill the marker types except for some/none and ok/err.

i see std::optional<T> can construct from std::optional<U> which if Option did, some(T) could return Option<T> and then convert later to Option<U>, instead of a marker type that deduces to the U. this would be a lot more friendly with returning things but Rust's Option does not convert from Option<T> to Option<U>. There's no impl From<Option<U>> where T: From<U> or something. otoh because it can hold refs/pointers, we really do probably want to allow converting Option<Sub&> to Option<Super&> in the same way we did for Box. inheritance is A Thing.

You can't construct an Option<T> from none() though, unless it's none<T>(). But like std::nullopt, which is a marker type for none, we could have none() return a marker if T isn't specified but some() return an Option . It's an asymmetry.

Even more of an asymmetry is Result<T, E>, both ok() and err() know only one of T or E so neither can actually make a Result. Marker types make sense there, so many they make sense for Option too.

But for the containers, and tuples, they seem to be more trouble.

danakj commented 1 year ago

The markers however make it much easier to work with references.

For calling void f(Tuple<i32&, i32&>) and void g(Tuple<i32, i32>), f(sus::tuple(i, i)) and g(sus::tuple(i, i)) both do the right thing.

But then the caller doesn't see the reference being captured so maybe that's worse in C++ land anyway.

danakj commented 1 year ago

One thing missing without sus::vec() is a way to type-deduce an empty vector.

We can sus::Vec(a, b, c) and deduce Vec<decltype(a)> which is good! But what about empty?

Maybe we need a generic sus::marker::empty or something?

danakj commented 1 year ago

Another option but kinda weird is


template <class T = sus::marker::Undeduced>
class Vec {
  Vec();  // works with Undeduced.
  Vec(T); // doesn't cause you can't construct Undeduced.

  Vec(Vec<Undeduced>);  // An empty Vec.
};```
danakj commented 1 year ago

Dropping tuple() is bad because it handles reference types well, so that we can construct a Tuple over references without writing the whole type out.

So as a compromise position, added type deduction guides for Tuple (and Vec and Array and Option) which do not preserve references but can be used for simply constructing concrete types without writing out the full template. And allow Tuple to convert when its contents can convert.

But keeping the tuple() deduction helper as well, as it supports references by deferring until an actual Tuple type is known.