Suppose I want to write a class Collect
that provides me with the function to transfer the items of a given std::vector<>
to a collection of any type.
Where transfering to a back-insertable collection is supported for all item types, but transfering to a associative collection (unordered_map, map) is only
supported if the items are variants of std::pair<,>
.
To implement this, I started by defining concepts that identify the type of requested containers (Shortened & incomplete for clarity):
/** Concept that checks whether the given type is a std::pair<> */
template<typename T> concept is_pair = requires(T pair) {
typename T::first_type;
typename T::second_type;
{std::get<typename T::first_type>(pair)} -> std::convertible_to<typename T::first_type>;
{std::get<typename T::second_type>(pair)} -> std::convertible_to<typename T::second_type>;
};
/** Concept that checks whether the given type is a back-insertable collection */
template<template<typename...> typename TContainer, typename TItem>
concept BackInsertableCollection = requires(TContainer<TItem> container, TItem item) {
typename decltype(container)::value_type;
container.push_back(item);
};
/** Concept that checks whether the given type is an associative collection */
template<template<typename...> typename TContainer, typename TItemKey, typename TItemValue>
concept AssocCollection = requires(TContainer<TItemKey, TItemValue> container, TItemKey key, TItemValue value) {
typename decltype(container)::value_type;
typename decltype(container)::key_type;
typename decltype(container)::mapped_type;
container[key] = value;
};
Then, I started implementing the Collect
class I described above, by first defining its empty template form:
/** Collector class that takes a std::vector<TItem> as argument,
* and produces a new container of the requested type TContainer,
* containing the items from the vector.
*/
template<template<typename...> typename TContainer, typename TItem>
struct Collect {};
I then create specializations of this class using the concepts defined above:
/** Collect specialization for BackInsertable containers */
template<template<typename...> typename TContainer, typename TItem>
requires BackInsertableCollection<TContainer, TItem>
struct Collect<TContainer, TItem> {
static auto collect(std::vector<TItem>& values) {
TContainer<TItem> container;
for(const auto& value : values) {
container.push_back(value);
}
return container;
}
};
/** Collect specialization for associative containers */
/** Requires items to be std::pair<>, automatically extracting key and value type for container */
template<template<typename...> typename TContainer, typename TItem>
requires is_pair<TItem> && AssocCollection<TContainer, typename TItem::first_type, typename TItem::second_type>
struct Collect<TContainer, TItem> {
static auto collect(std::vector<TItem>& values) {
TContainer<typename TItem::first_type, typename TItem::second_type> container;
for(const auto& value : values) {
container[value.first] = value.second;
}
return container;
}
};
Collecting non-std::pair<,>
items to back-insertable containers works fine. As does collecting std::pair<>
items to associative containers.
However, collecting std::pair<>
items into an std::vector<>
leads to compiler errors:
int main() {
{ // works: non-std::pair<> to back-insertible container
using Item = std::string;
std::vector<Item> input = {"1", "2"};
auto output = Collect<std::vector, Item>::collect(input);
}
{ // works: std::pair<> to associative container
using Item = std::pair<int, std::string>;
std::vector<Item> input = {{1, "1"}, {2, "2"}};
auto output = Collect<std::unordered_map, Item>::collect(input);
}
{ // compiler error: std::pair<> to back-insertible container
using Item = std::pair<int, std::string>;
std::vector<Item> input = {{1, "1"}, {2, "2"}};
auto output = Collect<std::vector, Item>::collect(input);
}
return 0;
}
The last case hits a static_assert
failure within std::vector
, because the AssocCollection
concept takes key- and value-type from the std::pair<,>
's first_type
, and second_type
. If it does that and places these two types as first and second template parameter to std::vector
, it provides an invalid argument to the std::vector<>
's allocator template-parameter.
Here is a link to the full example with the compiler errors: https://godbolt.org/z/oY3cfbrYE
I would have expected the concepts to behave more like SFINAE here, where such an error causes the template specialization to be ruled out, instead of leading to a compiler error. Is there a more C 20`ish way to get this to work, than falling back to SFINAE?
CodePudding user response:
Is there a more C 20`ish way to get this to work, than falling back to SFINAE?
The problem is that when TContainer
models back-insertable collection, Collect
's partial specialization of associative collections will still instantiate constraints, which leads to invalid instantiations such as std::vector<int,std::string>
to trigger static_assert
.
You can add additional constraints to this partial specialization to block its instantiation when the former partial specialization has been satisfied.
/** Collect specialization for associative containers */
template<template<typename...> typename TContainer, typename TItem>
requires (!BackInsertableCollection<TContainer, TItem>) &&
//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
is_pair<TItem> && AssocCollection<TContainer, typename TItem::first_type, typename TItem::second_type>
struct Collect<TContainer, TItem> {
static auto collect(std::vector<TItem>& values) {
TContainer<typename TItem::first_type, typename TItem::second_type> container;
for(const auto& value : values) {
container[value.first] = value.second;
}
return container;
}
};