DEV Community

Cover image for cpp-as-is: A taste of the future of genericity, in the past
Voltra
Voltra

Posted on

cpp-as-is: A taste of the future of genericity, in the past

At CppCon 2021, Herb Sutter gave a talk 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 that introduces two new operators: is and as. These features are currently implemented only in Circle.

cpp-as-is (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).

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

#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
*/
Enter fullscreen mode Exit fullscreen mode

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:

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;
  };
}
Enter fullscreen mode Exit fullscreen mode

You also have concepts constraints on is and as to make them easier to use and have better error messages:

namespace cpp_as_is::ext {
  template <class From, class To> concept InspectableWithIs;
  template <class From, class To> concept IsConvertibleWithAs;
}
Enter fullscreen mode Exit fullscreen mode

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)

Top comments (0)