'Get tuple element at runtime

I know that it is possible to get a random tuple element at runtime. Behind the scenes, make_integer_sequence and integer_sequence can get all elements at compile time and offer access to these elements at runtime.

But my problem is that it seems only to be possible to access the elements of a random index only with a lambda or function pointer. I would like to get the lambda to return the element-reference so that I could get something like this:

auto myElement = runtime_get(mytuple, 5);

I don't know how I could do this. Working with templates isn't that new to me but templates can get very difficult to understand if they become complex. I'm trying to learn more about them by playing a little bit with the tuple-stuff.



Solution 1:[1]

I think, this should work: runtime_get<std::string>(0, mytuple), so the type is predetermined.

Yes, this is certainly possible, but you need to do something if the runtime index isn't the right type. For example, throw an exception.

Here's one sample implementation, but note that I condensed it at the expense of some readability. Member function pointers and templated lambdas is possibly the worst combination of features the language has to offer, but it was pretty concise (live example):

template<typename Result, typename... Ts>
auto runtime_get(std::size_t i, std::tuple<Ts...>& t) -> Result& {
    using Tuple = std::tuple<Ts...>;

    // A set of functions to get the element at one specific index
    auto get_at_index = []<std::size_t I>(Tuple& tuple) -> Result& {
        if constexpr (std::is_same_v<std::tuple_element_t<I, Tuple>, Result>) {
            return std::get<I>(tuple);
        } else {
            throw std::runtime_error("Index does not contain the right type");
        }
    };

    // Regular index_sequence trick to get a pack of indices
    return [&]<std::size_t... Is>(std::index_sequence<Is...>) -> Result& {
        // The type of a single member function pointer of the closure type, using awkward memfun syntax
        using FPtr = auto(decltype(get_at_index)::*)(Tuple&) const -> Result&;
        // An array of said memfun pointers, each for one known index
        FPtr fptrs[sizeof...(Ts)]{&decltype(get_at_index)::template operator()<Is>...};
        // Invoke the correct pointer
        return (get_at_index.*(fptrs[i]))(t); 
    }(std::index_sequence_for<Ts...>());
}

int main() {
    std::tuple<std::string, int, double, std::string> t{"abc", 2, 5.9, "def"};
    for (int i = 0; i < 4; ++i) {
        try {
            std::string& s = runtime_get<std::string>(i, t);
            std::cout << "Success: " << s << '\n';
        } catch (const std::runtime_error& ex) {
            std::cout << "Failure: " << ex.what() << '\n';
        }
    }
}

Success: abc
Failure: Index does not contain the right type
Failure: Index does not contain the right type
Success: def

This is just the lvalue reference version, you might need other overloads. If you want a more reusable bit of trickery for the runtime-to-compile-time index conversion that you can hide away in a header, check out the std::call proposal.

Note also that this can be built out of a callback-based solution:

template<typename Result, typename... Ts>
auto runtime_get(std::size_t i, std::tuple<Ts...>& t) -> Result& {
    return callback_get(i, t, [](auto& elem) -> Result& { /* same implementation as get_at_index */ });
}

The key point is that types must be resolved at compile-time. In the case of a template as a callback, that callback is being instantiated for every possible type regardless of whether that instatiation is actually used at runtime. You end up with N different callback functions, one for each possible case the program could encounter. There's no analogue for a simple variable.

Therefore, you need to condense N possibilities down to the same behaviour. This can be done as above by choosing a specific type and throwing (or returning an empty optional) on a mismatch. This can also be done by returning a variant, which covers all possible types, but doesn't actually bring you any closer to overcoming the impossible part of this problem—std::visit uses the same callback mechanism where each possible type needs to be compiled against the given callback.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1