cpp-as-is: A taste of the future of genericity, in the past
Tags: cpp, library, cpp17
Published: 2023-09-10 14:02:08

At [CppCon 2021, Herb Sutter gave a talk](https://www.youtube.com/watch?v=raB_289NxBk) about potential pattern matching in the C++ standard.

One point in particular held my attention: there are many ways to extract or cast values in C++, but that makes it more difficult to write generic code (e.g. a single templated function).

He talked about the [P2392 proposal](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2392r0.pdf) that introduces two new operators: `is` and `as`. These features are currently implemented only in [Circle](https://www.circle-lang.org/).

[cpp-as-is](https://github.com/Voltra/cpp-as-is/tree/dev) ***(WIP)*** is a small library that aims to provide these features today (as long as you have C++17 support).

The idea came to me from both re-watching that talk, but also deeply thinking about how I could port to C++ the conversion utilities of Rust and how it has both `std::convert::From` and `std::convert::Into` traits when it really only needs `std::convert::Into`.

One of my favorite "feature" of C++ is how you can basically use the language to introduce new features and "extend the language", without having to change the language (think Michael Park's [patterns library](https://github.com/mpark/patterns)).

This library sort of follows that principle. The basic idea is to be able to write the following:

```cpp
#include <any>
#include <iostream>
#include <optional>
#include <string>
#include <variant>
#include <cpp_as_is/cpp_as_is.hpp>

using namespace cpp_as_is;

template <class T>
void inspect_underlying_int(const T& obj) {
  if (is<int>(obj)) {
    std::cout << as<int>(obj) << '\n';
  } else {
    std::cout << "<nothing>\n";
  }
}

int main() {
  inspect_underlying_int(std::any{42});

  std::optional<int> opt;
  inspect_underlying_int(opt);

  std::variant<std::string, int, float> v{23};
  inspect_underlying_int(v);

  const std::optional<int> opt2{420};
  inspect_underlying_int(opt2);
}

/*
Output is:

42
<nothing>
23
420
*/
```

Without `is` and `as` you have to write quite a few overloads if you want your one function to handle all cases properly. And the worst part is that you have to do it all over again with every single function.

Here, you just use two extension points and you're good to go with using `is` and `as`:

```cpp
namespace cpp_as_is::ext {
  template <class From, class To> struct is_conversion_traits {
    using arg_type = /*From*/;

    /*constexpr*/ static inline bool matches(/*arg_type*/) noexcept;
  };

  template <class From, class To> struct as_conversion_traits {
    using arg_type = /*From*/;
    using return_type = /*To*/;

    /*constexpr*/ static inline /*return_type*/ convert(/*arg_type*/) noexcept;
  };
}
```

You also have concepts constraints on `is` and `as` to make them easier to use and have better error messages:
```cpp
namespace cpp_as_is::ext {
  template <class From, class To> concept InspectableWithIs;
  template <class From, class To> concept IsConvertibleWithAs;
}
```

Here's a list of supported conversions (as of now):
* `T` -> `T`
* `T*` -> `T`
* `std::shared_ptr<T>` -> `T`
* `std::unique_ptr<T>` -> `T`
* `std::variant<V1..., T, V2...>` -> `T` (and boost alternatives)
* `std::any` -> `T` (and boost alternatives)
* `std::expected<T, E>` -> `T` (and boost alternatives)
* `std::expected<T, E>` -> `std::unexpected<E>` (and boost alternatives)
* `std::optional<T>` (and boost alternatives)
* `std::future<T>` -> `T` (and experimental, and boost alternatives)