Jonathan Boccara's blog

A Summary of the Metaclasses Proposal for C++

Published August 4, 2017 - 19 Comments

Daily C++

A couple of weeks ago, Herb Sutter posted his proposal about metaclasses, triggering a wave of enthusiasm among the C++ community. And for good reason.

His proposal gradually introduces the reader to the impressive potential of metaclasses, in particular to improve the expressiveness of current C++ idioms. I think everyone should be aware of the contents of this proposal.

Why this one in particular, you may think? On the top of the power brings to C++, I find it gives a lot of perspective on the language as it is today. Reading it will show you where the language is heading and how the features available today fit into that picture.

Oh just one detail: the proposal is 37 pages long, and each one of them is full of content.

If you have the time to read that kind of document then by all means, do it. Otherwise if you just want to get started I’ve read it for you and made this summary, so that you can understand what metaclasses are about. I’ve also added a selection of the components that I found the most wowing, to make you experiment that feeling of metaclasses.

Thanks to Herb Sutter for reviewing this article.

struct or class is not enough

Today struct and class are the two main ways to define a type in C++. From a technical perspective they practically behave the same way, but we have conventions to choose which one to use in order to express different meanings in our code.

But they’re just that: conventions. The language doesn’t do anything to enforce that we choose the right one in a given context. And not respecting a convention is even worse than not having a convention at all, because it sends the reader of the code off to a wrong track.

Also, be there for struct or class, the language hardwires some rules for all types, such as generating copy constructors and other such methods under certain conditions. But these rules are one-size-fits-all, and sometimes they’re not adapted to a particular type. This leads to the needs of correcting the effect of such rules with =delete and =default, and to difficult decisions for the standard committee (should we hardwire default comparison operators for all types?).

What’s more, for some types neither struct nor class is a good choice. Take the example of an interface, containing only pure virtual functions and meant to be derived from. Should it be a struct or a class? Neither one fits, so everybody needs to pick one with a reasoning sometimes flirting with the irrational.

Finally, some C++ idioms lead to duplicated code. Take the example of the interface again: even though interfaces always have pure virtual public methods and a virtual destructor, we’re forced to make sure we got this right every time. Today there is no way to factor out such common features.

Metaclasses

Metaclasses aim at fixing each of the above issue with struct and class, by letting you augment those two with your own types of type (hence the name metaclass).

So, a bit like classes are models from which you can instantiate objects at runtime, metaclasses (defined with the keyword $class in Herb’s proposal) are a model from which you can generate classes, at compile time. And these classes are like all other normal classes of the language, meaning in particular that you can instantiate objects from them at runtime.

To illustrate, the relationship between classes and objects have always looked like this:

class objects

 

and here is what it should look like with metaclasses thrown in:

metaclasses classes objects

Now to show you the proposed syntax for metaclasses, let’s keep the example of the interface, which Herb Sutter uses to illustrate metaclasses. Here is how to define a metaclass:

$class interface
{
    // code that describes what an interface is,
    // like having a virtual destructor, no copy constructor,
    // all public and pure virtual, etc.

    // see next section for implementation
};

And here is how to instantiate it: just use the name of the metaclass in place of struct or class:

interface Drivable
{
    void speedUp(int acceleration);
    void brake();
    void turn(int angle);
};

When parsing this, the compiler generates a class Drivable by making all those methods pure virtual and by adding a virtual destructor.

This gives access to unprecedented expressiveness for describing an interface (I’m ignoring the subject of strong types for arguments in this example).

Note that a metaclass would be used as a template argument too, with the same syntax as the one proposed for concepts:

template<interface I>
...

Reflection and compile time programming

Now how to implement the interface metaclass? Metaclasses implementations are based on two other proposals for C++: reflection and compile-time programming.

Reflection allows metaclasses to manipulate the features of a class itself (a bit like classes manipulate the features of their objects). For example, reflection allows to inspect the features of the methods of a class (you can recognize reflection in its current proposal with the use of the $ sign):

for (auto f : $interface.functions())
{
    if (!f.has_access())
    {
        f.make_public();
    }
}

You should read this as: for each function (method) in a class instantiated off the interface metaclass, if the scope of this method (public, protectedprivate) if not specified explicitly in code, then consider it public.

With reflection, metaclasses can also define functions, such as a pure virtual destructor for the interface metaclass:

~interface() noexcept = 0;

or:

~interface() noexcept { }
for (auto f : $interface.functions())
{
    f.make_pure_virtual();
}

Compile-time programming consists in defining a region in the codeline where the code is meant to be executed at compile time, with the evaluation of compile time data leading to a result. The region is delimited by a constexpr block, and the condition and results are expressed by the compile time evaluation -> { result } syntax. Here is an example on another metaclass, ordered, that defines default comparison operators if they aren’t already defined by the class:

