Joe's blog

Self-aware struct-like types in C++11

updated July 5, 2012 11:49:30 PDT

Even with C++11, C++ offers inadequate metaprogramming facilities for user-defined types compared to other programming languages. Given an arbitrary struct type, you can't iterate its fields and get useful information like name, offset, and size, without implementing those facilities by hand. (Nearly every would-be C++ replacement language fixes this, but they unfortunately aren't always viable options.) The C++11 standard library introduced the tuple template as a general-purpose metaprogrammable composite type, but it sucks in a number of ways:

Here's an alternative approach I came up with that provides a user interface nearly equivalent to primitive structs, is much easier to metaprogram with than tuple, and is easier to implement as well, requiring about 150 lines of header-only code. I've put a sample implementation up on Github at https://github.com/jckarter/selfaware. Here's a rundown of how it works.

Self-aware field templates

The main idea is to inherit the "struct" type from a set of field class templates, each of which defines a single field along with methods to generically access its name string, value, and type. For example, a field template named foo looks like this:

template<typename T>
struct foo {
    T foo;

    // field name
    constexpr static char const *name() { return "foo"; }

    // field type
    using type = T;

    // field value generic accessor
    T &value() & { return this->foo; }
    T const &value() const & { return this->foo; }
    T &&value() && { return this->foo; }
};

A preprocessor macro can generate these for us:

