【问题标题】:Using concepts for function overload resolution (instead of SFINAE)使用函数重载解析的概念(而不是 SFINAE)
【发布时间】:2020-02-26 12:50:05
【问题描述】:

试图与 SFINAE 说再见。

是否可以使用concepts来区分函数,让编译器根据发送的参数是否满足concept约束来匹配正确的函数?

比如重载这两个:

// (a)
void doSomething(auto t) { /* */ }

// (b)
void doSomething(ConceptA auto t) { /* */ }

因此,当调用编译器时,每次调用都会匹配正确的函数:

doSomething(param_doesnt_adhere_to_ConceptA); // calls (a)
doSomething(param_adheres_to_ConceptA); // calls (b)

相关问题:Will Concepts replace SFINAE?

【问题讨论】:

标签: c++ sfinae overload-resolution c++20 c++-concepts


【解决方案1】:

是的 concepts 就是为此目的而设计的。如果发送的参数不满足所需的概念参数,则该函数将不会被考虑在重载解决列表中,从而避免歧义。

此外,如果发送的参数满足多个功能,则会选择更具体的一个。

简单示例:

void print(auto t) {
    std::cout << t << std::endl;
}

void print(std::integral auto i) {
    std::cout << "integral: " << i << std::endl;
}

print 以上的函数是可以一起存在的有效重载。

  • 如果我们发送一个非整数类型,它将选择第一个
  • 如果我们发送一个整数类型,它将首选第二个

例如,调用函数:

print("hello"); // calls print(auto)
print(7);       // calls print(std::integral auto)

没有歧义 -- 这两个功能可以完美地并存在一起。

不需要任何 SFINAE 代码,例如 enable_if -- 已经应用(隐藏得很好)。


在两个概念之间进行选择

上面的例子展示了编译器如何更喜欢受约束的类型(std::integral auto)而不是不受约束的类型(just auto)。但这些规则也适用于两个相互竞争的概念。如果一个更具体,编译器应该选择更具体的一个。当然,如果这两个概念都满足并且没有一个更具体,这将导致模棱两可。

那么,是什么让概念更具体?如果是基于另一个1.

通用概念 - GenericTwople

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

更具体的概念 - Twople:

class Any;

template<class Me, class TestAgainst>
concept type_matches =
    std::same_as<TestAgainst, Any> ||
    std::same_as<Me, TestAgainst>  ||
    std::derived_from<Me, TestAgainst>;

template<class P, class First, class Second>
concept Twople =
    GenericTwople<P> && // <= note this line
    type_matches<std::tuple_element_t<0, P>, First> &&
    type_matches<std::tuple_element_t<1, P>, Second>;

注意,Twople 需要满足 GenericTwople 要求,因此更具体。

如果您在我们的 Twople 中替换该行:

    GenericTwople<P> && // <= note this line

根据这条线带来的实际要求,Twople 仍然有相同的要求,但它不再比 GenericTwople 更具体。当然,这与代码重用一起,是我们更喜欢基于 GenericTwople 定义 Twople 的原因。


现在我们可以玩各种重载了:

void print(auto t) {
    cout << t << endl;
}

void print(const GenericTwople auto& p) {
    cout << "GenericTwople: " << std::get<0>(p) << ", " << std::get<1>(p) << endl;
}

void print(const Twople<int, int> auto& p) {
    cout << "{int, int}: " << std::get<0>(p) << ", " << std::get<1>(p) << endl;
}

然后调用它:

print(std::tuple{1, 2});        // goes to print(Twople<int, int>)
print(std::tuple{1, "two"});    // goes to print(GenericTwople)
print(std::pair{"three", 4});   // goes to print(GenericTwople)
print(std::array{5, 6});        // goes to print(Twople<int, int>)
print("hello");                 // goes to print(auto)

我们可以走得更远,因为上面介绍的 Twople 概念也适用于多态性:

struct A{
    virtual ~A() = default;
    virtual std::ostream& print(std::ostream& out = std::cout) const {
        return out << "A";
    }
    friend std::ostream& operator<<(std::ostream& out, const A& a) {
        return a.print(out);
    }
};

struct B: A{
    std::ostream& print(std::ostream& out = std::cout) const override {
        return out << "B";
    }
};

添加以下重载:

void print(const Twople<A, A> auto& p) {
    cout << "{A, A}: " << std::get<0>(p) << ", " << std::get<1>(p) << endl;
}

并调用它(同时所有其他重载仍然存在):

    print(std::pair{B{}, A{}}); // calls the specific print(Twople<A, A>)

代码:https://godbolt.org/z/3-O1Gz


不幸的是,C++20 不允许概念专业化,否则我们会走得更远,使用:

template<class P>
concept Twople<P, Any, Any> = GenericTwople<P>;

这可以为this SO question 添加一个很好的可能答案,但是不允许概念专业化。


1 约束的部分排序的实际规则更复杂,请参阅:cppreference / C++20 spec

【讨论】:

  • 很棒的答案。我试图弄清楚如何用类而不是函数来进行类似的专门化:如果T 是浮点数,C&lt;T&gt; 将获得一个实现,如果T 是整数则获得另一个实现,如果T 是一个编译器错误还要别的吗。如果可能的话,它并不像我看到的所有函数示例那样简单。
  • @AdrianMcCarthy 我找到了a nice SO post on that :-)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-12-02
  • 2015-09-19
  • 2021-08-30
  • 1970-01-01
  • 2011-08-25
相关资源
最近更新 更多