Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / programming / compression

Wild West Coding: E.B.C.O. Compression

4.64/5 (3 votes)
28 Aug 2021CPOL12 min read 5.9K   21  
A reformulation of compressed pair into a type list
This article aims to provide an alternative to both ebco base and compressed pair. This amalgamation of the two will produce a generalized compressed type list that benefits from empty base class optimization, when appropriate, and serves as a reference wrapper to the types of the formed type list.

Introduction

If you ever had a type, or several types, that you needed to take up the least space possible, then you may have considered std's ebco base or compressed pair. As both are underscored to denote internal use only, subject to change, not for public consumption, that makes them less than ideal on that fact alone. In addition to this, the inclusion of multiple types beyond what constitutes a pair, leads to a series of long call chains and member accesses. With this come the need to think out which types get compressed as a second of which first. This requires an in-depth understanding of the compression itself to layout a format which achieves the most efficient packing arrangement. The creation of a variable length, compressed, type list aims to address these concerns.

Background

Empty base class optimization is an optimization performed on types that have no non static members or virtual functions. Normally, the size of any object must be at least one to guarantee that distinct objects of the same type are always distinct. This does not apply to base class objects though, allowing their size to be optimized out completely. This can be used as a compression technique, similar to boost's compressed pair.

The Design

The general design of the compressed type is a class that takes a variable number of types, one or more, and exposes only a single type. That type will expose only one member. That member will be a function call get. This will take a type as a template argument, and return a reference to that type. The reference will be able to be assigned, both from or to, from the call to get. The call to get will incur no function call overhead, and be paramount to the use of a reference variable. The type exposed by compressed will be constructible with either a constructor that takes in values for each type, or a default constructor which default constructs each given type. All error handling provided will be in the form of compile time error messages and be complete. That is, if it compiles without errors, it shall run without errors.

The first thing we will do is set up the definition of the compressed type which will be a class, as they default to private, and set up some error conditions. The first of which is the design choice of using static if (if constexpr). The use of which is not absolutely necessary, but saves the need for far less helper types, and makes the code clearer in its meaning. For those who are not familiar with if constexpr, only the code present in the scope of an expression that statically evaluates to true gets compiled. This is a C++17 language extension and its usage will limit the code to a minimum of that. If this solution becomes needed in earlier versions of the language, I will be glad to modify the code on an as needed basis. For our macro evaluated errors and warnings, we will be using a characteristic, the code to which is explained here. This will also allow us an aggressive form of both constexpr and inline. These will ensure our types construction, destruction, and usage, do not incur any overhead. These also are explained in the previous article and are beyond the scope of this article. In addition to that error, we will set up an empty types error. Instead of handling an empty compressed object, we will simply forbid it.

