使用函数重载解析的概念(而不是 SFINAE) [英] Using concepts for function overload resolution (instead of SFINAE)

查看:60
本文介绍了使用函数重载解析的概念(而不是 SFINAE)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

试图向 SFINAE 说再见.

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

例如重载这两个:

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

所以当被调用时,编译器会在每次调用时匹配正确的函数:

doSomething(param_doesnt_adhere_to_ConceptA);//调用 (a)做某事(param_adheres_to_ConceptA);//调用 (b)

<小时>

相关问题:Concepts 会取代 SFINAE 吗?

解决方案

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

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

简单示例:

void print(auto t) {std::cout <<<<std::endl;}无效打印(标准::积分自动我){std::cout <<积分:" <<我<<std::endl;}

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

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

例如,调用函数:

print("你好");//调用打印(自动)打印(7);//调用 print(std::integral auto)

没有歧义 -- 这两个函数可以完美地并排在一起.

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

<小时>

在两个概念之间进行选择

上面的例子展示了编译器如何优先考虑约束类型(std::integral auto)而不是无约束类型(just auto).但这些规则也适用于两个相互竞争的概念.如果一个更具体,编译器应该选择更具体的一个.当然,如果这两个概念都得到满足并且没有一个更具体,这将导致歧义.

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

通用概念 - GenericTwople:

template概念 GenericTwople = requires(P p) {需要 std::tuple_size<P>::value == 2;std::get<0>(p);std::get 1 (p);};

更具体的概念 - Twople:

class Any;模板<类我,类TestAgainst>概念类型匹配 =std::same_as||std::same_as||std::derived_from<Me, TestAgainst>;模板<P类,第一类,第二类>概念二重=GenericTwople<P>&&//<= 注意这一行type_matches, First>&&type_matches, Second>;

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

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

 GenericTwople

&&//<= 注意这一行

根据此行带来的实际要求,Twople 仍具有相同的要求,但不再比 GenericTwople 更具体.当然,这也是代码重用的原因,这就是我们更喜欢基于 GenericTwople 定义 Twople 的原因.

<小时>

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

void print(auto t) {cout<<<<结束;}无效打印(const GenericTwople auto& p){cout<<"GenericTwople: " <<std::get<0>(p)<<", " <<std::get<1>(p)<<结束;}void print(const Twople auto& p) {cout<<"{int, int}: " <<std::get<0>(p)<<", " <<std::get<1>(p)<<结束;}

并调用它:

print(std::tuple{1, 2});//去打印(Twople)打印(标准::元组{1,两个"});//去打印(GenericTwople)打印(标准::对{三",4});//去打印(GenericTwople)打印(std::array{5, 6});//去打印(Twople)打印(你好");//去打印(自动)

我们可以更进一步,因为上面介绍的 Twople 概念也适用于多态:

struct A{虚拟 ~A() = 默认值;虚拟 std::ostream&打印(std::ostream& out = std::cout)const {返回<<一种";}朋友 std::ostream&运算符<<(std::ostream& out, const A& a) {返回 a.print(out);}};结构 B: A{std::ostream&打印(std::ostream& out = std::cout)const override {返回<<乙";}};

添加以下重载:

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

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

 print(std::pair{B{}, A{}});//调用特定的print(Twople)

代码:https://godbolt.org/z/3-O1Gz<小时>

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

template概念 Twople<P, Any, Any>= GenericTwople P ;

这可以为 this SO question,但是不允许概念专业化.

<小时>

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

Trying to say goodbye to SFINAE.

Is it possible to use concepts to distinguish between functions, so the compiler can match the correct function based on whether or not a sent parameter meets concept constraints?

For example, overloading these two:

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

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

So when called the compiler would match the correct function per each call:

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


Related question: Will Concepts replace SFINAE?

解决方案

Yes concepts are designed for this purpose. If a sent parameter doesn't meet the required concept argument the function would not be considered in the overload resolution list, thus avoiding ambiguity.

Moreover, if a sent parameter meets several functions, the more specific one would be selected.

Simple example:

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

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

Above print functions are a valid overloading that can live together.

  • If we send a non integral type it will pick the first
  • If we send an integral type it will prefer the second

e.g., calling the functions:

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

No ambiguity -- the two functions can perfectly live together, side-by-side.

No need for any SFINAE code, such as enable_if -- it is applied already (hidden very nicely).


Picking between two concepts

The example above presents how the compiler prefers constrained type (std::integral auto) over an unconstrained type (just auto). But the rules also apply to two competing concepts. The compiler should pick the more specific one, if one is more specific. Of course if both concepts are met and none of them is more specific this will result with ambiguity.

Well, what makes a concept be more specific? if it is based on the other one1.

The generic concept - GenericTwople:

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

The more specific concept - 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>;

Note that Twople is required to meet GenericTwople requirements, thus it is more specific.

If you replace in our Twople the line:

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

with the actual requirements that this line brings, Twople would still have the same requirements but it will no longer be more specific than GenericTwople. This, along with code reuse of course, is why we prefer to define Twople based on GenericTwople.


Now we can play with all sort of overloads:

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

And call it with:

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)

We can go further, as the Twople concept presented above works also with polymorphism:

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

add the following overload:

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

and call it (while all the other overloads are still present) with:

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

Code: https://godbolt.org/z/3-O1Gz


Unfortunately C++20 doesn't allow concept specialization, otherwise we would go even further, with:

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

Which could add a nice possible answer to this SO question, however concept specialization is not allowed.


1 The actual rules for Partial Ordering of Constraints are more complicated, see: cppreference / C++20 spec.

这篇关于使用函数重载解析的概念(而不是 SFINAE)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