Skip to content

Instantly share code, notes, and snippets.

@snarkmaster
Created February 20, 2025 00:32
Show Gist options
  • Save snarkmaster/cfa209a4d05104a78769ca8062f382a0 to your computer and use it in GitHub Desktop.
Save snarkmaster/cfa209a4d05104a78769ca8062f382a0 to your computer and use it in GitHub Desktop.
Draft of uncommitted `folly::type_idx_map` compile-time named tuple (D62111100)

Draft of uncommitted folly::type_idx_map compile-time named tuple (D62111100)

Why a gist?

This mini-library might end up being added to folly/, but it's relevant in other OSS code, and so I'm sharing a version as a gist.

How do I use this?

The doc is at the top of TypeIdxMap.h.

Why did you make this?

Actually, in the current MVP of folly/coro/safe, I made do without type_idx_map by just using a 2-struct inheritance hierarchy, and by hardcoding the relevant typenames throughout. However, this might be revisited later.

The original rationale is like so:


The immediate rationale is configuring variable binding templates for async_closure and named scopes, in upcoming diffs.

Classically, if you want to configure a template, you define some structural types, like so:

struct InnerCfg {
  int a = 5;
  int b = 7;
};
struct OuterCfg {
  bool areWeThereYet = false;
  InnerCfg inner{};
};

template <auto Cfg>
struct C {
  auto operator<=>(const C&) const = default;
};

inline constexpr OuterCfg demoCfg = {true, {5, 7}};
C<demoCfg> x; 

When the use-case requires you to produce a template with a modified configuration, you can copy-and-update the params. Here's an example:

static_assert(C<fooNew>{} == C<[](auto in) {
  in.inner.b += 1;
  return in;
}(foo)>{});

This is all fairly nice until you want to create variations on the configuration, adding more values or types to the configuration. In the above struct model, this is most easily modeled by inheritance, but scenarios where you want a diamond get messy. In my async_closure example:

  • lang/Bindings.h is configured with 2 values for keys const_projection and category_projection.
  • lang/named/Bindings.h extends that 2-key configuration with a typename IdTag
  • coro/safe/AsyncArg.h extends the base 2-key configuration with a 3rd value keyed on async_arg_projection
  • The "diamond": async_closure_named wants to deal with bindings that have all 3 values, and the IdType.

In the above, a particular user-supplied "binding" object can come in any combination -- the 2 base keys, the 3 AsyncArg keys, 2 keys + typename IdTag, or all 3 keys + IdTag.

Virtual inheritance is not applicable here -- that would make the diamond struct non-literal. So, the above setup has to be modeled by breaking up the underlying pieces into orthogonal "mixins", but it's fairly annoying to construct these incrementally.

In contrast, the duck-typed "type -> value map" approach enabled by this diff doesn't require managing inheritance relations -- each piece of code can just inspect the keys that it needs, and/or add any key it provides.