constexpr
{
    if (! requires(ordered a) { a == a; }) ->
    {
        friend bool operator==(ordered const& a, ordered const& b)
        {
            constexpr
            {
                for (auto variable : ordered.variables())
                    -> { if (!(a.variable.name$ == b.(variable.name)$)) return false; }
            }
            return true;
        }
    }
}

Note the two constexpr blocks in the above code. The line with requires means “if an operator== is not already implemented for the class”. It reads a bit odd in this context, but it’s the natural syntax coming out of concepts.

Finally, metaclasses rely on compile-time checks to enforce constraints, with an appropriate message showing up in a compile error if the constraint is not respected. For example, here is how to make sure all the methods of an interface are public:

for (auto f : $interface.functions())
{
    compiler.require(f.is_public(), "interface functions must be public");
}

Here is the full implementation proposed for the interface metaclass:

$class interface
    {
    ~interface() noexcept { }
    constexpr
    {
        compiler.require($interface.variables().empty(), "interfaces may not contain data");
        for (auto f : $interface.functions())
        {
            compiler.require(!f.is_copy() && !f.is_move(), "interfaces may not copy or move; consider a" " virtual clone() instead");
            if (!f.has_access()) f.make_public();
            compiler.require(f.is_public(), "interface functions must be public");
            f.make_pure_virtual();
        }
    }
};

The cool things metaclasses can do

I have selected three things that metaclasses can do on top of being able to define interfaces and ordered classes as shown above, and that really wowed me

The value metaclass

Ever heard of regular types? Essentially they are types that comply to some rules that make them behave the way you would expect them to behave. They are developed in great details in the very popular book of Alex Stepanov Elements of Programming.

Regular types can be represented with the value metaclass, that splits its definition into two parts:

  • basic_value that defines all the default constructors, destructors and other assignment and move operators,
  • ordered that defines all the comparison operators.

And all these methods are implemented so as to be consistent with one another (so that after a copy assignment, operator== returns true for example). And all this can be simply expressed by the use of metaclass value:

value PersonName
{
    std::string firstName;
    std::string lastName;
};

The namespace_class metaclass

The current convention for defining template types or functions that belong to the implementation details of your library is to put them into a sub-namespace called detail. Indeed, you can’t hide these in .cpp file because, as templates, they need to be in the headers included by the clients of the library. Boost uses this convention extensively.

This convention does the job but has two issues: 1) nothing prevents a library user from using something in the detail namespace, jeopardizing backward compatibility of your library and 2) it’s annoying to go in and out of this namespace inside the code of the library.

One solution to these two problems would be to use a class instead of the namespace, and use private methods for implementation details, but this would create three new problems:

  • class doesn’t express that it is a namespace we really mean,
  • class offers a host of features that don’t make sense for a namespace, like member variables for example,
  • unlike a namespace, a class cannot be reopened and defined by several locations across the codeline.

The proposed namespace_class allows to have the best of both worlds. Here is its implementation:

$class namespace_class : reopenable // see below for reopenable
{
    constexpr
    {
        for (auto m : $reopenable.members())
        {
            if (!m.has_access ()) m.make_public();
            if (!m.has_storage()) m.make_static();
            compiler.require(m.is_static(), "namespace_class members must be static");
        }
}
};

with the reopenable allowing a definition in several part at different locations in code:

$class reopenable
{
    constexpr
    {
        compiler.require($reopenable.member_variables().empty(), "a reopenable type cannot have member variables");
        $reopenable.make_reopenable();
    }
};

And this is how it would be used to replace the detail namespace:

namespace_class my_libary
{
public:
    // public interface of the library

private:
    // implementation functions and types
};

Neat, right?

The plain_struct metaclass

Finally, the plain_struct aims at representing what we currently use struct for, but with the compiler checking that we respect the convention.

More precisely, it is a basic_value with only public functions and public nested types, no invariants (which means no user-defined default constructor, copy, assignment or destructor), and the strongest comparison operators that its members allow to write.

Want to know more?

Now that you have a clearer idea of what metaclasses are, I suggest that you read Herb Sutter’s proposal if you want to dig further into this topic. It’s well written and has a lot of examples. The parts I found the most impressive in terms of improved expressiveness after those I’ve presented here are:

  • the .as operator (section 2.6.2 and 2.6.3)
  • safe_union (section 3.10)
  • flag_enum (section 3.8)

But all of it is a great read anyway.

You can also watch Herb’s talk on metaclasses at the ACCU conference or his blog post announcing the proposal.

Metaclasses seem like a structural change of C++ to me, bringing unprecedented expressiveness to our interfaces, and robustness to our code. Let’s get ready for them.

Related articles:

  • Metaclasses, The Ultimate Answer to Strong Typing?
Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin

Comments are closed