1

Suppose I have a variable constructors, which is a tuple of constructor functions represented in variadic generic lambdas.

// types for constructors 
using type_tuple = std::tuple<ClassA, ClassB, ClassC>;

// Get a tuple of constructors(variadic generic lambda) of types in type_tuple
auto constructors = execute_all_t<type_tuple>(get_construct());

// For definitions of execute_all_t and get_construct, see link at the bottom.

I can instantiate an object with:

// Create an object using the constructors, where 0 is index of ClassA in the tuple.
ClassA a = std::get<0>(constructors)(/*arguments for any constructor of ClassA*/);

Is it possible to index the type in runtime with a magic_get like below?

auto obj = magic_get(constructors, 0)(/*arguments for any constructor of ClassA*/);

// Maybe obj can be a std::variant<ClassA, ClassB, ClassC>, which contains object of ClassA?

Edit: Ideally obj should be an instance of ClassA. If not possible, I can accept obj to be std::variant<ClassA, ClassB, ClassC>.

Please check out the minimal reproducible example: Try it online!


A similar question: C++11 way to index tuple at runtime without using switch .

Zheng Qu
  • 781
  • 6
  • 22
  • 1
    Comments are not for extended discussion; this conversation has been [moved to chat](https://chat.stackoverflow.com/rooms/206979/discussion-on-question-by-zheng-qu-run-time-indexing-of-tuple). – Samuel Liew Jan 31 '20 at 15:18

2 Answers2

3

You might have your runtime get return std::variant, something like:

template <typename ... Ts, std::size_t ... Is>
std::variant<Ts...> get_impl(std::size_t index,
                             std::index_sequence<Is...>,
                             const std::tuple<Ts...>& t)
{
    using getter_type = std::variant<Ts...> (*)(const std::tuple<Ts...>&);
    getter_type funcs[] = {+[](const std::tuple<Ts...>& tuple)
                            -> std::variant<Ts...>
                            { return std::get<Is>(tuple); } ...};

    return funcs[index](t);
}

template <typename ... Ts>
std::variant<Ts...> get(std::size_t index, const std::tuple<Ts...>& t)
{
    return get_impl(index, std::index_sequence_for<Ts...>(), t);
}

Then you might std::visit your variant to do what you want.

Demo

or for your "factory" example:

int argA1 = /*..*/;
std::string argA2 = /*..*/;
int argB1 = /*..*/;
// ...

auto obj = std::visit(overloaded{
                [&](const A&) -> std::variant<A, B, C> { return A(argA1, argA2); },
                [&](const B&) -> std::variant<A, B, C> { return B(argB1); },
                [&](const C&) -> std::variant<A, B, C> { return C(); },
            }, get(i, t))
Jarod42
  • 203,559
  • 14
  • 181
  • 302
1

This can probably be done more nicely, but here is an attempt according to your requirements in the comments.

Requires C++17, works on Clang, but gives an Internal Compiler Error on GCC.

It does require though, that you make the constructing function SFINAE-friendly, otherwise there is no way of checking whether it can be called:

So use

return [](auto... args) -> decltype(U(args)...) { return U(args...); };

instead of

return [](auto... args) { return U(args...); };

The behavior of this function given arguments tup and index is as follows:

It returns a lambda that when called with a list of arguments will return a std::variant of all the types that could result from calls of the form std::get<i>(tup)(/*arguments*/). Which one of these is actually called and stored in the returned variant is decided at runtime through the index argument. If index refers to a tuple element that cannot be called as if by std::get<index>(tup)(/*arguments*/), then an exception is thrown at runtime.

The intermediate lambda can be stored and called later. Note however that it saves a reference to the tup argument, so you need to make sure that the argument out-lives the lambda if you don't call and discard it immediately.

#include <tuple>
#include <type_traits>
#include <variant>
#include <utility>
#include <stdexcept>

template<auto V> struct constant_t {
    static constexpr auto value = V;
    using value_type = decltype(value);
    constexpr operator value_type() const {
        return V;
    }
};

template<auto V>
inline constexpr auto constant = constant_t<V>{};

template<auto V1, auto V2>
constexpr auto operator+(constant_t<V1>, constant_t<V2>) {
    return constant<V1+V2>;
}

template<typename T>
struct wrap_t {
    using type = T;
    constexpr auto operator+() const {
        return static_cast<wrap_t*>(nullptr);
    }
};

template<typename T>
inline constexpr auto wrap = wrap_t<T>{};

template<auto A>
using unwrap = typename std::remove_pointer_t<decltype(A)>::type;

template <typename Tup>
auto magic_get(Tup&& tup, std::size_t index) {
  return [&tup, index](auto&&... args) {
    // Get the input tuple size
    constexpr auto size = std::tuple_size_v<std::remove_const_t<std::remove_reference_t<Tup>>>;

    // Lambda: check if element i of tuple is invocable with given args
    constexpr auto is_valid = [](auto i) {
      return std::is_invocable_v<decltype(std::get<i>(tup)), decltype(args)...>;
    };

    // Lambda: get the wrapped return type of the invocable element i of tuple with given args
    constexpr auto result_type = [](auto i) {
      return wrap<std::invoke_result_t<decltype(std::get<i>(tup)), decltype(args)...>>;
    };

    // Recursive lambda call: get a tuple of wrapped return type using `result_type` lambda
    constexpr auto valid_tuple = [=]() {
      constexpr auto lambda = [=](auto&& self, auto i) {
        if constexpr (i == size)
          return std::make_tuple();
        else if constexpr (is_valid(i))
          return std::tuple_cat(std::make_tuple(result_type(i)), self(self, i + constant<1>));
        else
          return self(self, i + constant<1>);
      };
      return lambda(lambda, constant<std::size_t{0}>);
    }();

    // Lambda: get the underlying return types as wrapped variant
    constexpr auto var_type =
        std::apply([](auto... args) { return wrap<std::variant<unwrap<+args>...>>; }, valid_tuple);

    /**
     * Recursive lambda: get a variant of all underlying return type of matched functions, which
     * contains the return value of calling function with given index and args.
     *
     * @param self The lambda itself
     * @param tup A tuple of functions
     * @param index The index to choose from matched (via args) functions
     * @param i The running index to reach `index`
     * @param j The in_place_index for constructing in variant
     * @param args The variadic args for callling the function
     * @return A variant of all underlying return types of matched functions
     */
    constexpr auto lambda = [=](auto&& self, auto&& tup, std::size_t index, auto i, auto j,
                                auto&&... args) -> unwrap<+var_type> {
      if constexpr (i == size)
        throw std::invalid_argument("index too large");
      else if (i == index) {
        if constexpr (is_valid(i)) {
          return unwrap<+var_type>{std::in_place_index<j>,
                                   std::get<i>(tup)(decltype(args)(args)...)};
        } else {
          throw std::invalid_argument("invalid index");
        }
      } else {
        return self(self, decltype(tup)(tup), index, i + constant<1>, j + constant<is_valid(i)>,
                    decltype(args)(args)...);
      }
    };
    return lambda(lambda, std::forward<Tup>(tup), index, constant<std::size_t{0}>,
                  constant<std::size_t{0}>, decltype(args)(args)...);
  };
}

In C++20, you can simplify this by

  • using std::remove_cvref_t<Tup> instead of std::remove_const_t<std::remove_reference_t<Tup>>

  • changing the definition of unwrap to:

    template<auto A>
    using unwrap = typename decltype(A)::type;
    

    and using it as unwrap<...> instead of unwrap<+...>, which also allows removing the operator+ from wrap_t.


The purpose of wrap/unwrap:

wrap_t is meant to turn a type into a value that I can pass into functions and return from them without creating an object of the original type (which could cause all kinds of issues). It is really just an empty struct templated on the type and a type alias type which gives back the type.

I wrote wrap as a global inline variable, so that I can write wrap<int> instead of wrap<int>{}, since I consider the additional braces annoying.

unwrap<...> isn't really needed. typename decltype(...)::type does the same, it just gives back the type that an instance of wrap represents.

But again I wanted some easier way of writing it, but without C++20 this is not really possible in a nice way. In C++20 I can just pass the wrap object directly as template argument, but that doesn't work in C++17.

So in C++17 I "decay" the object to a pointer, which can be a non-type template argument, with an overloaded operator+, mimicking the syntax of the common lambda-to-function-pointer trick using the unary + operator (but I could have used any other unary operator).

The actual pointer value doesn't matter, I only need the type, but the template argument must be a constant expression, so I let it be a null pointer. The latter requirement is why I am not using the built-in address-of operator & instead of an overloaded +.

walnut
  • 21,629
  • 4
  • 23
  • 59
  • 1
    @ZhengQu An ICE is always a compiler bug. I would not report this code since it is too long with irrelevant details. If I get some time I will reduce it to a minimal example and report that (if there isn't already a bug report for the same issue). – walnut Feb 05 '20 at 20:19
  • @ZhengQu Your wrapper kind of defeats the purpose of all of this. You are calling `std::get<0>` on the result, which will always throw an exception if `index != 0`. As I mentioned in the comments on the question, you could achieve that with a simple `if`/`else`. Also note that the function could be written much simpler if you are willing to have unused types in the `std::variant`. I took some detours to avoid that. – walnut Feb 05 '20 at 20:21
  • 1
    @ZhengQu Ah yes, sorry. What I meant is that it will always throw an exception if the index is not matching the first element in the tuple that can be called with the provided arguments. It won't allow calling it with anything but that index. Everything else will throw. And if that is all you wanted you can achieve it much simpler (and in fact the index would then be redundant). My `magic_get` will return a variant of all types that can result from calling the tuple elements with the provided arguments and which one of those is called can be chosen at runtime with the index. – walnut Feb 05 '20 at 21:09
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/207308/discussion-between-zheng-qu-and-walnut). – Zheng Qu Feb 05 '20 at 21:19
  • 1
    I suggest you have a look at the compile-time library boost.hana, it has something similar to what I called `wrap`: `hana::type_c`. See https://www.boost.org/doc/libs/1_72_0/libs/hana/doc/html/structboost_1_1hana_1_1type.html – walnut Feb 08 '20 at 23:41
  • @ZhengQu I have moved my comments into the answer. I also realized that returning a `nullptr` instead of an address to the inline variable is a cleaner way of doing it. That way I don't need the forward-declaration. See my modified code. – walnut Feb 09 '20 at 00:00