New-Style Union: std::variant

Problems With C Unions, And C++ Types

  • union with C++ can be gotten right

    • It’s just unlikely

  • Need to call destructors explicitly

  • Other weirdnesses

  • no!

  • std::variant

Instantiation, Default Initialization, Member Access

#include <gtest/gtest.h>
#include <variant>

TEST(variant_suite, default_ctor)
{
    std::variant<int, float> v;                        // <--- default: int{}

    ASSERT_EQ(std::get<int>(v), 0);                    // <--- access: by-type
    ASSERT_EQ(std::get<0>(v), 0);                      // <--- access: by-position

    ASSERT_EQ(v.index(), 0);                           // <--- which position does it hold?
    ASSERT_TRUE(std::holds_alternative<int>(v));       // <--- which type does it hold?

    v = 36.5f;                                         // <--- now has float
                                                       // ...
    ASSERT_FLOAT_EQ(std::get<float>(v), 36.5);
    ASSERT_FLOAT_EQ(std::get<1>(v), 36.5);
    ASSERT_EQ(v.index(), 1);
    ASSERT_TRUE(std::holds_alternative<float>(v));
}

Non-Default Initialization

#include <gtest/gtest.h>
#include <variant>

TEST(variant_suite, converting_ctor)
{
    std::variant<int, float> v{36.5f};                 // <--- holds float

    ASSERT_FLOAT_EQ(std::get<float>(v), 36.5);
    ASSERT_FLOAT_EQ(std::get<1>(v), 36.5);
    ASSERT_EQ(v.index(), 1);
}

Accessing Non-Current Member: std::bad_variant_access

#include <gtest/gtest.h>
#include <variant>

TEST(variant_suite, bad_access)
{
    std::variant<int, float> v{42};

    try {
        std::get<float>(v);
    }
    catch (const std::bad_variant_access&) {
        SUCCEED();
    }
}

The Visitor

#include <gtest/gtest.h>
#include <variant>

struct Visitor
{
    Visitor() : int_seen{false}, float_seen{false} {}
    void operator()(int) { int_seen = true; }
    void operator()(float) { float_seen = true; }
    bool int_seen, float_seen;
};

TEST(variant_suite, visit)
{
    std::variant<int, float> v;

    Visitor vis;

    std::visit(vis, v);
    ASSERT_TRUE(vis.int_seen);

    v = 36.5f;
}

Non-Throwing Accessor: std::get_if<>

  • Exceptions are bad (at least in Embedded)

  • ⟶ back to pointers 🤘

#include <gtest/gtest.h>
#include <variant>

TEST(variant_suite, get_if)
{
    std::variant<int, float> v{42};

    const int* pi = std::get_if<int>(& /*REALLY!*/ v);
    ASSERT_NE(pi, nullptr);
    ASSERT_EQ(*pi, 42);

    const float* pf = std::get_if<float>(&v);
    ASSERT_EQ(pf, nullptr);
}

std::string And const char* Are Different

#include <gtest/gtest.h>
#include <string>
#include <variant>

TEST(variant_suite, string_charp)
{
    std::variant<std::string, const char*> v;
    ASSERT_EQ(std::get<std::string>(v), std::string());

    const char* s = "blah";
    v = s;
    ASSERT_EQ(std::get<const char*>(v), s);

    v = "blah";
    ASSERT_EQ(std::get_if<std::string>(&v), nullptr);
}

And User Defined Types?

#include <gtest/gtest.h>
#include <variant>

struct Foo
{
    Foo() = delete;
    Foo(void*) {}
};

TEST(variant_suite, no_default_ctor)
{
    // std::variant<Foo> v;                            // <--- error: no default ctor
    std::variant<Foo> v{nullptr};                      // <--- OK

    (void)v;
}

And Memory Usage?

  • Not defined by standard

  • Results presented are from GCC

    $ gcc --version
    gcc (GCC) 13.3.1 20240913 (Red Hat 13.3.1-3)
    Copyright (C) 2023 Free Software Foundation, Inc.
    This is free software; see the source for copying conditions.  There is NO
    warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
    
  • What does make sense?

  • ⟶ A typesafe union has to carry a type discriminator: at least as wide as a uint8_t for < 256 different types

#include <variant>
#include <cstdint>
#include <iostream>

int main()
{
    {
        std::variant<uint8_t, uint8_t> v;
        std::cout << sizeof(v) << std::endl;
    }
    return 0;
}
$ ./c++11-variant-mem-usage-2-uint8
2