Variants and Unions in C++

STRICT_VARIANT
A simpler variant in C++
Chris Beck
https://github.com/cbeck88/strict_variant
What is a variant?
A variant is a 
heterogenous container.
  
std
::
vector
<
T
>
 
 
many
 
objects of 
one 
type
  
std
::
variant
<
T
, 
U
, 
V
>
 
 
one 
object of 
any
 
of 
T
, 
U
, or 
V
AKA “tagged-union”, “typesafe union”
What is a union?
struct 
bar {      
// Size is sum of sizes,
  
short
 
a;        
// plus padding for alignment
  
float 
b;
  
double 
c;
};
union 
foo {       
// Size is max of sizes,
  
short
 
a;        
// alignment is max of alignments
  
float 
b;
  
double 
c;
};
union 
foo {       
  
short
 
a;        
  
float 
b;
  
double 
c;
};
int 
main
() {
  foo f;
  f.a = 
5
;
  f.a += 
7
;
  f.b = 
5
;
  f.b += 
.5f
;
}
Storing to union may change the 
active member
.
Reading inactive member may lead to
implementation-defined or undefined behavior!
Why would you use this?
Need to store 
several types 
of objects in a
collection, but 
no natural inheritance relation
.
Using an 
array
 of 
unions
, store objects
contiguously
, with 
very little memory wasted
.
Low-level signals / event objects
Messages matching various schema
struct 
SDL_KeyboardEvent 
{
  
Uint32
 type;       
// SDL_KEYDOWN or SDL_KEYUP
  
Uint8
 state;       
// SDL_PRESSED or SDL_RELEASED
  
SDL_Keysym 
keysym; 
// Represents the key that was pressed
};
struct 
SDL_MouseMotionEvent 
{
  
Uint32
 type;       
// SDL_MOUSEMOTION
  
Uint32
 state;      
// bitmask of the current button state
  
Sint32
 x;
  
Sint32
 y;
};
union
 
SDL_Event 
{
  
SDL_KeyboardEvent 
key;
  
SDL_MouseMotionEvent 
motion;
  ...
};
Why would you use this?
A 
variant
 is a type-safe alternative to a 
