忽略 C++20 概念特化中的模板实例化失败

Ignoring template instantiation failures in C++20 concept specializations

假设我想写一个 class Collect,它为我提供了将给定 std::vector<> 的项目转移到任何类型的集合的功能。 所有项目类型都支持转移到可向后插入的集合,但转移到关联集合(unordered_map,地图)仅 如果项目是 std::pair<,>.

的变体,则支持

为了实现这一点,我首先定义了标识所请求容器类型的概念(为清楚起见,已缩短且不完整):

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

然后,我开始实现我上面描述的 Collect class,首先定义它的空模板形式:

/** 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 {};

然后我使用上面定义的概念创建这个 class 的专业化:

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

将非std::pair<,> 物品收集到可向后插入的容器中效果很好。将 std::pair<> 项目收集到关联容器也是如此。 但是,将 std::pair<> 项收集到 std::vector<> 中会导致编译器错误:

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

最后一个案例在 std::vector 中遇到了 static_assert 失败,因为 AssocCollection 概念采用了来自 std::pair<,> 的 [=26] 的键和值类型=],以及 second_type。如果它这样做并将这两种类型作为 std::vector 的第一个和第二个模板参数,它会为 std::vector<> 的分配器模板参数提供一个无效参数。 这是带有编译器错误的完整示例的 link:https://godbolt.org/z/oY3cfbrYE

我原以为这些概念在这里的行为更像 SFINAE,这样的错误会导致模板特化被排除,而不是导致编译器错误。有没有比回到 SFINAE 更 C++20`ish 的方法来让它工作?

Is there a more C++20`ish way to get this to work, than falling back to SFINAE?

问题在于,当TContainer对可插入集合进行建模时,Collect对关联集合的偏特化仍然会实例化约束,这会导致实例化无效,例如std::vector<int,std::string>触发 static_assert.

您可以向此偏特化添加额外的约束,以在满足前一个偏特化时阻止其实例化。

/** 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.