C++ Logo

std-proposals

Advanced search

[std-proposals] New method 'common_base' for 'std::variant'

From: Frederick Virchanza Gotham <cauldwell.thomas_at_[hidden]>
Date: Tue, 6 Sep 2022 13:01:59 +0100
I program microcontrollers in my day job, so I'm weary of using
dynamic memory allocation. I avoid it wherever possible.

Currently I have an "interface class" to handle COM ports:

        class IRS232 {
        public:
            virtual void ConfigInputBuffer(char *const arg_buf,
std::size_t const arg_buflen) = 0;
            virtual bool Open(char const *arg_str_portname, long
unsigned arg_baud) = 0;
            virtual void Close(void) = 0;
            virtual std::size_t Read(void) = 0;
            virtual void Send(char const*,std::size_t) = 0;

            virtual ~IRS232(void) {}
        };

I have two classes that derive from IRS232, like this:

        class RS232_Chunked : public IRS232 { . . . };
        class RS232_Instant : public IRS232 { . . . };

So normally in a Desktop C++ program, if I wanted to decide at runtime
what kind of RS232 processing I want, I would do:

        IRS232 *g_pcoms = nullptr;

        void Initialise_Comms(bool const chunked = false)
        {
            g_pcoms = chunked ? new RS232_Chunked : new RS232_Instant;
        }

But because I'm on a microcontroller here, I don't want dynamic memory
allocation. I want to use the same piece of memory for either an
RS232_Chunked or an RS232_Instant. Before C++17, I would have used
"std::aligned_union" for this purpose, but now instead we have
"std::variant".

So I can do the following:

        std::variant<std::nullopt_t,RS232_Chunked,RS232_Instant>
g_coms( std::in_place_index<0u>, std::nullopt );

        IRS232 *g_pcoms = nullptr;

        void Initialise_Comms(bool const chunked = false)
        {
            if ( chunked )
            {
                g_coms.emplace<1u>();
                g_pcoms = &std::get<1u>(g_coms);
            }
            else
            {
                g_coms.emplace<2u>();
                g_pcoms = &std::get<2u>(g_coms);
            }
        }

In order to simply this, I think that the "std::variant" class should
have a method called "common_base", which could be used as follows:

        std::variant<std::nullopt_t,RS232_Chunked,RS232_Instant>
g_coms( std::in_place_index<0u>, std::nullopt );

        IRS232 *g_pcoms = nullptr;

        void Initialise_Comms(bool const chunked = false)
        {
            if ( chunked ) g_coms.emplace<1u>();
                      else g_coms.emplace<2u>();

            g_pcoms = g_coms.common_base<IRS232>();
        }

The method, "common_base", would have the following compile-time
characteristics/constraints:
(1) If the first type in the variant is 'std::nullopt_t', and if the
    variant object currently hosts a 'std::nullopt_t', then return a nullptr
(2) All other types in the variant must have T as a base class

The implementation would be something like as follows:
#include <variant> // variant
#include <type_traits> // is_same_v, is_base_of_v
#include <optional> // nullopt_t
#include <tuple> // tuple_element_t
#include <utility> // in_place_t

#define CASE_FOR_VARIANT(i) \
case i##u: if constexpr ( sizeof...(Types) > i##u ) \
           { \
                static_assert( std::is_base_of_v< Base, \
                               std::tuple_element_t< i##u, \
                                   std::tuple<Types...> > >, \
                               "Type " #i " is not derived from " \
                               "the common base class" ); \
                                                                       \
                return &get< i##u >(*this); \
           }

template<class... Types>
class my_variant : public std::variant<Types...> {
public:

    using std::variant<Types...>::variant;

    template<class Base>
    Base *common_base(void) noexcept
    {
        using std::get;

        switch ( this->index() )
        {
        case 0u:
            using FirstType = std::tuple_element_t< 0u, std::tuple<Types...> >;
            if constexpr ( std::is_same_v<std::nullopt_t,FirstType> )
return nullptr;
                    else return &get<0u>(*this);

        CASE_FOR_VARIANT( 1); CASE_FOR_VARIANT( 2); CASE_FOR_VARIANT( 3);
        CASE_FOR_VARIANT( 4); CASE_FOR_VARIANT( 5); CASE_FOR_VARIANT( 6);
        CASE_FOR_VARIANT( 7); CASE_FOR_VARIANT( 8); CASE_FOR_VARIANT( 9);
        CASE_FOR_VARIANT(10); CASE_FOR_VARIANT(11); CASE_FOR_VARIANT(12);
        CASE_FOR_VARIANT(13); CASE_FOR_VARIANT(14); CASE_FOR_VARIANT(15);
        CASE_FOR_VARIANT(16); CASE_FOR_VARIANT(17); CASE_FOR_VARIANT(18);
        CASE_FOR_VARIANT(19); CASE_FOR_VARIANT(20); CASE_FOR_VARIANT(21);
        CASE_FOR_VARIANT(22); CASE_FOR_VARIANT(23); CASE_FOR_VARIANT(24);
        CASE_FOR_VARIANT(25); CASE_FOR_VARIANT(26); CASE_FOR_VARIANT(27);
        CASE_FOR_VARIANT(28); CASE_FOR_VARIANT(29); CASE_FOR_VARIANT(30);
        CASE_FOR_VARIANT(31);
        }

        return nullptr; // just to suppress compiler warning
    }
};

#include <iostream> // cout

struct Mammal { virtual void Speak(void) = 0; };
struct Dog : Mammal { void Speak(void) override { std::cout << "Dog\n"; } };
struct Cat : Mammal { void Speak(void) override { std::cout << "Cat\n"; } };

class Fish {};

my_variant<std::nullopt_t,Dog,Cat> my_mammal( std::in_place_index<0u>,
std::nullopt );

int main(void)
{
    my_mammal.emplace<2u>();

    Mammal *const p = my_mammal.common_base<Mammal>();

    if ( p )
    {
        p->Speak();
    }
    else
    {
        std::cout << "We've got a nullptr\n";
    }

    // If we add in a Fish, then we can no longer use common_base<Mammal>
    my_variant<std::nullopt_t,Dog,Cat,Fish> some_other_object(
std::in_place_index<0u>, std::nullopt );
}

Received on 2022-09-06 12:02:12