cpp_library(
name = "type_idx_map",
headers = ["TypeIdxMap.h"],
exported_deps = [
"//folly:traits",
],
)
cpp_unittest(
name = "type_idx_map_test",
srcs = ["TypeIdxMapTest.cpp"],
deps = [
":type_idx_map",
"//folly:utility",
"//folly/portability:gtest",
"//folly/portability:source_location",
],
)
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <folly/Traits.h>
///
/// `type_idx_map{A{}, type_idx<B> = 7}` is a compile-time "named tuple".
/// Its primary purpose is a simple "options container" for templates. It
/// stores values, but its keys are types. Above:
/// - `A{}` is a value of stored with the key `A`.
/// - `7` is a value stored with the key `B`.
/// The example's signature is `type_idx_map<pair<A, A>, pair<B, int>>`.
///
/// To get values, `x.at<T>()` is usually more legible in plain functions,
/// but `x[type_idx<T>]` is better in generic code since it avoids the
/// dependent template mess: `x.template at<T>()`.
///
/// Similarly, the static `contains()` has a `<T>` and `type_idx<T>` flavor.
///
/// The container also supports comparison, and bulk operations that
/// can move or copy a map, and then replace, append or remove keys.
///
/// See `TypeIdxMapTest.cpp` for usage examples.
///
namespace folly {
namespace detail {
template <typename... T>
constexpr bool is_unique_v = []<size_t... IJs>(std::index_sequence<IJs...>) {
return (
[]<size_t I, size_t J>(std::index_sequence<I, J>) {
// T[I] != T[J]? Skip redundant comparisons above the diagonal.
return I >= J ||
!std::is_same_v<
type_pack_element_t<I, T...>,
type_pack_element_t<J, T...>>;
}(std::index_sequence<IJs / sizeof...(T), IJs % sizeof...(T)>{}) &&
...);
}(std::make_index_sequence<sizeof...(T) * sizeof...(T)>{});
template <typename T>
struct type_idx_t {
using type = T;
constexpr auto operator=(auto v) const {
return std::pair{*this, std::move(v)};
}
constexpr auto operator<=>(const type_idx_t&) const = default;
};
template <typename K, typename V>
struct type_idx_map_leaf {
V v;
constexpr auto operator<=>(const type_idx_map_leaf&) const = default;
};
template <typename Seq, typename...>
struct type_idx_map_impl;
template <typename... Ks, typename... Vs>
struct type_idx_map_impl<tag_t<Ks...>, Vs...> : type_idx_map_leaf<Ks, Vs>... {
constexpr type_idx_map_impl(Vs&&... vs)
: type_idx_map_leaf<Ks, Vs>{std::forward<Vs>(vs)}... {}
constexpr auto operator<=>(const type_idx_map_impl&) const = default;
};
template <typename K, typename V>
constexpr const V& type_idx_map_get(const type_idx_map_leaf<K, V>& t) {
return t.v;
}
template <typename K, typename V>
constexpr V& type_idx_map_get(type_idx_map_leaf<K, V>& t) {
return t.v;
}
template <typename K, typename V>
constexpr V&& type_idx_map_get(type_idx_map_leaf<K, V>&& t) {
return std::move(t.v);
}
template <typename V>
constexpr auto type_map_idx_val(V&& v) {
return std::forward<V>(v);
}
template <typename K, typename V>
constexpr auto type_map_idx_val(std::pair<type_idx_t<K>, V>&& kv) {
return std::forward<decltype(kv)>(kv).second;
}
template <typename V>
inline constexpr std::type_identity<std::pair<V, V>> type_idx_map_sig{};
template <typename K, typename V>
inline constexpr std::type_identity<std::pair<K, V>>
type_idx_map_sig<std::pair<type_idx_t<K>, V>>{};
} // namespace detail
template <instantiated_from<std::pair>... TagVals>
requires detail::is_unique_v<typename TagVals::first_type...>
class type_idx_map;
template <typename... KVs>
type_idx_map(KVs...)
-> type_idx_map<typename decltype(detail::type_idx_map_sig<KVs>)::type...>;
namespace detail {
constexpr auto type_idx_map_ctad_helper(auto&&... ps) {
return type_idx_map{std::forward<decltype(ps)>(ps)...};
}
} // namespace detail
template <typename T>
inline constexpr detail::type_idx_t<T> type_idx{};
template <instantiated_from<std::pair>... TagVals>
requires detail::is_unique_v<typename TagVals::first_type...>
class type_idx_map {
private:
template <typename... KVs>
using key_list = tag_t<
typename decltype(detail::type_idx_map_sig<KVs>)::type::first_type...>;
public:
detail::type_idx_map_impl<
tag_t<typename TagVals::first_type...>,
typename TagVals::second_type...>
impl_; // Must be public to be a structural type
// Exposed for tests
template <typename>
static constexpr bool is_valid_key_list = false;
template <typename... Ks>
static constexpr bool is_valid_key_list<tag_t<Ks...>> =
(detail::is_unique_v<Ks...> &&
((type_list_find_v<Ks, tag_t<typename TagVals::first_type...>> <
sizeof...(TagVals)) &&
...));
template <typename... KVs, typename KList = key_list<KVs...>>
requires is_valid_key_list<KList>
constexpr explicit type_idx_map(KVs&&... kvs)
: impl_(detail::type_map_idx_val(std::forward<KVs>(kvs))...) {}
constexpr auto operator<=>(const type_idx_map&) const = default;
/// As a rule, operations on keys like `[]`, `at`, or `contains`, have two
/// equivalent styles: (i) passing `type_idx<T>` as an arg, or (ii)
/// passing `T` as a template param. The rationale is that `type_idx<T>`,
/// while longer, avoids the ugly "dependent template" syntax required for
/// template params in generic code.
template <typename T>
static constexpr bool contains(detail::type_idx_t<T>) {
return requires {
detail::type_idx_map_get<T>(std::declval<decltype(impl_)>());
};
}
template <typename T>
static constexpr bool contains() {
return contains(type_idx<T>);
}
/// Per above, use `map[type_idx<T>]` in templates, `map.at<T>()` otherwise.
template <typename T>
constexpr auto& operator[](detail::type_idx_t<T>) & {
return detail::type_idx_map_get<T>(impl_);
}
template <typename T>
constexpr const auto& operator[](detail::type_idx_t<T>) const& {
return detail::type_idx_map_get<T>(impl_);
}
template <typename T>
constexpr auto&& operator[](detail::type_idx_t<T>) && {
return detail::type_idx_map_get<T>(std::move(impl_));
}
template <typename T>
constexpr auto& at() & {
return (*this)[type_idx<T>];
}
template <typename T>
constexpr const auto& at() const& {
return (*this)[type_idx<T>];
}
template <typename T>
constexpr auto& at() && {
return std::move(*this)[type_idx<T>];
}
/// Make a same-keyed `type_idx_map` with some values replaced. Keys must
/// be unique, and must already exist in the map.
constexpr auto copy_replace(auto&&... kvs) const& {
return replace_impl(impl_, std::forward<decltype(kvs)>(kvs)...);
}
constexpr auto move_replace(auto&&... kvs) && {
return replace_impl(std::move(impl_), std::forward<decltype(kvs)>(kvs)...);
}
/// Make a `type_id_map` that first lists all the KVs from the current map,
/// and then `kvs` -- which must have new, unique keys.
constexpr auto copy_append(auto&&... kvs) const& {
return append_impl(impl_, std::forward<decltype(kvs)>(kvs)...);
}
constexpr auto move_append(auto&&... kvs) && {
return append_impl(std::move(impl_), std::forward<decltype(kvs)>(kvs)...);
}
/// Make a `type_id_map` that contains the selected KVs from the current
/// map, in the order set by `Ks` (must exist and be unique).
template <typename... Ks>
constexpr auto copy_some(tag_t<Ks...>) const& {
return some_impl<Ks...>(impl_);
}
template <typename... Ks>
constexpr auto move_some(tag_t<Ks...>) && {
return some_impl<Ks...>(std::move(impl_));
}
/// Make a `type_id_map` that contains the KVs from the current map, in
/// the same order, except for the given keys (must exist and be unique).
template <typename... Ks>
constexpr auto copy_remove(tag_t<Ks...>) const& {
return remove_impl<Ks...>(impl_);
}
template <typename... Ks>
constexpr auto move_remove(tag_t<Ks...>) && {
return remove_impl<Ks...>(std::move(impl_));
}
template <typename... Ks>
requires(detail::is_unique_v<Ks...>)
static constexpr bool has_same_key_set(tag_t<Ks...>) {
return (sizeof...(Ks) == sizeof...(TagVals)) &&
(contains(type_idx<Ks>) && ...);
}
private:
template <typename... KVs, typename KList = key_list<KVs...>>
requires is_valid_key_list<KList>
static constexpr auto replace_impl(auto&& impl, KVs&&... kvs) {
return type_idx_map{[&]() {
using Tag = typename TagVals::first_type;
constexpr size_t I = type_list_find_v<Tag, KList>;
if constexpr (I >= sizeof...(KVs)) {
return std::pair{
type_idx<Tag>,
detail::type_idx_map_get<Tag>(std::forward<decltype(impl)>(impl))};
} else {
return std::forward<type_pack_element_t<I, KVs...>>(
std::get<I>(std::tie(kvs...)));
}
}()...};
}
template <typename K>
static constexpr auto kv_pair_for_(auto&& impl) {
return std::pair{
type_idx<K>,
detail::type_idx_map_get<K>(std::forward<decltype(impl)>(impl))};
}
template <typename... KVs>
static constexpr auto append_impl(auto&& impl, auto&&... kvs) {
return detail::type_idx_map_ctad_helper(
kv_pair_for_<typename TagVals::first_type>(
std::forward<decltype(impl)>(impl))...,
std::forward<decltype(kvs)>(kvs)...);
}
template <typename... Ks>
requires is_valid_key_list<tag_t<Ks...>>
static constexpr auto some_impl(auto&& impl) {
return detail::type_idx_map_ctad_helper(
kv_pair_for_<Ks>(std::forward<decltype(impl)>(impl))...);
}
template <typename... Ks>
requires is_valid_key_list<tag_t<Ks...>>
static constexpr auto remove_impl(auto&& impl) {
return
[&]<typename... Keeps>(tag_t<Keeps...>) {
return some_impl<Keeps...>(std::forward<decltype(impl)>(impl));
}(type_list_concat_t<
tag_t,
std::conditional_t<
(type_list_find_v<typename TagVals::first_type, tag_t<Ks...>> <
sizeof...(Ks)),
tag_t<>,
tag_t<typename TagVals::first_type>>...>{});
}
};
// Not sensitive to key order.
template <typename T, auto TagOfKeys>
concept type_idx_map_of =
is_instantiation_of_v<type_idx_map, T> && T::has_same_key_set(TagOfKeys);
} // namespace folly
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <folly/Utility.h>
#include <folly/lang/TypeIdxMap.h>
#include <folly/portability/GTest.h>
#include <folly/portability/SourceLocation.h>
using namespace folly;
// This is here so that test "runs" show up in CI history
TEST(TypeIdxMap, all_tests_run_at_build_time) {}
// Better UX than `assert()` in constexpr tests.
constexpr void test(bool ok) {
if (!ok) {
throw std::exception(); // Throwing in constexpr code is a compile error
}
}
namespace {
struct A {
constexpr bool operator==(const A&) const = default;
};
struct B {};
struct C {};
} // namespace
static_assert(detail::is_unique_v<>);
static_assert(detail::is_unique_v<A>);
static_assert(detail::is_unique_v<A, B>);
static_assert(detail::is_unique_v<A, B, C>);
static_assert(!detail::is_unique_v<A, A>);
static_assert(!detail::is_unique_v<A, B, A>);
static_assert(!detail::is_unique_v<A, B, A, C>);
static_assert(!detail::is_unique_v<A, B, B, C>);
constexpr auto check_contains() {
type_idx_map m{type_idx<A> = 5, type_idx<B> = "b"};
static_assert(std::is_trivially_copyable_v<decltype(m)>);
test(m.contains(type_idx<A>));
test(m.contains(type_idx<B>));
test(!m.contains(type_idx<C>));
test(!m.contains(type_idx<int>));
test(m.contains<A>());
test(m.contains<B>());
test(!m.contains<C>());
test(!m.contains<int>());
// `contains()` is static, though most usage won't be
test(decltype(m)::contains<A>());
test(decltype(m)::contains(type_idx<B>));
test(!decltype(m)::contains<C>());
test(!decltype(m)::contains(type_idx<int>));
return true;
}
static_assert(check_contains());
// Check that move-only types can be stored and replaced
struct Moo : MoveOnly {
int n;
constexpr bool operator==(const Moo& other) const { return n == other.n; }
};
constexpr auto check_construct_get_replace() {
type_idx_map m1{type_idx<A> = 5, type_idx<B> = 7, Moo{.n = 9}};
test(m1[type_idx<A>] == 5);
test(m1[type_idx<B>] == 7);
test(m1[type_idx<Moo>].n == 9);
test(m1.at<A>() == 5);
test(m1.at<B>() == 7);
test(m1.at<Moo>().n == 9);
// We can't directly test invalid args to `copy_replace()`.
test(m1.is_valid_key_list<tag_t<Moo>>);
test(m1.is_valid_key_list<tag_t<B, Moo>>);
test(m1.is_valid_key_list<tag_t<A, B, Moo>>);
test(!m1.is_valid_key_list<tag_t<C>>); // Type not in map
test(!m1.is_valid_key_list<tag_t<Moo, B, Moo>>); // Type repeated
auto m2 = m1.copy_replace(Moo{.n = 3}, type_idx<B> = 4);
test(m2[type_idx<A>] == 5);
test(m2[type_idx<B>] == 4);
test(m2[type_idx<Moo>].n == 3);
auto m3 = std::move(m2).move_replace(type_idx<A> = 3);
test(m3[type_idx<A>] == 3);
test(m3[type_idx<B>] == 4);
test(m3[type_idx<Moo>].n == 3);
using Sig =
type_idx_map<std::pair<A, int>, std::pair<B, int>, std::pair<Moo, Moo>>;
test(std::is_same_v<decltype(m1), Sig>);
test(std::is_same_v<decltype(m2), Sig>);
test(std::is_same_v<decltype(m3), Sig>);
// Yes, `m2` was moved-out above, but with the underlying data being
// trivially copiable, it's safe to re-access it in this test.
//
// NOLINTNEXTLINE(bugprone-use-after-move)
return std::tuple{std::move(m1), std::move(m2), std::move(m3)};
}
static_assert(
check_construct_get_replace() ==
std::tuple{
type_idx_map{type_idx<A> = 5, type_idx<B> = 7, Moo{.n = 9}},
type_idx_map{type_idx<A> = 5, type_idx<B> = 4, Moo{.n = 3}},
type_idx_map{type_idx<A> = 3, type_idx<B> = 4, Moo{.n = 3}}});
[[maybe_unused]] vtag_t<type_idx_map{type_idx<A> = 5, Moo{.n = 9}}>
kTypeIdxMapIsStructural;
constexpr auto check_append_and_equals() {
const type_idx_map m1{};
test(m1 == type_idx_map{});
const auto m2 = m1.copy_append(type_idx<C> = 1);
test(m2 == type_idx_map{type_idx<C> = 1});
auto m3 = m2.copy_append(type_idx<B> = Moo{.n = 2});
test(m3 == type_idx_map{type_idx<C> = 1, type_idx<B> = Moo{.n = 2}});
auto m4 = std::move(m3).move_append(type_idx<A> = 3);
test(
m4 ==
type_idx_map{
type_idx<C> = 1, type_idx<B> = Moo{.n = 2}, type_idx<A> = 3});
return true;
}
static_assert(check_append_and_equals());
constexpr bool comparable(auto a, auto b) {
return requires {
{ a < b } -> std::convertible_to<bool>;
};
}
constexpr auto check_less_than() {
test(!(type_idx_map{} < type_idx_map{}));
test(!(type_idx_map{7} < type_idx_map{7}));
test(!(type_idx_map{8} > type_idx_map{8}));
test(type_idx_map{7} < type_idx_map{8});
test(type_idx_map{type_idx<A> = -1} < type_idx_map{type_idx<A> = 0});
// We don't sort key types: comparison requires the key order to match.
test(comparable(
type_idx_map{2, type_idx<A> = 1}, type_idx_map{2, type_idx<A> = 1}));
test(!comparable(
type_idx_map{type_idx<A> = 1, 2}, type_idx_map{2, type_idx<A> = 1}));
// Lexicographic ordering: latter keys are less significant
test(!(type_idx_map{2, type_idx<A> = 1} < type_idx_map{2, type_idx<A> = 1}));
test(!(type_idx_map{2, type_idx<A> = 2} < type_idx_map{2, type_idx<A> = 1}));
test(type_idx_map{2, type_idx<A> = 1} < type_idx_map{2, type_idx<A> = 2});
test(
type_idx_map{1, type_idx<C> = 1, type_idx<B> = 1, type_idx<A> = 1} <
type_idx_map{1, type_idx<C> = 1, type_idx<B> = 2, type_idx<A> = 3});
test(
type_idx_map{1, type_idx<C> = 1, type_idx<B> = 1, type_idx<A> = 1} <
type_idx_map{1, type_idx<C> = 2, type_idx<B> = 9, type_idx<A> = 9});
test(
type_idx_map{1, type_idx<C> = 1, type_idx<B> = 1, type_idx<A> = 1} <
type_idx_map{2, type_idx<C> = 9, type_idx<B> = 9, type_idx<A> = 9});
return true;
}
static_assert(check_less_than());
constexpr auto check_same_key_set() {
test(type_idx_map{}.has_same_key_set(tag<>));
test(!type_idx_map{}.has_same_key_set(tag<int>));
auto m1 = type_idx_map{5};
test(m1.has_same_key_set(tag<int>));
test(!m1.has_same_key_set(tag<>));
test(!m1.has_same_key_set(tag<int, double>));
test(!m1.has_same_key_set(tag<double>));
auto m2 = type_idx_map{5, type_idx<A> = 'a'};
test(m2.has_same_key_set(tag<int, A>));
test(m2.has_same_key_set(tag<A, int>));
test(!m2.has_same_key_set(tag<>));
test(!m2.has_same_key_set(tag<int>));
test(!m2.has_same_key_set(tag<A>));
test(!m2.has_same_key_set(tag<int, A, char>));
// Test `type_idx_map_of` concept wrapping `has_same_key_set`.
using int_char_map = type_idx_map<std::pair<int, int>, std::pair<char, int>>;
test(type_idx_map_of<int_char_map, tag<int, char>>);
test(type_idx_map_of<int_char_map, tag<char, int>>);
test(!type_idx_map_of<int_char_map, tag<int, char, double>>);
test(!type_idx_map_of<int_char_map, tag<int>>);
test(!type_idx_map_of<int, tag<int>>);
return true;
}
static_assert(check_same_key_set());
constexpr auto check_some() {
static_assert(type_idx_map{}.copy_some(tag<>) == type_idx_map{});
static_assert(type_idx_map{}.move_some(tag<>) == type_idx_map{});
auto m1 = type_idx_map{5};
test(m1.copy_some(tag<>) == type_idx_map{});
test(m1.copy_some(tag<int>) == m1);
test(std::move(m1).move_some(tag<int>) == type_idx_map{5});
auto m2 = type_idx_map{5, A{}};
test(m2.copy_some(tag<>) == type_idx_map{});
test(m2.copy_some(tag<int>) == type_idx_map{5});
test(m2.copy_some(tag<A>) == type_idx_map{A{}});
test(m2.copy_some(tag<int, A>) == m2);
test(!comparable(m2.copy_some(tag<A, int>), m2)); // the order changed
test(m2.copy_some(tag<A, int>) == type_idx_map{A{}, 5});
return true;
}
static_assert(check_some());
constexpr auto check_remove() {
static_assert(type_idx_map{}.copy_remove(tag<>) == type_idx_map{});
static_assert(type_idx_map{}.move_remove(tag<>) == type_idx_map{});
auto m1 = type_idx_map{5};
test(m1.copy_remove(tag<int>) == type_idx_map{});
test(m1.copy_remove(tag<>) == m1);
test(std::move(m1).move_remove(tag<int>) == type_idx_map{});
auto m2 = type_idx_map{5, A{}};
test(m2.copy_remove(tag<>) == m2);
test(m2.copy_remove(tag<int>) == type_idx_map{A{}});
test(m2.copy_remove(tag<A>) == type_idx_map{5});
test(m2.copy_remove(tag<int, A>) == type_idx_map{});
test(m2.copy_remove(tag<A, int>) == type_idx_map{});
return true;
}
static_assert(check_remove());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment