众所周知,CPO 可以使用概念来检查它找到的用户定义函数重新加载的性能要求(如输入类型、输出类型等)。但是他们无法检查用户函数的语义,对吧?如以下代码所示:
namespace My_A{
struct A{};
void some_name(A);//not fit
}
namespace My_B{
struct B{};
int some_name(B);//fit, but do unrelated matters
}
template<typename T>
concept usable_some_name =
requires(T arg) {
some_name(arg) -> int;
};
struct __use_fn{
template<typename T>
int operator()(T a)
{
if constexpr (usable_some_name<T>)
return some_name(a);
// ...
}
};
__use_fn use{};
它可以清除不合格的函数,但不能保证"合格"的函数会做它期望做的事情。如果我想确保我的代码没有错误,要么我必须规避名称使用所有可能的接触 CPO(这是不可能的),要么我必须确保我的同名函数(如果有的话)具有这些 CPO 期望的语义(这似乎也是一个不合理的负担)当所有 CPO 都可以使用时。
这是一个不合理的要求,我必须定义我的函数,使其在每次具有与范围算法使用的相同名称时都以与范围算法所需的相同方式运行?
与传统方法相比:
template <>
struct hash<my_type>
{
// ...
}
或
priority_queue<my_type, vector<my_type>, my_type_greater> pq;
我们可以很容易地发现,在这些传统方式中,我们可以仅在我们明确需要时才调用我们的自定义,而不会像 CPO 那样有任何错误调用的风险。
当前的 CPO 是否带来了过度耦合的东西?(我知道它的设计目的和它的优点是什么)有没有更好的方法(也许是tag_invoke
或什么,我不知道)来解决这个问题?
自定义点具有许多内置机制,可大大降低意外匹配的可能性。
让我们考虑一下ranges::begin(t)
.这适用于以下任一类型T
:
- 一个数组
- 拥有会员
begin
- 具有可通过
begin(t)
访问的 ADL 可见begin
非成员函数
在后两种情况下,所选begin
函数必须
- 除
t
外,不带其他参数。 - 返回与输入/输出迭代器匹配的对象。
因此,为了与ranges::begin
发生冲突,其他一些 CPO 必须执行以下所有操作:
- CPO 必须选择令人难以置信的非描述性单词"begin"作为它正在寻找的可扩展函数的名称。
- 此函数必须采用零个其他参数。
- 此函数必须返回一个值,与其类型匹配的输入/输出迭代器可以是合法的返回类型。
特别是最后两个有点...罕见。我的意思是,即使对于"开始",有人将begin
用作空扩展函数并且其返回值合法地是一个迭代器而不是实际上不是范围的第一个元素的可能性有多大?这听起来不太可能。
现在是的,标准库 CPO 确实具有以下优势:标准。每个人都知道begin/end
用于迭代器范围。因此,没有人会真正将它们用于CPO。
但是没有人强迫你为这些扩展函数使用一个单词的名称。标准库可以做到这一点,但如果它让你对冲突感到更安全,你可以使用my_functionality_extension_method_name
。您的 CPO 仍可命名为my_namespace::method_name
。实际上,您可以让成员版本method_name
,而非成员版本更长。或任何对你有用的东西。
说到这一点,您需要记住拥有 CPO 和 CPO所做的区别。CPO 的存在是为了防止人们专门重载或专门化函数(以及使用 ADL 调用它)。这就是为什么它是一个函数对象而不是一个常规函数。
但是,成为 CPO 并不能决定您如何扩展它们。ranges::data
并不取决于您是否具有可访问的data
功能。相反,它通过std::to_address
工作,这通过指针特征或用户提供的特化。
因此,如果您希望使用其他方法来扩展 CPO,例如特征类或其他方法,您可以自由地这样做。CPO 只是调用该功能的一种机制。