Consider a class like this (omitting details):
template<typename K, typename V>
class my_flat_map
{
// A lot of member functions taking a const ref to key
auto find(K const& key) { /*...*/ }
auto contains(K const& key) { /*...*/ }
auto insert_or_assign(K const& key, V const& val) { /*...*/ }
auto insert_if_missing(K const& key, V const& val) { /*...*/ }
// ...
};
When instantiating the template with a string-like key type I'd like to have all those member functions just accepting the corresponding string_view instead of a const reference to the actual key type. I know that I can obtain this with a partial specialization for each possible string class:
template<typename V>
class my_flat_map<std::u32string,V>
{
auto find(std::u32string_view key) { /*...*/ }
// ...
};
template<typename V>
class my_flat_map<std::u16string,V>
{
auto find(std::u16string_view key) { /*...*/ }
// ...
};
template<typename V>
class my_flat_map<std::u8string,V>
{
auto find(std::u8string_view key) { /*...*/ }
// ...
};
template<typename V>
class my_flat_map<other::esotic_string,V>
{
auto find(other::esotic_string_view key) { /*...*/ }
// ...
};
// ...
The bodies of all the member functions are exactly the same, the only change is the signature. Is there a mechanism to express this without repeating all the code?
You could add an alias to your class:
#include <string>
#include <string_view>
// for all non-string types, we fall back to const T&
template <typename T>
struct key_view { using type = const T&; };
// For any specialization of std::basic_string, we use the
// corresponding std::basic_string_view.
// For example, key_view<std::string>::type = std::string_view
template <typename Char, typename Traits, typename Alloc>
struct key_view<std::basic_string<Char, Traits, Alloc>> {
using type = std::basic_string_view<Char, Traits>;
};
// If the key is already a std::basic_string_view, we just use that as the type.
template <typename Char, typename Traits>
struct key_view<std::basic_string_view<Char, Traits>> {
using type = std::basic_string_view<Char, Traits>;
};
template<typename K, typename V>
struct my_flat_map
{
// No major change to the flat_map needed, we just use this alias
using key_view = key_view<K>::type;
auto find(key_view key) { /*...*/ }
auto contains(key_view key) { /*...*/ }
auto insert_or_assign(key_view key, V const& val) { /*...*/ }
auto insert_if_missing(key_view key, V const& val) { /*...*/ }
// ...
};
Without any further changes to my_flat_map
, you can add more partial specializations of key_view
to handle more string-like types (perhaps Qt or boost strings).
Also note that using key_view
in insert_
functions makes no sense. When inserting, the standard library usually uses two overloads for K&&
and const K&
. You can't insert a view anyway, so keep it:
auto insert_or_assign(K const& key, V const& val) { /*...*/ }
auto insert_or_assign(K && key, V const& val) { /*...*/ }
auto insert_or_assign(K const& key, V && val) { /*...*/ }
auto insert_or_assign(K && key, V && val) { /*...*/ }
To avoid repetition, you can use a function template instead. Look into std::map::try_emplace
for design inspiration:
template <typename Key, typename... Args>
auto try_emplace_or_assign(K && key, Args && ...args) { /*...*/ }
You can simply turn some operations into templates to generally support heterogeneous lookup, not just in special cases like std::string_view
:
#include <concepts>
template <typename T, typename U>
concept equality_with_impl = requires (const T& a, const U& b) {
{ a == b } -> std::convertible_to<bool>;
};
template <typename T, typename U>
concept equality_with = equality_with_impl<T, U> && equality_with_impl<U, T>;
template<typename K, typename V>
struct my_flat_map
{
template <equality_with<K> T, typename Equal = std::ranges::equal_to>
auto find(const T& key, Equal equal = {}) { /*...*/ }
// ...
};
Now, if the user calls my_flat_map<std::string, ...>::find
with a const char*
, std::string
, or std::string_view
, it would always work because std::string
has a ==
operator for these.
The user can also provide a custom Equal
function object in case there is no appropriate ==
.
Note: if you're wondering why both equality_with
and equality_with_impl
are necessary, refer to: Why does same_as concept check type equality twice?