C++
template<typename... Types>
class compressed
{
 
#if !defined(__cpp_constexpr)    
    JOE((error("C++ if constexpr required.")))
#elif defined(__cpp_constexpr) && !(__cpp_constexpr >= 201603L)     
#   ifndef COMPRESSED_SILENCE_STATIC_IF_WARNING
        JOE((warning("C++ if constexpr required, 
        check your compiler's documentation for availability. 
        Silence with : COMPRESSED_SILENCE_STATIC_IF_WARNING")))
#   endif // COMPRESSED_SILENCE_STATIC_IF_WARNING
#endif
    static_assert((sizeof...(Types) > 0), 
                   "The given types for compressed must not be empty");

All of the helper types and traits will be contained within the compressed class. As some of the solution relies on casting to make function calls, the types must be public to access the calls. We can use structs that default to public and allow each of that types members to be public, to ease this. All of these helper types although public in their definition, will be private in their access from outside of compressed. This will ensure they don't end up being misused.

C++
template <class _Ty>
using remove_cvref_t = typename std::remove_cv
                       <typename std::remove_reference<_Ty>::type>::type;
 
template <typename Type, typename... Types>
using is_any_of = std::disjunction<std::is_same<Type, Types>...>;
 
template <typename _Type, typename _Other>
using EnableIf = typename std::enable_if<!std::is_same
                 <remove_cvref_t<_Other>, _Type>::value, int>::type;
 
template< typename ..._Types>
struct AutoPair;
template< typename ..._Types>
class PairCat;

The first helper type we will create is the EbcoBase struct. It's the job of this type to decide if a given type can benefit from empty base class optimization, and provide a single interface in either occurrence. It differs from std's ebco base in that it provides a default constructor, necessary to provide default construction of the compressed type. Copy and move constructors are added, necessary to forward the given values along the constructor chains. The value constructor is protected by an enable if that serves to make sure it doesn't get used as a copy or move constructor. It also has a static assert to clarify constructor issues when the wrong value type is given for construction. This seems to refine some of the errors produced by the compiler as well. In the other types that inherit from this one, we simply forward a varying amount of args to simplify constructor error messages. All the members are decorated with the characteristic constexpr, save the destructor. This could be constexpr in C++20 and onwards, but we are using C++17 as our limiting factor. So instead, we use a forced inline. Even still, only the individual types constructors and destructors ever run. Not any of our helper types constructors and destructors. Our single call get_value, provides const and non const overloads. All of our helper types will follow this pattern:

C++
template<typename _Type, bool = 
         !std::is_final<_Type>::value && std::is_empty<_Type>::value >
struct EbcoBase : private _Type
{ // Empty Base Class Optimization, active
 
    JOE((constexpr)) EbcoBase() noexcept = default;
    JOE((inline))   ~EbcoBase() noexcept = default;
 
    template <class _Other, EnableIf<EbcoBase, _Other> = 0>
    JOE((constexpr))
        explicit EbcoBase(_Other&& _Val) noexcept
        (std::is_nothrow_constructible<_Type, _Other>::value)
        : _Type(std::forward<_Other>(_Val)) {
        static_assert(std::is_constructible<_Type, _Other>::value, 
        "The given type can not be constructed with the provided value");
    }
 
    JOE((constexpr)) EbcoBase(const EbcoBase&) noexcept = default;
    JOE((constexpr)) EbcoBase(EbcoBase&&) noexcept = default;
 
    JOE((constexpr)) _Type& get_value() noexcept { return *this; }
    JOE((constexpr)) const _Type& get_value() const noexcept { return *this; }
};
template<typename _Type>
struct EbcoBase<_Type, false>
{ // Empty Base Class Optimization, inactive
 
    JOE((constexpr)) EbcoBase() noexcept = default;
    JOE((inline))   ~EbcoBase() noexcept = default;
 
    template <class _Other, EnableIf<EbcoBase, _Other> = 0>
    JOE((constexpr))
        explicit EbcoBase(_Other&& _Val) 
        noexcept(std::is_nothrow_constructible<_Type, _Other>::value)
        : value(std::forward<_Other>(_Val)) {
        static_assert(std::is_constructible<_Type, _Other>::value, 
        "The given type can not be constructed with the provided value");
    }
 
    JOE((constexpr)) EbcoBase(const EbcoBase&) noexcept = default;
    JOE((constexpr)) EbcoBase(EbcoBase&&) noexcept = default;
 
    JOE((constexpr)) _Type& get_value() noexcept { return value; }
    JOE((constexpr)) const _Type& get_value() 
                                    const noexcept { return value; }
private:
    _Type value;
};

The next type, AutoPair, that we build will be responsible for using multiple inheritance to glue the EbcoBases together into a single type. It exposes a single call get_type, whose job it is to build a cast to the appropriate EbcoBase of the type given as a template parameter of get_type. That calls the corresponding call to get_value from the matching EbcoBase, returning the right reference to that type. If the given type is not one of the types the pair was created with, then the cast will fail. We don't protect this just yet, as other calls will be made through this in upper layers.

Although there is no problem using same types with multiple inheritance, there arises an issue upon retrieval of the types value. When composing the cast to EbcoBase, it will cast to the same type for any reoccurring types, returning the same value for all. For this reason, we forbid reoccurring types, as the second or more instances would not be able to be retrieved with this method. The static assert ensures compilation will break for repeating types.

We could just expose AutoPair and use that as our finishing type as it is capable of handling an indefinite amount of types. But, foreach empty class that we inherit from after the first, the compiler falls back to assigning a minimum of one byte per type so that each subobject remains distinct. As ebco compression is subject to packing and alignment specifiers, this could add whatever the minimum specifiers size is for one empty class. As an example, say we have an alignment of one and a packing of one. We have three types, two empty classes and a pointer. On x64 a pointer is eight bytes, our first empty class would benefit from ebco adding no size, and the second would add one byte. This would add up to nine. If the alignment was specified on just one of the classes as eight, than our total size would bounce from nine to sixteen. As that would be the next eight byte boundary. As the inheritance is multiple, the alignment specifiers become inherited by the compressed_t type. A packing specifier of eight would have the same effect, as it would add padding just the same as an alignment specifier. This is why compression of more than two types requires making pairs of type and pair. More on that later.

C++
template<typename _Type>
struct AutoPair<_Type> : protected EbcoBase<_Type>
{
    JOE((constexpr)) AutoPair() noexcept = default;
    JOE((inline))   ~AutoPair() noexcept = default;
 
    JOE((constexpr)) AutoPair(const AutoPair&) noexcept = default;
    JOE((constexpr)) AutoPair(AutoPair&&) noexcept = default;
 
    template <class _Other, EnableIf<AutoPair, _Other> = 0>
    JOE((constexpr)) explicit AutoPair(_Other&& _Arg) 
    noexcept(std::is_nothrow_constructible<EbcoBase<_Type>, _Other>::value)
        : EbcoBase<_Type>(std::forward<_Other>(_Arg)) {};
 
    template<typename Type>
    JOE((constexpr)) Type& get_type() noexcept {
        return static_cast<EbcoBase<Type>*>(this)->get_value();
    }
    template<typename Type>
    JOE((constexpr)) const Type& get_type() const noexcept {
        return static_cast<const EbcoBase<Type>*>(this)->get_value();
    }
};
template<typename _Type, typename ..._Types>
struct AutoPair<_Type, _Types...> : 
protected EbcoBase<_Type>, public AutoPair< _Types...>
{
    static_assert(!is_any_of<_Type, _Types...>::value, 
    "The given types for compression must be unique.");
 
    JOE((constexpr)) AutoPair() noexcept = default;
    JOE((inline))   ~AutoPair() noexcept = default;
 
    JOE((constexpr)) AutoPair(const AutoPair&) noexcept = default;
    JOE((constexpr)) AutoPair(AutoPair&&) noexcept = default;
 
    template <typename _Other, typename ..._Others, EnableIf<AutoPair, _Other> = 0>
    JOE((constexpr)) explicit AutoPair(_Other&& _Arg, _Others&&... _Args) 
        noexcept(std::is_nothrow_constructible<EbcoBase<_Type>, _Other>::value && 
        std::is_nothrow_constructible<AutoPair< _Types...>, _Others...>::value)
        : EbcoBase<_Type>(std::forward<_Other>(_Arg))
        , AutoPair< _Types...>(std::forward<_Others>(_Args)...) {};
 
    template<typename Type>
    JOE((constexpr)) Type& get_type() noexcept {
        return static_cast<EbcoBase<Type>*>(this)->get_value();
    }
    template<typename Type>
    JOE((constexpr)) const Type& get_type() const noexcept {
        return static_cast<const EbcoBase<Type>*>(this)->get_value();
    }
};

Now we have a way of retrieving our values by type, and the suitable compression is applied automatically. Next we have to merge an indefinite amount of pairs together and provide a unified access point. This is the responsibility of PairCat. It will expose the function call get as a means of accessing type values. For a single type, we inherit from EbcoBase directly. Our get call builds the static cast to EbcoBase pointer and calls get_value, returning the value of the given type. In this layer, we protect any casting problems with a static assert, which also helps to clarify the error as to why the type is failing.

C++
template<typename _Type>
class PairCat<_Type> : private EbcoBase<_Type>
{
public:
    JOE((constexpr)) PairCat() noexcept = default;
    JOE((inline))   ~PairCat() noexcept = default;
 
    JOE((constexpr)) PairCat(const PairCat&) noexcept = default;
    JOE((constexpr)) PairCat(PairCat&&) noexcept = default;
 
    template <class _Other, EnableIf<PairCat, _Other> = 0>
    JOE((constexpr)) explicit PairCat(_Other&& _Arg) 
    noexcept(std::is_nothrow_constructible<EbcoBase<_Type>, _Other>::value)
        : EbcoBase<_Type>(std::forward<_Other>(_Arg)) {};
 
    template<typename Type>
    JOE((constexpr)) Type& get() noexcept {
        static_assert(std::is_same<Type, _Type>::value, 
        "The given type for get must be one of the compression types.");
        return static_cast<EbcoBase<Type>*>(this)->get_value();
    }
    template<typename Type>
    JOE((constexpr)) const Type& get() const noexcept {
        static_assert(std::is_same<Type, _Type>::value, 
        "The given type for get must be one of the compression types.");
        return static_cast<const EbcoBase<Type>*>(this)->get_value();
    }
private:
    template<typename...>
    friend class PairCat;
};

For two objects, we simply inherit from AutoPair and wrap a call to get_type inside get. We inherit AutoPair privately, cutting off any calls to get_type and get_value in the inheritance chain leaving only get as a means of access. This template friends with itself to allow other specializations to access this particular get_type call from that particular specialization, as all will be friended with each other.

C++
template<typename _Ty1, typename _Ty2>
class PairCat< _Ty1, _Ty2> : private AutoPair<_Ty1, _Ty2>
{
    static_assert(!std::is_same<_Ty1, _Ty2>::value, 
    "The given types for compression must be unique.");
    using Base = AutoPair<_Ty1, _Ty2>;
public:
    using Base::Base;
 
    JOE((inline)) ~PairCat() noexcept = default;
 
    JOE((constexpr)) PairCat(const PairCat&) noexcept = default;
    JOE((constexpr)) PairCat(PairCat&&) noexcept = default;
 
    template<typename Type>
    JOE((constexpr)) Type& get() noexcept {
        static_assert(is_any_of<Type, _Ty1, _Ty2>::value, 
        "The given type for get must be one of the compression types.");
        return static_cast<Base*>(this)->get_type<Type>();
    }
    template<typename Type>
    JOE((constexpr)) const Type& get() const noexcept {
        static_assert(is_any_of<Type, _Ty1, _Ty2>::value, 
        "The given type for get must be one of the compression types.");
        return static_cast<const Base*>(this)->get_type<Type>();
    }
private:
    template<typename...>
    friend class PairCat;
};

It seems we have added another type just to wrap AutoPair with no new functionality. But wait, there's more! Our next specialization will consume three types and create a pair of type and pair. In our get call, we have to dispatch our call to get_type differently. If the type matches the first type, then we can use the get_type call directly from the cast to base. If it is either of the other two types, then we first must retrieve the inner pair or "PairBase", and call get_type with that. This is where the use of the static if comes in handy.

C++
template<typename _Ty1, typename _Ty2, typename _Ty3>
class PairCat< _Ty1, _Ty2, _Ty3> : private AutoPair<_Ty1, AutoPair< _Ty2, _Ty3 > >
{
    static_assert(!is_any_of<_Ty1, _Ty2, _Ty3>::value, 
    "The given types for compression must be unique.");
    using PairBase = AutoPair< _Ty2, _Ty3 >;
    using Base = AutoPair<_Ty1, PairBase >;
public:
    template <typename _Other1, typename _Other2, typename _Other3>
    JOE((constexpr)) explicit PairCat(_Other1&& _Arg1, 
    _Other2&& _Arg2, _Other3&& _Arg3) 
    noexcept(std::is_nothrow_constructible<Base>::value)
        : Base(std::forward<_Other1>(_Arg1)
            , std::forward< PairBase >(
                PairBase(std::forward<_Other2>(_Arg2), 
                std::forward<_Other3>(_Arg3))
                )
        ) { }
    JOE((constexpr)) PairCat() noexcept = default;
    JOE((inline))   ~PairCat() noexcept = default;
 
    JOE((constexpr)) PairCat(const PairCat&) noexcept = default;
    JOE((constexpr)) PairCat(PairCat&&) noexcept = default;
 
    template<typename Type>
    JOE((constexpr)) Type& get() noexcept {
        static_assert(is_any_of<Type, _Ty1, _Ty2, _Ty3>::value, 
        "The given type for get must be one of the compression types.");
        if constexpr (std::is_same<Type, _Ty1>::value)
            return static_cast<Base*>(this)->template get_type<Type>();
        else
            return static_cast<Base*>(this)->template 
            get_type<PairBase>().template get_type<Type>();
    }
    template<typename Type>
    JOE((constexpr)) const Type& get() const noexcept {
        static_assert(is_any_of<Type, _Ty1, _Ty2, _Ty3>::value, 
        "The given type for get must be one of the compression types.");
        if constexpr (std::is_same<Type, _Ty1>::value)
            return static_cast<const Base*>(this)->template get_type<Type>();
        else
            return static_cast<const Base*>(this)->template 
            get_type<PairBase>().template get_type<Type>();
    }
private:
    template<typename...>
    friend class PairCat;
};

The next specialization will be a recursive template, popping off types two at a time. This will handle all other amounts of types greater than three. It will inherit from an AutoPair of type and AutoPair of type and PairCat. The use of the PairCat for the innermost pair extends the recursion indefinitely. This template will only be hit if there are four or more types. It will pop off two of them, and continue on to the next PairCat. Our get call will dispatch as follows: If the type is the same as type one, than call get_type with that type. If the type is the same as type two, than first retrieve the inner pair, than call get_type from that. All other types will be handled by retrieving the inner pair first, the inner most pair second, than calling get from that to use the dispatch provided by that PairCat.

C++
template<typename _Ty1, typename _Ty2, typename ..._Types>
class PairCat< _Ty1, _Ty2, _Types...> : private AutoPair<_Ty1, 
AutoPair< _Ty2, PairCat<_Types...> > >
{
    static_assert(!is_any_of<_Ty1, _Ty2, _Types...>::value, 
    "The given types for compression must be unique.");
    using CatBase = PairCat<_Types...>;
    using PairBase = AutoPair< _Ty2, CatBase >;
    using Base = AutoPair<_Ty1, PairBase >;
public:
    template <typename _Other1, typename _Other2, typename ..._Others>
    JOE((constexpr)) explicit PairCat(_Other1&& _Arg1, 
    _Other2&& _Arg2, _Others&& ..._Args) 
    noexcept(std::is_nothrow_constructible<Base>::value)
        : Base(std::forward<_Other1>(_Arg1)
            , std::forward< PairBase >(
                PairBase(std::forward<_Other2>(_Arg2),
                    std::forward<PairCat<_Others...> 
                    >(PairCat<_Others...>(std::forward<_Others>(_Args)...))
                )
                )
        ) { }
    JOE((constexpr)) PairCat() noexcept = default;
    JOE((inline))   ~PairCat() noexcept = default;
 
    JOE((constexpr)) PairCat(const PairCat&) noexcept = default;
    JOE((constexpr)) PairCat(PairCat&&) noexcept = default;
 
    template<typename Type>
    JOE((constexpr)) Type& get() noexcept {
        static_assert(is_any_of<Type, _Ty1, _Ty2, _Types...>::value, 
        "The given type for get must be one of the compression types.");
        if constexpr (std::is_same<Type, _Ty1>::value)
            return static_cast<Base*>(this)->template get_type<Type>();
        else if constexpr (std::is_same<Type, _Ty2>::value)
            return static_cast<Base*>(this)->template 
            get_type<PairBase>().template get_type<Type>();
        else
            return static_cast<Base*>(this)->template 
            get_type<PairBase>().template get_type<CatBase>().template get<Type>();
    }
    template<typename Type>
    JOE((constexpr)) const Type& get() const noexcept {
        static_assert(is_any_of<Type, _Ty1, _Ty2, _Types...>::value, 
        "The given type for get must be one of the compression types.");
        if constexpr (std::is_same<Type, _Ty1>::value)
            return static_cast<const Base*>(this)->template get_type<Type>();
        else if constexpr (std::is_same<Type, _Ty2>::value)
            return static_cast<Base*>(this)->template 
            get_type<PairBase>().template get_type<Type>();
        else
            return static_cast<const Base*>(this)->template 
            get_type<PairBase>().template get_type<CatBase>().template get<Type>();
    }
private:
    template<typename...>
    friend class PairCat;
};

Now all that is left is to expose our PairCat as a finished type through a public using statement/typedef.

C++
public:
    using type = PairCat<Types...>;
};
 
// uses empty base class optimization compression
template<typename... Types>
using compressed_t = typename compressed<Types...>::type;

Note that PairCat and all the other helper types and traits are all private types of compressed. Only the finished type is ever public. With the addition of a helper type compressed_t that draws out the using statement, our compressed type is finished.

Using the Code

There are many use cases for the compressed_t type. Such as a member of a class, or as a single variable, or through direct inheritance. A compressed_t with a single type will inherit from EbcoBase. This would allow an empty base class optimization dynamically, if the type given was suitable, and make it a member otherwise. As a member of a class, it can be used to compress multiple objects into the minimum space possible for any set of given types. As a variable, well you're just being stingy, but it can be done. Let's look at an std snippet as it stands.

C++
using MyAlloc = Rebind_alloc_t<Alloc, Ref_count_resource_alloc>;
 
virtual void _Destroy() noexcept override {  
    _Mypair._Get_first()(_Mypair._Myval2._Myval2); // deleter in first, 
                                                   // resource in second's second
}
 
virtual void _Delete_this() noexcept override { 
    MyAlloc Al = _Mypair._Myval2._Get_first();     // allocator in second's first
    this->~_Ref_count_resource_alloc();
    std::_Deallocate_plain(Al, this);
}
 
std::_Compressed_pair<Dx, std::_Compressed_pair<Myalty, 
Resource>> _Mypair; // pair of type and pair

This is with only three types. With each additional type, the syntax becomes increasingly more difficult. Five types or more and you have just a plain mess. Now let's look at the alternative syntax.

C++
using MyAlloc = Rebind_alloc_t<Alloc, Ref_count_resource_alloc>;
 
virtual void _Destroy() noexcept override {  
    comp.get<Dx>()(comp.get<Resource>());          // deleter in get,
                                                   // resource in get
}

virtual void _Delete_this() noexcept override {  
    MyAlloc Al = comp.get<MyAlloc>();               // allocator in get
    this->~_Ref_count_resource_alloc();
    std::_Deallocate_plain(Al, this);
}
 
joe::compressed_t<Dx, MyAlloc, Resource> comp;  // type list

Normally, using a compressed pair, you would have to think out which pair goes into which member variable. As the second of compressed pair is always a member variable. With compressed_t, the ebco is dynamic, although under the hood, we are still making pairs of type and pair. But the deeper understanding of compressing an empty type against a type that is not empty, as an empty type first, then all others as a member variable of empty type and type... This can be avoided with a simple rule of thumb. All empty types come first in the list (in any order), while all non empty types come last in the list (in any order). Thus....

C++
compressed_t<Dx, MyAlloc, MyExtra, int*, double*> comp;

is the same size and access as...

C++
compressed_t<MyAlloc, MyExtra, Dx, double*, int*> comp;

Now let's look at the difference in construction.

C++
_Ref_count_resource_alloc(Resource Px, Dx Dt, const Alloc& Ax)
    : _Ref_count_base(),
      _Mypair(std::_One_then_variadic_args_t{}, std::move(Dt), 
              std::_One_then_variadic_args_t{}, Ax, Px) {}
C++
_Ref_count_resource_alloc(Resource Px, Dx Dt, const Alloc& Ax)
    : _Ref_count_base(),
      comp(std::move(Dt), Ax, Px) {}

Far simpler to construct. During the construction of a compressed_t, none of the helper types constructors are ever run. Instead, all of the given types constructors are run in place, and only their value is passed on to compressed_t. Although, if an undecorated forward is used, you will see the results of the perfect forwarding in debug builds. The use of a decorated forward will change this. Otherwise, it gets optimized out completely in release builds, or anything O2 or higher. I only mention this as some companies have reason to release debug builds only. This is the same with the helper types destructors. Only the individual given types destructors are only ever run. And the overhead of the call to get is exactly the same as using an in place reference variable. That is to say there is no function call overhead. As they are treated as refs, and if their values can be proven at compile time, there is an opportunity of them being optimized out as well. That makes this a highly efficient solution in run time usage as well as size.

For our proof of claim...

C++
#pragma pack(push, 1)
#include <type_traits>
#include <iostream>
#include <stdexcept>
#include <memory>
 
#include "compressed.h"
 
using MyAlloc = std::allocator<int>;

// this type was added not for need, but to show off both the extent of compression, 
// and simplicity of syntax
using MyCAlloc = std::allocator<char>;
using Dx = std::default_delete<int>;

// this type was added for the same reason as MyCAlloc
using DxC = std::default_delete<char>;
using Resource = int*;
 
int main()
{
    // 5-types, the "mess threshold"
  joe::compressed_t<Dx, DxC, MyAlloc, MyCAlloc, 
  Resource> comp(Dx{}, DxC{}, MyAlloc{}, MyCAlloc{}, new int(10));
  //joe::compressed_t<Dx, DxC, MyAlloc, MyCAlloc, Resource> comp;
 
  std::cout << "sizeof of compressed_t : 
  " << sizeof(decltype(comp)) << std::endl; 
  // size : should always be confirmed by a runtime test, 
  // after all alignments and packing have been applied

    if (comp.get<Resource>() != nullptr) {
        // sanity check is unneeded, but does prove the values 
        // are being passed along the constructor chains,
        // this was a problem when trying to support default construction. 
        // Tweaking the enable if and adding copy and move to the helper types 
        // corrected this and now allows for both value construction, 
        // and default construction, provided the types themselves do.
        
        std::cout << "valueof of compressed_t Resource : " 
                  << *comp.get<Resource>() << std::endl;
        comp.get<Dx>()(comp.get<Resource>());
        comp.get<Resource>() = comp.get<MyAlloc>().allocate(1);
        (*comp.get<Resource>()) = 11;
        std::cout << "valueof of compressed_t Resource : "
                  << *comp.get<Resource>() << std::endl;
        comp.get<Dx>()(comp.get<Resource>());

        // these are some of the common things a custom smart pointer might do, 
        // we didn't wrap them in a type, so that we don't get lost in optimizations
        // that would normally be performed on the custom type 
        // vs optimizations on the compressed_t.
    }
    else {
        comp.get<Resource>() = comp.get<MyAlloc>().allocate(1);
        (*comp.get<Resource>()) = 11;
        std::cout << "valueof of compressed_t Resource : " 
                  << *comp.get<Resource>() << std::endl;
        comp.get<Dx>()(comp.get<Resource>());
    }
    std::cin.ignore();
    return 0;
}
#pragma pack(pop)
#undef JOE

Here is a link to Compiler Explorer to play with the code sample:

For details on compiling with MSVC on CE, refer to the wild west article MACROs. If you are using it directly on MSVC, no changes will need to be made. Otherwise, our target platforms are Windows, Linux, and Mac. Target compilers are GCC, Clang, and MSVC.

Points of Interest

This article was written in response to the rumor that the ISO standards committee was toying with using tuple as a replacement for compressed pair. This is offered as an alternative. As it uses the lesser known std::get<Type>() syntax, it is not that far from using a tuple. Tuple, however, is not required to use ebco compression. This offers the best of both. A type list, the get syntax, and ebco compression. But it would be an understatement to say that one average user of C++, a "regular joe", could not alone influence the committee. This is where you as the reader, and member of the C++ community can effect change. How do you feel it stacks up. All comments both ugly and other wise are welcome. I only ask that if you do have a design dislike, that you post a possible alternative. In that way, we can evolve the solution together.

History

  • 28th August, 2021: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)