C++ Logo

std-proposals

Advanced search

[std-proposals] Halfway between 'variant' and 'any'

From: Frederick Virchanza Gotham <cauldwell.thomas_at_[hidden]>
Date: Mon, 2 Jan 2023 16:39:11 +0000
It would be very useful to have a class that is halfway between 'variant'
and 'any'. What I want is a class with the versatility off 'any' except
that it allocates on the stack rather than on the heap.

A few months ago I proposed an alteration to the 'variant' class which
would allow it to be used as a pointer to a common base class which is
shared by all the specified derived classes. I proposed back then that you
would define an object as follows:

    variant_common_base<Base,Derived1,Derived2> my_object;

The drawback here is that the translation unit containing the definition of
'my_object' must be aware of, and contain the definitions of the classes,
Derived1 and Derived2. Also in the future if we add a third derived class,
we must revise the definition of 'my_object' as follows:

    variant_common_base<Base,Derived1,Derived2,Derived3> my_object;

Well with the new class I'm proposing today, which I call 'derivative', you
simply specify the base class:

    derivative<Base> my_object;

Then you can use it to host any object which satisfies the following two
criteria:
(1) The object publicly inherits from Base
(2) The object size is no greater than 4 * sizeof(Base) -- you can
customise this limit

Here's the code I've written so far. I wrote this on my Android phone and
so my compiler doesn't have concepts so I've used 'static_assert' instead.
Note that I keep track of the base pointer in order to accommodate complex
multiple virtual inheritance:

#include <cassert> // assert
#include <cstddef> // size_t, max_align_t
#include <new> // 'placement new'
#include <utility> // forward
#include <functional> // function
#include <type_traits> // instead of concepts

// My compiler doesn't have 'derived_from' so
// I've written it on the next line
template<class Derived,class Base>
bool constexpr derived_from =
  std::is_base_of_v<Base, Derived> &&
  std::is_convertible_v<const volatile Derived*, const volatile Base*>;

template<class T,
std::size_t buflen = 4u * sizeof(T)>
class derivative {
 T *p_base = nullptr;
 std::function<void(void)> destroy;
    alignas(std::max_align_t) char unsigned buf[buflen];

public:
    void reset(void)
    {
     if ( destroy ) destroy();
     destroy = nullptr;
     p_base = nullptr;
    }

    ~derivative(void) { reset(); }

    template<class U, class... Args>
    void emplace(Args&&... args)
    {
        static_assert(derived_from<U,T>);
        static_assert(buflen >= sizeof(U));

        reset();

        U *const p_derived = static_cast<U*>(static_cast<void*>(buf));

     ::new(p_derived) U( std::forward<Args>(args)... );

        this->p_base = p_derived;

       destroy = [p_derived](void)
         {
           p_derived->~U();
         };
    }

    T *operator->(void)
    {
       assert( nullptr != p_base );
       assert( static_cast<bool>(destroy) );
       return p_base;
    }

    bool has_value(void) { return static_cast<bool>(destroy); }
};

#include <sstream>
#include <fstream>

derivative<std::istream> s;

int main(void)
{
   s.emplace<std::ifstream>("c:\\autoexec.bat");
   s->clear();
   s.emplace<std::istringstream>();
   s->clear();
}

Note that std::function doesn't use dynamic allocation until the lambda
captures exceed 16 bytes (and I'm only using 8 bytes for one pointer).

A more simple class than 'derivative' could be made if we remove the common
Base class requirement. I would call this other class 'generic' and I'd
write it something like as follows:

#include <cstddef> // size_t, max_align_t
#include <new> // 'placement new'
#include <utility> // forward
#include <functional> // function
#include <typeinfo> // typeid, type_info
#include <exception> // exception

template<std::size_t buflen>
class generic {
    struct bad_generic_access : std::exception {};

    std::function<void(void)> destroy;

    alignas(std::max_align_t) char unsigned buf[buflen];

    std::type_info const *ti = nullptr;

public:
    void reset(void)
    {
       if ( nullptr == ti ) return;

       ti = nullptr;
       if ( destroy ) destroy();
       destroy = nullptr;
    }

    ~generic(void) { reset(); }

    template<class U, class... Args>
    void emplace(Args&&... args)
    {
        static_assert(buflen >= sizeof(U));

        reset();

        U *const p = static_cast<U*>(static_cast<void*>(buf));

       ::new(p) U( std::forward<Args>(args)... );

       destroy = [p](void)
         {
           if constexpr ( std::is_class_v<U> ) p->~U();
         };

       ti = &typeid(*p);
    }

    bool has_value(void) const { return nullptr != ti; }

    template<class U>
    U &get(void)
    {
       if ( false == has_value() ) throw bad_generic_access();

       if ( typeid(U) != *ti ) throw bad_generic_access();

       return *static_cast<U*>(static_cast<void*>(buf));
    }
};

#include <sstream>
#include <fstream>

generic<8192u> s;

int main(void)
{
   s.emplace<std::ifstream>("c:\\autoexec.bat");
   s.get<std::ifstream>().clear();
   s.emplace<std::istringstream>();
   s.get<std::istringstream>().clear();
   s.emplace<int>(5);

   s.get<int>() += 2;
}

So you can use 'generic' just like 'any' except it allocates on the stack
rather than the heap.

Received on 2023-01-02 16:39:13