Storage customisation#

In a previous tutorial, we mentioned briefly how the wrap class implements a small storage optimisation, but we left the specifics out. In this tutorial, we will see how it is possible to customise the storage options for the wrap class.

How small is small?#

The threshold size under which the wrap class avoids dynamic memory allocation is represented by the configuration parameter config::static_size. The unit of measure, as usual, is bytes.

The default value for config::static_size is somewhat arbitrary, but it should ensure that on most platforms it is possible to store in static storage pointers and structs with a few members. If config::static_size is set to zero, then the small-object optimisation is disabled and the wrap class always uses dynamically-allocated memory.

One very important thing to understand about config::static_size is that it accounts also for the type-erasure storage overhead. That is, if you specify a config::static_size of 24 bytes, some of those bytes will be taken up by the type-erasure machinery, and thus the storage available for the type-erased value will be less than 24 bytes. How much less exactly is dependent on a variety of factors and difficult to compute in advance.

For this reason, tanuki provides a holder_size helper which can be used to compute how much total memory is needed to statically store a value of a type T in a wrap. Let us a see a simple example.

We have our usual super-basic interface and its implementation:

template <typename Base, typename Holder, typename T>
struct any_iface_impl : public Base {
};

struct any_iface {
    template <typename Base, typename Holder, typename T>
    using impl = any_iface_impl<Base, Holder, T>;
};

Let us say that we want to ensure that we can store in static storage objects which are up to the size of a pointer. We can define a custom config instance in which we set the config::static_size parameter to holder_size<void *, any_iface>:

inline constexpr auto custom_config1 = tanuki::config<>{.static_size = tanuki::holder_size<void *, any_iface>};

This will ensure that value types whose size is up to sizeof(void *) can be stored in static storage.

We can then define a wrap class using the custom config instance and verify that indeed static storage is employed when the wrap contains a pointer using the has_static_storage() function:

    using wrap1_t = tanuki::wrap<any_iface, custom_config1>;

    wrap1_t w1(nullptr);

    std::cout << std::boolalpha;
    std::cout << "Is w1 using static storage? " << has_static_storage(w1) << '\n';
Is w1 using static storage? true

On the other hand, if we try to store 2 pointers instead of 1, we can verify that the wrap class switches to dynamic storage:

    struct two_ptrs {
        void *p1 = nullptr;
        void *p2 = nullptr;
    };

    wrap1_t w2(two_ptrs{});

    std::cout << "Is w2 using static storage? " << has_static_storage(w2) << '\n';
Is w2 using static storage? false

One limitation of the holder_size helper is that it requires the interface (i.e., the second parameter - any_iface in the example) to have an implementation for the value type (i.e., the first parameter - void * in the example).

What about alignment?#

The alignment of the static storage within wrap can be configured via the config::static_align config option. By default, the maximum alignment supported on the platform (that is, alignof(std::max_align_t)) is used. This ensures that objects of any type can be stored in static storage (provided of course that there is enough space).

In some cases, it may be convenient to specify a smaller config::static_align value in order to reduce the memory footprint of a wrap. Note that config::static_align is subject to the usual constraints for an alignment value - specifically, the value must be a power of two.

If a config::static_align value smaller than the default one is specified, and the alignment required for a specific value type T exceeds it, then the wrap class will automatically switch to dynamic storage.

Analogously to config::static_size, config::static_align accounts for the alignment constraints imposed not only by the value type but also by the type-erasure machinery. That is, if you specify a config::static_align of 8, that does not necessarily mean that it is possible to store in static storage values with an alignment of 8 or less, as the type-erasure machinery might impose stricter alignment constraints.

Similarly to holder_size, the holder_align helper can be used to compute the alignment requirement of wrap’s static storage for a specific value type T. Let us see a simple example.

First, we define a new configuration instance in which we specify both a custom static size and a custom alignment:

    constexpr auto custom_config2 = tanuki::config<>{.static_size = tanuki::holder_size<void *, any_iface>,
                                                     .static_align = tanuki::holder_align<void *, any_iface>};

We can then define a new wrap type using the custom_config2 settings, and verify that it can store pointers in static storage:

    using wrap2_t = tanuki::wrap<any_iface, custom_config2>;

    wrap2_t w3(nullptr);

    std::cout << "Is w3 using static storage? " << has_static_storage(w3) << '\n';
Is w3 using static storage? true

Finally, we can verify how specifying a smaller alignment can (at least on some platforms) reduce the memory footprint of the wrap class:

    std::cout << "sizeof(wrap1_t) is " << sizeof(wrap1_t) << ", sizeof(wrap2_t) is " << sizeof(wrap2_t) << '\n';
sizeof(wrap1_t) is 32, sizeof(wrap2_t) is 24

Full code listing#

#include <iostream>

#include <tanuki/tanuki.hpp>

template <typename Base, typename Holder, typename T>
struct any_iface_impl : public Base {
};

struct any_iface {
    template <typename Base, typename Holder, typename T>
    using impl = any_iface_impl<Base, Holder, T>;
};

inline constexpr auto custom_config1 = tanuki::config<>{.static_size = tanuki::holder_size<void *, any_iface>};

int main()
{
    using wrap1_t = tanuki::wrap<any_iface, custom_config1>;

    wrap1_t w1(nullptr);

    std::cout << std::boolalpha;
    std::cout << "Is w1 using static storage? " << has_static_storage(w1) << '\n';

    struct two_ptrs {
        void *p1 = nullptr;
        void *p2 = nullptr;
    };

    wrap1_t w2(two_ptrs{});

    std::cout << "Is w2 using static storage? " << has_static_storage(w2) << '\n';

    constexpr auto custom_config2 = tanuki::config<>{.static_size = tanuki::holder_size<void *, any_iface>,
                                                     .static_align = tanuki::holder_align<void *, any_iface>};

    using wrap2_t = tanuki::wrap<any_iface, custom_config2>;

    wrap2_t w3(nullptr);

    std::cout << "Is w3 using static storage? " << has_static_storage(w3) << '\n';

    std::cout << "sizeof(wrap1_t) is " << sizeof(wrap1_t) << ", sizeof(wrap2_t) is " << sizeof(wrap2_t) << '\n';
}