2

This is motivated by an answer I gave a newbie user, where I suggested they use an std::variant instead of a union.

With a union, you may have something like the following:

struct Box {
    struct Item { float value; };
    using Boxes = std::vector<Box>;

    union Value {
        Item item;
        Boxes boxes;
    };

    Value contents;
    std::string label;
};

(not exactly what the original question had, I'm taking some poetic license here.) and with a variant, the class might look like this:

struct Box {
    struct Item { float value; };
    using Boxes = std::vector<Box>;

    std::variant<Item, Boxes> contents;
    std::string label;
};

The thing is, that with the first variant, I can write

if (box.contents.boxes.size() > 2) { foo(); }

and provided that I've already established there are going to be sub-boxes, this will work.

With an std::variant, I have to write:

if (std::get<Boxes>(box.contents).size() > 2) { foo(); }

I feel the second version is much less readable, a bit confusing, and quite distracting. Plus - I have to know the type of boxes.

What can I do, in my code, to spare my users the need to make this kind of std::get() calls, and make their lives more pleasant?

Acorn
  • 24,970
  • 5
  • 40
  • 69
einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • 3
    Prefer using `std::visit` instead of `std::get` when accessing `std::variant`s – Mestkon Jun 12 '20 at 11:32
  • @Mestkon: Generally, that's valid advice. But suppose I know which is the active type. – einpoklum Jun 12 '20 at 11:36
  • 1
    The `if` using `std::variant` is not doing the same as the `union` one: it does an unneeded extra check since you have already established there are going to be sub-boxes. Since there is no good way to avoid that check, `std::variant` is not general enough to replace `union`. Thus I avoid recommending it. The compile times, codegen and verbose syntax do not help, either. – Acorn Jun 12 '20 at 12:37
  • @Acorn: 1. You're right about the extra check. 2. Theoretically, the compiler can realize, at least in simple cases, that the check is unnecessary and remove it, but I'm not sure that happens in practice. – einpoklum Jun 12 '20 at 12:48

2 Answers2

5

Just add some accessors wrapping std::gets:

struct Box {
    struct Item { float value; };
    using Boxes = std::vector<Box>;

    std::variant<Item, Boxes> contents;
    std::string label;

    decltype(auto) item()       { return std::get<Item>(contents); }
    decltype(auto) item() const { return std::get<Item>(contents); }

    decltype(auto) boxes()       { return std::get<Boxes>(contents); }
    decltype(auto) boxes() const { return std::get<Boxes>(contents); }
};

And then it goes:

if (box.boxes().size() > 2) { foo(); }
yuri kilochek
  • 12,709
  • 2
  • 32
  • 59
1

How about "visiting" methods? Something like this:

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;


struct Box {
    struct Item { float value; };
    using Boxes = std::vector<Box>;

    std::variant<Item, Boxes> contents;
    std::string label;

    decltype(auto) size() const {
        return std::visit(overloaded {
            [](const Item&)        { return 1; }
            [](const Boxes& boxes) { return boxes.size(); } // non-recursive
        }, *this);
    }
};

and then you write:

if (box.size() > 2 ) { foo(); }

?

einpoklum
  • 118,144
  • 57
  • 340
  • 684