Anonymous types in C++

Since C++11 we have lambdas – anonymous functions that can be declared anywhere in code. They are quite a useful tool and have many additional capabilities when compared to regular functions. I am not going to talk about them here, as they are not the topic of this post, but one of them is especially useful – the ability for them to be defined in block scope, rather than only in namespace or class scope.

A natural analogue to lambdas would be anonymous types that could be declared anywhere including inside expressions. You may say that since C++11 we already have such a tool – std::tuples. However I wouldn’t call it an anonymous type, as it doesn’t have most of the properties that normal class types have:

  • It can’t have methods
  • It can’t have base classes
  • It can’t have access specifiers – all “members” are “public”
  • Its members can’t have names – instead they have to be accessed using the ugly verbose std::get.
  • It can’t have static members
  • It can’t model a union type – std::optional, std::variant and std::any are for that
  • It can’t be templated
  • It can’t have nested classes or enumerations
  • It isn’t a unique type – std::tuple<const char*, int> is the same type regardless of whether it represents a person (name, age), an associative container key-value-pair or a span (memory block pointer, memory block size).

The list could probably go on. You could say that tuples are not meant to be used like that and they don’t need all the capabilities that normal structs and classes have, but the same argument could be used for lambdas, yet they have all functionality of normal functions (including templating them explicitly since C++20).

When I think of anonymous types, I don’t want to have to deal with all these limitations. Wouldn’t it be great, if we could declare functions returning anonymous structs with members that would have meaningful names that we could use to access them rather than by using std::get:

struct scroll_offset { int width, height; } getWindowSize(GLFWwindow* window) {
    int width, height;
    glfwGetWindowSize(window, &width, &height);
    return {width, height};
}

Or declare functions taking parameters of anonymous types that could be defined in place, in the function definition, without the need to pollute the enclosing scope:

constexpr auto simplify(struct {int numerator, denominator;} ratio) -> decltype(ratio) {
    int gcd = std::gcd(ratio.numerator, ratio.denominator);
    return { ratio.numerator / gcd, ratio.denominator / gcd };
}

Or inherit from anonymous classes:

class Derived public class Base {
    int x, y;
public:
    int& getX() {return x; }
    int& getY() {return y; }
}
{
    int sum() {
        return getX() + getY();
    }
};

Sadly, its all just a dream… Or is it? What if I told you that you could code like that in C++ and compile it right now? Well, see for yourself (1, 2, 3).

Awesome, isn’t it? I’m sure there must be more use cases of anonymous types and this is just scratching the surface of what is possible in C++20. So how does this wizardry work, you may ask. Let’s dive into the code and see what’s inside.

Disclaimer: Before we start, I think its fair to say that the current compiler support for C++20 is minimal. The code below doesn’t compile on current Clang (11) at all and only certain use cases work on GCC (10) and MSVC (19.28). I expect issues to go away as time goes on and the compilers become more standard-compliant.

C++20 – Brave New Code

Until C++20 there existed one particular forbidden usage of lambdas – declaration in unevaluated contexts. These include the expressions inside the noexcept, typeid, sizeof and probably the most importantly the decltype operators. Fortunately this has changed in C++20 for the better. The reasons for why the previous behaviour was limiting and how it was proposed to be changed can be found in P0315.

Another important change is that since P0732 objects of certain class types can be non-type template parameters including non-capturing lambdas.

This allows as to create a lambda_holder class that holds a lambda object as a part of its type and contains a type member type that holds the return type of the lambda:

template <auto LambdaObj>
struct lambda_holder
{
    using type = std::invoke_result_t<decltype(LambdaObj)>;
};

Actually this is the bare minimum of what is required to have anonymous types. At this point we already have a lot of functionality. In order to have anonymous types, the idea is to instantiate lambda_holder with a lambda object that returns an object of a type declared inside the lambda. Let’s see:

using anonymous_type = lambda_holder<
    [](){
        struct int xydummy{};
        return dummy
    }
>::type;

Not particularly elegant, but it works. It has some issues that we have to solve (like what if the struct wouldn’t be default-constructible) and it lacks some functionality (like being able to declare an anonymous template), but it works. It is enough to work like in the examples (1, 2, 3).

What is different between this code and the code at the beginning of the article is that the former uses macros to make the anonymous type declaration less verbose and although it wasn’t shown in the examples it also allows to define anonymous templates and uses a bit of template magic™ to make it possible to omit writing <> when the type is not supposed to be a template.

In this post I wanted to show off the idea of how anonymous types work and explaining fully how that implementation would be too long and out of scope, as this post is about the general idea. The interested reader can find the full implementation here. The interface and implementation is thoroughly commented there. For now that library is only meant to be experimental and probably won’t be that useful until full support for C++20 arrives.

I’m sure this idea can have a lot of different use cases that weren’t shown off here. The examples given were very basic and were only meant to show off what are some things that can be done, that couldn’t have been that like that before. Feel free to give your thoughts and comments down below. Very likely some mistakes have been made and I would be very thankful for any of them being reported. Thanks and I hope this idea will prove useful.

Exit mobile version