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 );
}
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