实现与 C++20 概念配对的概念

Implementing a concept for Pair with C++20 concepts

要打印任何类型的 std::pair 我们可以实现以下方法:

template<typename First, typename Second>
void printPair(const std::pair<First, Second>& p) {
    std::cout << p.first << ", " << p.second << std::endl;
}

但是假设我们要实现一个可以打印任何类型的一对的方法,不一定std::pair,基于以下要求:

有一个 concept,我们称它为 Pair,可以允许编写如下方法:

void printPair(const Pair auto& p) {
    std::cout << p.first << ", " << p.second << std::endl;
}

如何定义这样的concept

旧语法 - 用于历史目的

下面的代码在 概念技术规范 的早期版本的某个时间点有效,并通过实验实现进行编译,但在 TS 和对 C++20 规范不再有效。由于 历史原因 以及作为对规范更改的说明,它保留在这里。


旧版 Concepts TS 的语法如下:

template<typename _pair>
concept Pair = requires(_pair p) {
    { p.first } -> typename _pair::first_type;
    { p.second } -> typename _pair::second_type;
};

以上语法在 C++20 中无效。对于有效的 C++20 语法,请参阅此问题的其他答案。


这将允许通用 printPair 为 std::pair 以及符合 Pair 要求的任何其他用户 "pair" 工作:

void printPair(const Pair auto& p) {
    std::cout << p.first << ", " << p.second << std::endl;
}

struct UserPair {
    int first = 1;
    const char* second = "hello";
    using first_type = decltype(first);
    using second_type = decltype(second);
};

int main() {
    printPair(std::make_pair(1, 3));
    printPair(UserPair{});
}

旧版本 TS 的工作代码示例:https://godbolt.org/z/x6f76D

这里有一些有趣的微妙之处。

template<class P>
concept Pair = requires(P p) {
    typename P::first_type;
    typename P::second_type;
    p.first;
    p.second;
    requires std::same_as<decltype(p.first), typename P::first_type>;
    requires std::same_as<decltype(p.second), typename P::second_type>;
};

前四行有些多余,但可以帮助生成更好的错误消息。 其余行应该是不言自明的。请注意,在普通 class 成员访问上使用 decltype 会生成数据成员的声明类型。

最后两行也可以写成

    { p.first } -> std::same_as<typename P::first_type&>;
    { p.second } -> std::same_as<typename P::second_type&>;

此处,复合要求类型约束 应用于decltype((p.first))。该表达式是左值,因此生成的类型是左值引用类型。请注意,此版本将同时接受 first_type first;first_type& first;.

在@Nicol Bolas 对原始问题的评论之后,我同意将 concept 缩小到只允许符合 std::pair 要求的 Pair 并不是最好的设计,最好允许所有以下:

  • std::pair 和类似 类 与 firstsecond 字段
  • std::tuple 尺码 2,std::array 尺码 2 和类似 类

确实 std::pair 属于这两个类别,因为它提出了类似元组的语法,但是我们希望能够容纳先公开 的用户类型和 second 字段,但未实现类似元组的语法。

为此,我们可以实现两个独立的概念,然后使用连词创建第三个:


1。简单配对 concept

template<class P>
concept SimplePair = requires(P p) {
    p.first;
    p.second;
};

2。元组对 concept

template<class P>
concept TuplePair = requires(P p) {
    requires std::tuple_size<P>::value == 2;
    std::get<0>(p);
    std::get<1>(p);
};

^以上也支持std::array


3。一对 concept

template<class P>
concept Pair = TuplePair<P> || SimplePair<P>;

现在我们可以有一个通用的 printPair,在 if constexpr:

中使用 requires clause
void printPair(const Pair auto& p) {
    if constexpr( SimplePair<decltype(p)> ) {
        std::cout << p.first << ", " << p.second << std::endl;
    }
    else {
        std::cout << std::get<0>(p) << ", " << std::get<1>(p) << std::endl;
    }
}

使用示例

struct MyPair {
    int first = 5;
    const char* second = "six";
};

int main() {
    printPair(std::make_tuple(1, "two")); // 1, two
    printPair(std::make_pair(3, 4));      // 3, 4
    printPair(MyPair{});                  // 5, six
    printPair(std::array{7, 8});          // 7, 8
    // not allowed, compilation error:
    // printPair(std::array{9, 10, 11});
    // printPair(std::make_tuple("one"));
    // printPair(std::make_tuple(1, 2, 3));
}

代码:https://godbolt.org/z/MXgqu3

我真的很喜欢这个问题和围绕它的讨论,特别是来自 T.C 的解决方案(我没有 50 分在那里评论,所以我将 post 评论作为另一个解决方案). 我刚刚经历了类似的情况,需要一对概念,但还需要库在 C++17 和 C++20 上工作。

此解决方案来自 T.C。适用于 c++17 和 c++20。

template<class P>
concept bool Pair = requires(P p) {
    typename P::first_type;
    typename P::second_type;
    p.first;
    p.second;
    requires my_same_as<decltype(p.first), typename P::first_type>;
    requires my_same_as<decltype(p.second), typename P::second_type>;
};

其中 my_same_as 定义为来自 c++20 的 std::same_as

template<class Me, class Other>
  concept bool my_same_as = std::is_same_v<Me, Other> && std::is_same_v<Other, Me>;

我已经尝试了几个 "implementations of pairs",有趣的是字段 firstsecond 可能因参考或非参考类型而异。

T.C。提到我们可以将字段替换为:

    { p.first } -> my_same_as<typename P::first_type&>;
    { p.second } -> my_same_as<typename P::second_type&>;

我发现这只适用于 c++20,奇怪的是不适用于 c++17(它编译很好,但与概念不匹配!)。不知何故,它既不匹配引用也不匹配非引用(需要使用 ||std::remove_reference_t 的复杂实现)。

我为 c++17 和 c++20 找到的一个可移植解决方案是:

template<typename P>
concept bool Pair = requires(P p)
{   
    typename P::first_type;
    typename P::second_type;
    { p.first } -> my_convertible_to<typename P::first_type>;
    { p.second } -> my_convertible_to<typename P::second_type>;
};

其中 my_convertible_to 等同于 c++20 中的 std::convertible_to

template <class From, class To>
concept bool my_convertible_to =
  std::is_convertible_v<From, To> &&
  requires(std::add_rvalue_reference_t<From> (&f)()) {
    static_cast<To>(f());
  };

我无法解释为什么这种微妙的行为从 c++17 变为 c++20(在 is_same_v 逻辑上),但我 post 在这里,因为它可能会帮助其他人类似的情况。我对 c++17 使用 g++-8,对 c++20 使用 g++-10.1。感谢大家的学习!