Home > Mobile >  Ignoring template instantiation failures in C 20 concept specializations
Ignoring template instantiation failures in C 20 concept specializations

Time:12-15

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;
    }
};

Demo.

  • Related