union
Prevents you from using inactive members
Ensures that destructors are called when the
active member changes – crucial for C++!
void 
print_variant
(
boost
::variant<
int
, 
float
, 
double
> v) {
  
if
 (
const int 
* i = 
boost
::get<
int
>(&v)) {
    
std
::cout << *i;
  } 
else
 
if
 (
const float
 * f = 
boost
::get<
float
>(&v) {
    
std
::cout << *f;
  } 
else
 
if
 (
const double
 * d = 
boost
::get<
double
>(&v) {
    
std
::cout << *d;
  } 
else
 {
    assert(
false
);
  }
}
Query the active member using 
get
:
 
boost
::get
 returns 
null
 
if requested type doesn’t
match run-time type.
void
 
print_double
(
double
 d) {
  
std
::cout << d;
}
void 
print_variant
(
boost
::variant<
int
, 
float
, 
double
> v) {
  
boost
::apply_visitor(print_double, v);
}
Better, use a 
visitor
:
 
This only works because 
int
, 
float
 can be
promoted to 
double
 as part of overload resolution.
void 
print_variant
(
boost
::variant<
int
, 
float
, 
double
> v) {
  boost
::apply_visitor([](
auto
 val) {
                         
std
::cout << val;
                       }, v);
}
Using a lambda as a visitor (C++14)
:
 
No promotion here!
More generally, use templates in the visitor object.
struct 
mini_xml;
using
 mini_xml_node =
  boost
::variant<
boost
::recursive_wrapper<mini_xml>,
                 
std
::
string
>;
struct 
mini_xml {
  
std
::
string
 name;
  
std
::
vector
<mini_xml_node> children;
};
Recursive Data Structures (XML)
 
recursive_wrapper<
T
>
 
is “syntactic sugar”
It works like 
std
::unique_ptr<
T
>
But when visiting, or using 
get
, can pretend it is 
T
.
enum
 Message
 {
  Quit,
  ChangeColor(i32, i32, i32),
  Move { x: i32, y: i32 },
  Write(
String
),
}
fn 
process_message
(msg: Message) {
  
match
 msg {
    Message::Quit => quit(),
    Message::ChangeColor(r, g, b) => change_color(r, g, b),
    Message::Move { x, y } => move_cursor(x, y),
    Message::Write(s) => 
println!
(
"{}"
, s);
  }
}
Pattern Matching (Rust):
using 
Message = 
boost
::variant<Quit,
                               ChangeColor,
                               Move,
                               Write>;
void 
process_message
(
const
 Message & msg) {
  
boost
::apply_visitor(
    overload([](Quit) { quit(); },
             [](ChangeColor c) { change_color(c.r, c.g, c.b); }
             [](Move m) { move_cursor(m.x, m.y); }
             [](Write w) { 
std
::cout << w.s << std::endl; }),
    , msg);
}
Pattern Matching (C++):
Existing Implementations
boost
::variant
std
::variant 
(C++17)
strict_variant 
(this talk)
and others...
Surprisingly, many significant design
differences and tradeoffs!
Problem: Exception Safety
 
How to handle 
throwing
, 
type-changing
 assignment.
~A()
Now
 
B(...)
 throws...
Now what? 
A
 is already gone, and have no 
B
A
1
Solution: Double Storage
 
If 
B(...)
 throws, still have 
A
~A()
When 
C
 comes, flip back to first side
A
1
B
5
Solution: 
boost::variant
 
First move 
A
 to heap. (If it fails, we are still ok.)
If 
B(...)
 succeeds, delete 
A
 pointer.
If 
B(...)
 fails, move 
A
 pointer to storage.
                          (Can’t fail.)
A
1
B
2
A*
4
Solution: 
std::variant
 
~A()
, set counter to 
0
Now
 
B(...)
 throws...
Now we are “empty”.
valueless_by_exception()
 reports 
true
visiting is an error until new value provided!
A
1
0
Tradeoffs
No wasted memory
No empty state
Strong exception-safety, rollback semantics
No dynamic allocations, backup copies
Because of C++ language rules,
we can’t have everything we want.
Solution: 
strict_variant
 
If 
B(B&&)
 can’t throw, great, do the obvious.
If 
B(B&&)
 can throw, 
B
 always lives on heap.
Construct 
B
 on heap. If it fails, didn’t touch 
A
.
~A()
, then move 
B*
 to storage. Can’t fail.
A
1
B
2
B*
strict_variant
 design
 
 
1.
Make a “simple” variant which assumes members
are nothrow moveable. (This is easy!)
2.
Then, to make a general variant, stick anything that
throws in a 
recursive_wrapper
 and use the
simple code. (Pointers can always be moved!)
High level design: Reducing to a simpler problem.
template 
<
typename 
T
>
struct 
wrap_if_throwing_move {
  
using
 type =
    
typename std
::conditional<
      
std
::is_nothrow_move_constructible<
T
>::value,
      
T
,
      recursive_wrapper<
T
>
    >::type;
};
template
 <
typename 
T
>
using
 wrap_if_throwing_move_t = 
typename 
wrap_if_throwing_move<
T
>::type;
template
 <
typename
... 
Ts
>
using
 variant = simple_variant<wrap_if_throwing_move_t<
Ts
>...>;
“Step 2”, the reduction, fits here on the screen.
Why use 
strict_variant
instead of 
boost
::
variant
?
boost
::
variant
 
supports even C++98
This means, it has to basically work even if we
can’t check 
noexcept
 
status of operations.
This greatly limits design options.
strict_variant
 targets C++11
This allows an, IMO, simpler and better strategy.
Other features
boost
::
variant
 
and 
std
::variant
sometimes do annoying things
strict_variant
 uses SFINAE to prevent
many “evil” standard conversions here.
std
::
variant
<
int
, 
std
::
string
> v;
v = 
true
; 
// Compiles! Because of bool -> int   :(
std
::
variant
<
bool
, 
std
::
string
> u;
u = 
"The future is now!"
; 
// Selects bool, not std::string!  :(
THANK YOU
 
http://chrisbeck.co
http://github.com/cbeck88/
Slide Note
Embed
Share

Variants and unions are essential concepts in C++ programming for managing heterogenous data and optimizing memory usage. Variants allow storing objects of multiple types in a single container, while unions provide a way to efficiently utilize memory by sharing the same storage space for different types of data. This article explores the definitions, use cases, and differences between variants and unions, with practical examples and best practices. Learn how to leverage these language constructs to enhance your C++ programming skills.

  • C++
  • Variants
  • Unions
  • Memory optimization
  • Data storage

Uploaded on Sep 12, 2024 | 0 Views


Download Presentation

Please find below an Image/Link to download the presentation.

The content on the website is provided AS IS for your information and personal use only. It may not be sold, licensed, or shared on other websites without obtaining consent from the author. Download presentation by click this link. If you encounter any issues during the download, it is possible that the publisher has removed the file from their server.

E N D

Presentation Transcript


  1. STRICT_VARIANT A simpler variant in C++ Chris Beck https://github.com/cbeck88/strict_variant

  2. What is a variant? A variant is a heterogenous container. std::vector<T> many objects of one type std::variant<T, U, V> one object of any of T, U, or V AKA tagged-union , typesafe union

  3. What is a union? struct bar { // Size is sum of sizes, short a; // plus padding for alignment float b; double c; }; union foo { // Size is max of sizes, short a; // alignment is max of alignments float b; double c; };

  4. union foo { short a; float b; double c; }; int main() { foo f; f.a = 5; f.a += 7; f.b = 5; f.b += .5f; } Storing to union may change the active member. Reading inactive member may lead to implementation-defined or undefined behavior!

  5. Why would you use this? Need to store several types of objects in a collection, but no natural inheritance relation. Using an array of unions, store objects contiguously, with very little memory wasted. Low-level signals / event objects Messages matching various schema

  6. struct SDL_KeyboardEvent { Uint32 type; // SDL_KEYDOWN or SDL_KEYUP Uint8 state; // SDL_PRESSED or SDL_RELEASED SDL_Keysym keysym; // Represents the key that was pressed }; struct SDL_MouseMotionEvent { Uint32 type; // SDL_MOUSEMOTION Uint32 state; // bitmask of the current button state Sint32 x; Sint32 y; }; union SDL_Event { SDL_KeyboardEvent key; SDL_MouseMotionEvent motion; ... };

  7. Why would you use this? A variant is a type-safe alternative to a union Prevents you from using inactive members Ensures that destructors are called when the active member changes crucial for C++!

  8. Query the active member using get: void print_variant(boost::variant<int, float, double> v) { if (const int * i = boost::get<int>(&v)) { std::cout << *i; } else if (const float * f = boost::get<float>(&v) { std::cout << *f; } else if (const double * d = boost::get<double>(&v) { std::cout << *d; } else { assert(false); } } boost::get returns nullif requested type doesn t match run-time type.

  9. Better, use a visitor: void print_double(double d) { std::cout << d; } void print_variant(boost::variant<int, float, double> v) { boost::apply_visitor(print_double, v); } This only works because int, float can be promoted to double as part of overload resolution.

  10. Using a lambda as a visitor (C++14): void print_variant(boost::variant<int, float, double> v) { boost::apply_visitor([](auto val) { std::cout << val; }, v); } No promotion here! More generally, use templates in the visitor object.

  11. Recursive Data Structures (XML) struct mini_xml; using mini_xml_node = boost::variant<boost::recursive_wrapper<mini_xml>, std::string>; struct mini_xml { std::string name; std::vector<mini_xml_node> children; }; recursive_wrapper<T>is syntactic sugar It works like std::unique_ptr<T> But when visiting, or using get, can pretend it is T.

  12. Pattern Matching (Rust): enum Message { Quit, ChangeColor(i32, i32, i32), Move { x: i32, y: i32 }, Write(String), } fn process_message(msg: Message) { match msg { Message::Quit => quit(), Message::ChangeColor(r, g, b) => change_color(r, g, b), Message::Move { x, y } => move_cursor(x, y), Message::Write(s) => println!("{}", s); } }

  13. Pattern Matching (C++): using Message = boost::variant<Quit, ChangeColor, Move, Write>; void process_message(const Message & msg) { boost::apply_visitor( overload([](Quit) { quit(); }, [](ChangeColor c) { change_color(c.r, c.g, c.b); } [](Move m) { move_cursor(m.x, m.y); } [](Write w) { std::cout << w.s << std::endl; }), , msg); }

  14. Existing Implementations boost::variant std::variant (C++17) strict_variant (this talk) and others... Surprisingly, many significant design differences and tradeoffs!

  15. Problem: Exception Safety How to handle throwing, type-changing assignment. ~A() Now B(...) throws... Now what? A is already gone, and have no B B A 1

  16. Solution: Double Storage If B(...) throws, still have A ~A() When C comes, flip back to first side C B A B 1 5

  17. Solution: boost::variant First move A to heap. (If it fails, we are still ok.) If B(...) succeeds, delete A pointer. If B(...) fails, move A pointer to storage. (Can t fail.) B A B 1 2 4 A*

  18. Solution: std::variant ~A(), set counter to 0 Now B(...) throws... Now we are empty . valueless_by_exception() reports true visiting is an error until new value provided! B A 1 0

  19. Tradeoffs Because of C++ language rules, we can t have everything we want. No wasted memory No empty state Strong exception-safety, rollback semantics No dynamic allocations, backup copies

  20. Solution: strict_variant If B(B&&)can t throw, great, do the obvious. If B(B&&) can throw, B always lives on heap. Construct Bon heap. If it fails, didn t touch A. ~A(), then move B*to storage. Can t fail. B B A 1 2 B*

  21. strict_variant design High level design: Reducing to a simpler problem. 1. Make a simple variant which assumes members are nothrow moveable. (This is easy!) 2. Then, to make a general variant, stick anything that throws in a recursive_wrapper and use the simple code. (Pointers can always be moved!)

  22. Step 2, the reduction, fits here on the screen. template <typename T> struct wrap_if_throwing_move { using type = typename std::conditional< std::is_nothrow_move_constructible<T>::value, T, recursive_wrapper<T> >::type; }; template <typename T> using wrap_if_throwing_move_t = typename wrap_if_throwing_move<T>::type; template <typename... Ts> using variant = simple_variant<wrap_if_throwing_move_t<Ts>...>;

  23. Why use strict_variant instead of boost::variant? boost::variant supports even C++98 This means, it has to basically work even if we can t check noexcept status of operations. This greatly limits design options. strict_variant targets C++11 This allows an, IMO, simpler and better strategy.

  24. Empty State no Exception Safety yes Backup Copies no Number of states 2n double storage yes no no n+1 std::variant no yes yes 2n boost::variant no yes no n strict_variant

  25. Other features boost::variant and std::variant sometimes do annoying things std::variant<int, std::string> v; v = true; // Compiles! Because of bool -> int :( std::variant<bool, std::string> u; u = "The future is now!"; // Selects bool, not std::string! :( strict_variant uses SFINAE to prevent many evil standard conversions here.

  26. THANK YOU http://chrisbeck.co http://github.com/cbeck88/

Related


More Related Content

giItT1WQy@!-/#giItT1WQy@!-/#giItT1WQy@!-/#giItT1WQy@!-/#giItT1WQy@!-/#giItT1WQy@!-/#giItT1WQy@!-/#