1

I am writing a barebones version of std::variant for a personal project and learning experience. The visitation strategy that I want to implement is a if...else if chain, not a constexpr table of function pointers. The reason is that the latter is notoriously hard for compilers to optimize, and it is easy to produce a benchmark where std::visit is beaten by a chain of if...else if.

I am trying to implement it with fold expressions, but I couldn't find a way to return a value when the correct visitor is found. This is what I have so far:

template <typename... Ts> 
struct my_variant 
{
    std::byte _buffer[std::max({sizeof(Ts)...})];
    std::size_t _discriminator;

    // ... 

    auto match(auto&&... fs) 
    {
        overload_set matcher(std::forward<Fs>(fs)...);

        [&]<std::size_t... Is>(std::index_sequence<Is...>) 
        {
            ([&]
            {
                if (_discriminator == Is) 
                {
                    // How to return from here?
                    matcher(*reinterpret_cast<Ts *>(&_buffer));
                }
            }(), ...);
        }
        (std::make_index_sequence_for<Ts...>{});
    }
};

My current strategy is to create a std::index_sequence for all the types in the variant, then fold over the comma operator to make the compiler generate a bunch of if statements. Since if is not an expression, I had to wrap it into a lambda expression in order to be able to fold over it. If I try to return, I will return from the lambda itself, and that doesn't propagate to the upper layers.

I could use a buffer to store the result, then return it, but that defeats the purpose as it will prevent RVO.

Is there a way I can write match non-recursively and still return the visitor's result allowing RVO to take place?

Vittorio Romeo
  • 90,666
  • 33
  • 258
  • 416
  • 2
    I think you want [this approach](https://mpark.github.io/programming/2019/01/22/variant-visitation-v2/) instead. – Barry Mar 04 '20 at 14:18

1 Answers1

1

You need to pick an operator that doesn't discard the value.

template <typename T>
struct result { // aka std::optional
    std::aligned_storage_t<T> store;
    bool has_value;

    result() : has_value(false) {}
    result(T t) : new(store) T(std::move(t)), has_value(true) {}

    const result & operator| (const result & other) const { return has_value ? *this : other; }

    T get() { return std::move(*reinterpret_cast<T *>(store)); }
};

template <typename... Ts> 
struct my_variant 
{
    std::byte _buffer[std::max({sizeof(Ts)...})];
    std::size_t _discriminator;

    // ... 

    auto match(auto&&... fs) 
    {
        overload_set matcher(std::forward<Fs>(fs)...);

        using result_t = result<std::common_type_t<std::invoke_result_t<matcher, Ts>...>>;

        return [&]<std::size_t... Is>(std::index_sequence<Is...>) 
        {
            return ([&]() -> result_t
            {
                if (_discriminator == Is) 
                {
                    // How to return from here?
                    return matcher(*reinterpret_cast<Ts *>(&_buffer));
                }
                return result_t{};
            }() | ...);
        }
        (std::make_index_sequence_for<Ts...>{}).get();
    }
};
Caleth
  • 52,200
  • 2
  • 44
  • 75
  • This is clever, but doesn't really solve the original problem. You still need a buffer to store the result, and there's additional checks and an additional move constructor call. – Vittorio Romeo Mar 04 '20 at 14:58