How to store 3 different types in a sequential container (e.g., std::vector)

119 views Asked by At

I'm working on a problem (it's essentially an encoding problem), where the encoded type can be one of the 3 types:

  1. The first type is fully described by 2 int8_ts. I can use std::pair to contain this or use a struct.

  2. The second type can be fully described by a boolean.

  3. The third type can be described by an integral value that takes on values in {0, 1, 2, 3}, so basically a uint2_t (if that exists). I can use a struct to contain this.

The issue I am having is I need to store all instances of the encoded type in some sequential container (ideally a std::vector<T>), but I'm having some trouble figuring out how to do this in a clean way.

I think I can use std::variant to represent the 3 types, or define a custom struct, perhaps with a kind member attribute (along with the attributes necessary to represent the 3 types) that tells me which one of the 3 types is being represented in an instance of the struct. The former seems like it's going to involve a lot of "type checking" using std::variant methods, and the latter is a lot of wasted memory.

Are there other approaches that can work here?

3

There are 3 answers

7
Jan Schultke On

You're right, the obvious solution is std::variant.

struct A {
    std::int8_t a, b;
};
using B = bool;
using C = std::uint8_t; // enough to represent {0, 1, 2, 3}, no smaller type available

std::vector<std::variant<A, B, C>> stuff;

Besides std::variant, it's also feasible to store a std::array<std::uint8_t>, 2> and just treat it as one of the three types when needed. Depending on your needs, you don't even need to store which "union member" is active then.

You can obviously use a plain union of A, B, and C as well.

0
A. K. On

I'd suggest using variant to discriminate against the types and enum for the last type. Having enum for the last will help detect certain bugs at compile time.

#include<variant>
#include<cstdint>
#include <utility>

using first = std::pair<int8_t, int8_t>;
using second = bool;
using third = enum {zero, one, two, three};

int foo() {
    std::variant<first, second, third> V;
}

0
Ted Lyngmo On

The former seems like it's going to involve a lot of "type checking" using std::variant methods, and the latter is a lot of wasted memory.

The latter could be a struct with a one byte member telling you which of the other members that is active - and the other members could be placed in a union so that only memory for the largest member is required.

As it happens, that's what a std::variant is - so use that.

As for a lot of type checking, you could use std::visit to build up an effective checking mechanism behind the scenes so that you don't have to clutter your code with that too much.

Example (similar to one of the examples in the std::visit link):

#include <variant>

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

using T1 = std::pair<int8_t, int8_t>;
using T2 = bool;
enum class T3 : unsigned char {zero, one, two, three};

int main() {
    std::variant<T1, T2, T3> v;

    v = true;

    std::visit(overloaded{
        [](T1 p) { std::cout << "got pair\n"; },
        [](T2 b) { std::cout << "got bool\n"; },
        [](T3 e) { std::cout << "got enum\n"; }
    }, v);
}

Output:

got bool