Open Content at CppCon: One API for Types and Values

This curiosity was part of my CppCon Open Content talk in 2024, “Another Grab-bag of C++ Oddments”. The session highlighted C++ oddities, in the spirit of discovery through play.

For reasons (TM), I want to be able to do this:

auto x = VALUE(42);        // 42
auto y = VALUE("hello"sv); // std::string_view{"hello"}
auto z = VALUE(int);       // std::type_identity<int>{}

Of note here:

  • x uses a value that can be used as a template argument
  • y uses a value that is not admissible as a template argument (std::string_view is not a structural type)
  • z uses a type “as if it were a value”
  • for my use cases, the “values” here are known at compile time and are almost always literals

Some use cases I have in mind are things like logging, where I want to be able to say things like…

LOG("{} {}", VALUE(42), VALUE(int));

…and have the logging library do the right thing. (If given a type, we might typically do the old __PRETTY_FUNCTION__ trick to recover its name.)

Well, it seems fairly obvious that this is going to have to be a macro. Here’s the skeleton.

#define VALUE(...)
  [] {
    if constexpr(/* detect */) {
      // __VA_ARGS__ is a type
    } else {
      // __VA_ARGS__ is a value
    }
  }()

I’m omitting the backslashes at the end of lines for clarity. And I’m using a variadic macro to allow it to handle arguments with commas in them (like types which are template specializations, for example).

OK, the first thing we need is a way to determine whether we have a type or a value. The only way I know to do that is with an overload set.

template <auto> 
constexpr auto is_type() -> std::false_type;

template <typename>
constexpr auto is_type() -> std::true_type;

// then use the expression
decltype(is_type<__VA_ARGS__>())::value

This works for 42 and int, but not for a string_view, because it’s a hard compilation error to attempt to pass it as a template argument. So let’s make a type that we can pass.

struct from_any {
  template <typename... Ts>
  constexpr from_any(Ts const &...) {}

  constexpr operator int() const { return 0; }
};

// then use the expression
decltype(is_type<from_any(__VA_ARGS__)>())::value;

from_any is a class that is implicitly constructible from any argument(s). from_any is a structural type, so we could pass it as a template argument (from C++20 onwards), but giving it an implicit conversion to int also means this works pre-C++20. We’ll just change the overload set to be friendly:

template <int> // was auto
constexpr auto is_type() -> std::false_type;

template <typename>
constexpr auto is_type() -> std::true_type;

Wait, how does the expression work? Well, consider the cases:

// this is a value of type from_any
// constructed from 42
from_any(42)

// this is a value of type from_any
// constructed from a string_view
from_any("hello"sv)

// this is a type - it's the type of
// a function taking int and returning from_any
from_any(int)

(Raised eyebrows and murmurs from the audience.) It’s not common to see “bare” function types like this, but they are well-formed. So this gives us what we need to differentiate types and values (including non-structural values).

template <int>
constexpr auto is_type() -> std::false_type;
template <typename>
constexpr auto is_type() -> std::true_type;

is_type<from_any(42)>()        // false_type
is_type<from_any("hello"sv)>() // false_type
is_type<from_any(int)>()       // true_type

Now the detection part of the skeleton is fleshed out.

#define VALUE(...)
  [] {
    if constexpr(decltype(is_type<from_any(__VA_ARGS__)>())::value) {
      // __VA_ARGS__ is a type
    } else {
      // __VA_ARGS__ is a value
    }
  }()

The next thing to do is to figure out what to put in the first half of the if constexpr block.

Although only one side at a time will actually be used, both sides have to be syntactically well-formed for both types and values. So we can’t just assume that we have a type, and pass it to a template. But we can use a similar trick to is_type to recover the type in the top half and then put it into a type_identity. (Strictly if we want this to continue working before C++20, we need to provide our own type_identity class template, but that is easily done, so I’ll omit it here.)

template <typename> struct typer;
template <typename T>
struct typer<from_any(T)> {
  using type = T;
};

template <int>
constexpr auto type_of() -> void;
template <typename T>
constexpr auto type_of()
  -> typename typer<T>::type;

// and then
using actual_type = 
  decltype(type_of<from_any(__VA_ARGS__)>());

type_of is using the same overloading form that is_type uses, and this time we’re using a typer helper to recover the T from the function type (from_any(T)). If a value went through this machinery, it would just spit out void, but still be good syntax-wise.

We’re more than halfway done now:

#define VALUE(...)
  [] {
    if constexpr(decltype(is_type<from_any(__VA_ARGS__)>())::value) {
      using actual_type = decltype(type_of<from_any(__VA_ARGS__)>());
      return std::type_identity<actual_type>{};
    } else {
      // __VA_ARGS__ is a value
    }
  }()

Just the other half of the if constexpr to go.

At this point in the Open Content session, the fire alarm went off! The session was adjourned and rescheduled for lunchtime, opposite Jason Turner’s session. So if you left here, and went to see Jason at lunch, I don’t blame you. But you did miss this next bit, which was perhaps the most funny/surprising moment of the talk…

Now we need another construct that is syntactically well-formed when given either a value or a type, but this time we just need it to produce the value.

Consider the following syntax:

// special is a type we'll write
(__VA_ARGS__) + special{};

When __VA_ARGS__ is a value, this is a binary plus expression. When __VA_ARGS__ is a type, this is a C-style cast of a unary plus expression. This construction works for both types and values! (Outbursts of “WHAT” from the audience.) All we have to do is define special appropriately.

struct special {
  template <typename T>
  friend constexpr auto operator+(T&& t, special) {
    return t;
  }

  friend constexpr auto operator+(special) -> special;
  template <typename T> constexpr operator T() const;
};

Given the if constexpr, we’re only ever going to exercise this code with a value, so the unary plus interpretation will never actually be used. We don’t need to define the unary operator+ and the conversion operator – they only need to be declared so that the type interpretation is syntactically well-formed.

(Side note: we could have used operator* here too, since it’s also an overloadable operator with both unary and binary forms. Either plus or multiply works.)

Now we are done.

#define VALUE(...)
  [] {
    if constexpr(decltype(is_type<from_any(__VA_ARGS__)>())::value) {
      using actual_type = decltype(type_of<from_any(__VA_ARGS__)>());
      return std::type_identity<actual_type>{};
    } else {
      return (__VA_ARGS__) + special{};
    }
  }()

This does the job we originally wanted. Is it useful? Maybe. It is fun and educational to write and present at CppCon Open Content? Definitely.

Thanks to everyone who came to my open content. See you at the next conference hallway track. And if you can’t come to a conference, maybe you can spare a couple of hours a month to go to a meetup. If you don’t have a local one (or even if you do), you are welcome at the Denver C++ meetup. We meet on-site and remotely on the first Thursday of every month. It’s always fun.

Published
Categorized as C++