Home > Back-end >  C how to accept arbitrary length list of pairs at compile time?
C how to accept arbitrary length list of pairs at compile time?

Time:12-13

I'm looking to build a compile-time read only map and was hoping to back it with a std::array or std::tuple where each element is a std::pair. For ease of use, I would like to avoid having to annotate every entry at construction, and I'd like it to deduce the number of elements in the map i.e.:

constexpr MyMap<int, std::string_view> my_map{
  {1, "value1"},
  {2, "value2"},
};

I've tried a number of strategies to do this, but I seem to be getting stuck in making a ctor or function that is able to both accept an arbitrary number of elements and also tell the compiler that all the braced entries being passed (e.x. {1, "value1"}) is a pair, otherwise it cannot deduce the type.

For example:

template <typename Key, typename Mapped, typename... Args>
constexpr auto make_map(std::pair<Key, Mapped>&& first, Args&&... args) {
  if constexpr (sizeof...(Args) == 0) {
    return std::tuple{std::forward<decltype(first)>(first)};
  }
  return std::tuple_cat(
    std::tuple{std::forward<decltype(first)>(first)},
    make_map(std::forward<Args>(args)...)
  );
}

It seems like I could make a macro that would quickly allow me to make versions of the function for say all arguments up to a reasonable number (say 10-15) but this feels uglier and worse.

Is there a way to do what I want, or do I need to resort to macros or making users annotate each entry with std::pair?

CodePudding user response:

If I am understanding correctly, the size of map is known and fixed? If so, why not use a regular c-style array constructor? Unfortunately, there is no way to make the compiler deduce the type of direct initialization lists (ex: deduce {1, "value"} to std::pair<int, std::string_view>) So, you have to specify the type for deduction to work.

#include <array>
#include <string_view>
#include <utility>

template <typename K, typename V, size_t N>
class MyMap {
 public:
  using value_type = std::pair<K, V>;

  constexpr explicit MyMap(value_type(&&init)[N])
      : data_(std::to_array(std::forward<value_type[N]>(init))) {}

  const std::array<value_type, N> data_;
};

template <typename K, typename V, size_t N>
constexpr MyMap<K, V, N> MakeMyMap(
    typename MyMap<K, V, N>::value_type(&&init)[N]) {
  return MyMap{std::forward<typename MyMap<K, V, N>::value_type[N]>(init)};
}

int main(int argc, char* argv[]) {
  constexpr std::string_view value_1 = "value1";
  constexpr std::string_view value_2 = "value2";

  constexpr auto my_map = MakeMyMap<int, std::string_view>({
      {1, value_1},
      {2, value_2},
  });

  static_assert(my_map.data_.at(0) == std::make_pair(1, value_1));
  static_assert(my_map.data_.at(1) == std::make_pair(2, value_2));

  return EXIT_SUCCESS;
}

Note: this is c 20 only because of std::to_array (https://en.cppreference.com/w/cpp/container/array/to_array). But one can easily implement that in c 17

#include <array>
#include <cstddef>
#include <type_traits>
#include <utility>

namespace internal {

template <bool Move = false, typename T, std::size_t... I>
constexpr std::array<std::remove_cv_t<T>, sizeof...(I)> to_array_impl(T (&a)[sizeof...(I)], std::index_sequence<I...>) {
  if constexpr (Move) {
    return {{std::move(a[I])...}};
  } else {
    return {{a[I]...}};
  }
}

}  // namespace internal

template <typename T, std::size_t N>
constexpr std::array<std::remove_cv_t<T>, N> to_array(T (&a)[N]) noexcept(
    std::is_nothrow_constructible_v<T, T&>) {
  static_assert(!std::is_array_v<T>);
  static_assert(std::is_constructible_v<T, T&>);
  return internal::to_array_impl(a, std::make_index_sequence<N>{});
}

template <typename T, std::size_t N>
constexpr std::array<std::remove_cv_t<T>, N> to_array(T(&&a)[N]) noexcept(
    std::is_nothrow_move_constructible_v<T>) {
  static_assert(!std::is_array_v<T>);
  static_assert(std::is_move_constructible_v<T>);
  return internal::to_array_impl<true>(a, std::make_index_sequence<N>{});
}
  • Related