#define SELFAWARE_IDENTIFIER(NAME) \
    template<typename T> \
    struct NAME { \
        T NAME; \
        // field name \
        constexpr static char const *name() { return #NAME; } \
        // field type \
        using type = T; \
        // field value generic accessor \
        T &value() & { return this->NAME; } \
        T const &value() const & { return this->NAME; } \
        T &&value() && { return this->NAME; } \
    };

The self-aware struct template

The "struct" template now needs only to inherit a set of field template instances and provide some constructors:

template<typename...Fields>
struct Struct : Fields... {
    // A convenience alias for subclasses
    using struct_type = Struct;

    // Preserve default constructors
    Struct() = default;
    Struct(Struct const &) = default;

    // Forwarding elementwise constructor
    template<typename...T>
    constexpr Struct(T &&...x) : Fields{static_cast<T&&>(x)}... {}
};

A Struct type can then be used either by aliasing a Struct instance or by inheriting an instance and its constructors. (As of Clang 3.1 and GCC 4.7, neither compiler yet supports inheriting constructors, so aliasing is currently more practical.)

SELFAWARE_IDENTIFIER(foo)
SELFAWARE_IDENTIFIER(bar)
// Aliasing a Struct instance
using FooBar = Struct<foo<int>, bar<double>>;
// Inheriting a Struct instance (requires inheriting constructors)
struct FooBar2 : Struct<foo<int>, bar<double>> { using struct_type::struct_type; };

Values of the type look like normal structs to user code:

FooBar frob(int x) {
    FooBar f = {x, 0.0};
    f.foo += 1;
    f.bar += 1.0;
    return f;
}

The type is trivial if its component types are trivial, and its instances can be used in compile-time calculations if its component types can, like primitive structs. (However, because it inherits multiple nonempty types, it's not standard-layout, and thus not quite POD.)

static_assert(std::is_trivial<FooBar>::value, "should be trivial");
static_assert(FooBar{2, 3.0}.foo + FooBar{2, 4.0}.foo == 4, "2 + 2 == 4");

Metaprogramming with self-aware structs

Since the fields of the Struct template are encoded in a template parameter pack, there's a lot you can do to it with unpack expressions and recursive templates. Here are a few examples:

Function application

Applying a function object to a Struct's unpacked fields is easy—just unpack the value() method of each field superclass into a function call expression:

template<typename Function, typename...Fields>
auto apply(Function &&f, Struct<Fields...> const &a_struct)
    -> decltype(f(a_struct.Fields::value()...))
{
    return f(a_struct.Fields::value()...);
}

double hypotenuse(double x, double y) { return sqrt(x*x, y*y); }

double fooBarHypotenuse(FooBar const &x) { return apply(hypotenuse, x); }

Interop with tuple

A Struct can be converted into a tuple or tie similarly:

template<typename...Fields>
auto structToTuple(Struct<Fields...> const &s)
    -> std::tuple<typename Fields::type...>
{
    return std::make_tuple(s.Fields::value()...);
}
template<typename...Fields>
void assignStructFromTuple(Struct<Fields...> &s,
                           std::tuple<typename Fields::type...> const &t)
{
    std::tie(s.Fields::value()...) = t;
}

Generating code from field metadata

The Struct template can implement a static method to iterate through its fields, passing the name string, offset, size, and type of each field to a function object in turn. (Getting the offset unfortunately relies on undefined behavior, because C++11 restricts offsetof to standard-layout types and provides no other well-defined means that I know of for determining offsets independent of an instance.)

template<typename...Fields>
struct Struct : Fields... {
    // ... see above ...

    // NB: relies on undefined behavior
    template<typename Field>
    static std::uintptr_t offset_of() {
        return reinterpret_cast<std::uintptr_t>(&static_cast<Struct*>(nullptr)->Field::value());
    }

    template<template<typename T> class Trait, typename Function>
    static void each_field(Function &&f)
    {
        // Unpack expressions are only allowed in argument lists and initialization lists,
        // so this expression unpacks the function call expression into the initializer list
        // for an unused array (which the optimizer is nice enough to discard)
        char pass[] = {
            (f(Fields::name(), offset_of<Fields>(), sizeof(typename Fields::type),
              Trait<typename Fields::type>::value()), '\0')...};
        (void)pass; // suppress unused variable warnings
    }
};

Many libraries that deal with binary data have finicky APIs for describing struct layouts. A good example is OpenGL's glVertexAttribPointer interface, which is used to describe the format of vertex information in memory. The each_field function template, paired with a traits class, can generate the correct sequence of glVertexAttribPointer automatically from a Struct instance's metadata:

struct GLVertexType { GLuint size; GLenum type; GLboolean normalized; };

// A trait class to provide glVertexAttribPointer arguments appropriate for a type
template<typename> struct GLVertexTraits;
template<GLuint N>
struct GLVertexTraits<float[N]> {
    static GLVertexType value() { return {N, GL_FLOAT, GL_FALSE}; }
};
template<GLuint N>
struct GLVertexTraits<std::uint8_t[N]> {
    static GLVertexType value() { return {N, GL_UNSIGNED_BYTE, GL_TRUE}; }
};

template<typename Struct>
bool bindVertexAttributes(GLuint program)
{
    _VertexAttributeBinder iter(program, sizeof(T));
    Struct::template each_field<GLVertexTraits>(
        [=program](char const *name, size_t offset, size_t size, GLVertexType info) {
            GLint location = glGetAttribLocation(program, name);
            glVertexAttribPointer(location, info.size, info.type, info.normalized,
                                  sizeof(Struct), reinterpret_cast<const GLvoid*>(offset));
            glEnableVertexAttribArray(location);
        });
    return iter.ok;
}

Selecting a field at runtime by string name

A recursive template can generate code to pick a field at runtime from a string argument, passing the value through a function object to narrow the return type:

template<typename R, typename Field, typename...Fields, typename Visitor, typename...AllFields>
R _select_field(Visitor &&v, char const *name, Struct<AllFields...> const &a_struct)
{
    if (strcmp(name, Field::name()) == 0)
        return v(a_struct.Field::value());
    else
        return _select_field<R, Fields...>(static_cast<Visitor&&>(v), name, a_struct);
}

template<typename R, typename Visitor, typename...AllFields>
R _select_field(Visitor &&v, char const *name, Struct<AllFields...> const &a_struct)
{
    throw std::runtime_error("bad field name");
}

template<typename Visitor, typename Field, typename...AllFields>
auto select_field(Visitor &&v, char const *name, Struct<Field, AllFields...> const &a_struct)
    -> decltype(v(a_struct.Field::value()))
{
    return _select_field<decltype(v(a_struct.Field::value())), Field, AllFields...>
        (static_cast<Visitor&&>(v), name, a_struct);
}
select_field can then be used like this:
template<typename T>
struct converter {
    template<typename U> T operator()(U &&x) { return T(x); }
};

void testStructSelectField()
{
    FooBar x{11, 22.0};

    double foo = select_field(converter<double>(), "foo", x);
    double bar = select_field(converter<double>(), "bar", x);
    assert(foo == 11.0);
    assert(bar == 22.0);
}

Problems

This technique still isn't ideal. Most obviously, field templates all need to be defined somewhere, which adds maintenance friction, and they rely on unseemly preprocessor magic to create. Fields and Struct instances could perhaps be instantiated together in one macro, perhaps by pulling in boost::preprocessor. Compile time, always an issue with C++, also suffers from use of the Struct template. Clang 3.1 takes almost a second on this 2.4 GHz Core 2 Duo just to compile the 199-line selfaware-test.cpp test suite. And tuple, for all its faults, is standard, and will be available on any platform that purports to support C++11. Neither Struct nor tuple is standard-layout and thus can't interoperate with C in a standard-guaranteed, portable way. I'd love to hear about other approaches to enabling composite type metaprogramming in C++.



Archives
© 2012 Durian Software. | Contact Us