为啥 INVOKE 总是取消引用数据成员而不是尽可能地调用?

Posted

技术标签:

【中文标题】为啥 INVOKE 总是取消引用数据成员而不是尽可能地调用?【英文标题】:Why does INVOKE always dereference data members instead of calling when possible?为什么 INVOKE 总是取消引用数据成员而不是尽可能地调用? 【发布时间】:2019-01-26 00:26:42 【问题描述】:

这个问题 (Why does INVOKE facility in the C++11 standard refer to data members?) 询问为什么 INVOKE 讨论了数据成员,却忽略了它们的实际调用方式。

这个问题 (What is std::invoke in c++?) 讨论了为什么它们会被访问,但为什么它们不被调用如果可调用

[func.require]:

定义 INVOKE(f, t1, t2, ..., tN) 如下:

(1.1) (t1.*f)(t2, …, tN) 当 f 是指向类 T 的成员函数的指针并且 is_base_of_v> 为真时;李> (1.2) (t1.get().*f)(t2, ..., tN) 当 f 是指向类 T 的成员函数的指针并且 remove_cvref_t 是 reference_wrapper 的特化时; (1.3) ((*t1).*f)(t2, …, tN) 当f是一个指向类T的成员函数的指针并且t1不满足前两项时; (1.4) t1.*f 当 N == 1 且 f 是指向类 T 的数据成员且 is_base_of_v> 为 true 时; (1.5) t1.get().*f 当 N == 1 且 f 是指向类 T 的数据成员的指针且 remove_cvref_t 是 reference_wrapper 的特化; (1.6) (*t1).*f 当 N == 1 且 f 是指向类 T 的数据成员的指针且 t1 不满足前两项时; (1.7) f(t1, t2, ..., tN) 在所有其他情况下。

1.4 到 1.6 处理对指向数据成员的指针的访问,这对于给定函子和存储的可调用对象是有意义的。我不明白的是为什么它不调用这些成员,而是简单地取消引用它们?如果f 拥有operator (),我希望1.4 将与1.1 的语法和呃...invoke有问题的对象平行。

为什么会有这个限制,它的目的是什么?


这里有一些代码澄清:

#include <functional>
#include <iostream>

struct func1 
 
    void operator()()  std::cout << "Invoked functor\n"; 
;

void func2()

    std::cout << "Invoked free function\n";


struct D1 ;

struct T1 
    func1 f1;
    void func3()  std::cout << "Invoked member function\n"; 
    D1 d1;
;

int main()

    T1 t1;
    func1 free_f1;
    std::invoke(&T1::f1, t1);               //does nothing
    std::invoke(&func1::operator(), t1.f1); //okay, so there is a workaround, if clumsy
    std::invoke(&func2);                    //calls func2
    std::invoke(&T1::func3, t1);            //calls func3
    std::invoke(&T1::d1, t1);               //does nothing (expected)
    std::invoke(free_f1);                   //works on non-member functors

    return 0;

这编译得很好,但只在第二次调用invoke 时调用func1()。我理解为什么 INVOKE 在第一个参数不是可调用对象时什么都不做。我的问题是为什么标准不允许调用指向数据成员的可调用指针,即为什么标准不要求在上述std::invoke 的第一次使用中调用f1


编辑:由于std::invoke 是在 C++17 中添加的,因此我将这个问题标记为这样,希望参与该过程的人能够有所启发。 这是用于添加std::invoke() 的original paper,它实际上解释了它想要统一处理仿函数的动机:

虽然 INVOKE 表达式的行为可以通过现有标准库组件的组合来重现,但在此类解决方案中需要对函子和成员指针进行单独处理。

在上面的代码中,您可以看到这是可行的……只是不适用于指向本身是函子的成员数据的指针。这只是一个疏忽吗?

【问题讨论】:

数据一般不是可以调用的。 @eerorika 可以存储 lambdas 和函数对象。 它对所有类型的成员都有意义,而不仅仅是函子。这在链接的问题中有解释,所以我认为链接的问题是完全重复的,不是吗? 你可以做struct Xint n;; std::function&lt;int(X&amp;)&gt; f(&amp;X::n); 现在,f(x) 返回x.n。请注意,&amp;X::n 是一个指向数据成员的普通指针,而不是可调用的。这就是(1.4) 支持的用法。 (1.5)(1.6) 留给读者作为练习。 @IgorTandetnik 是的,这在链接的问题中,但它并没有让我更接近这个问题的答案。 【参考方案1】:

在组装 C++11 的标准库时,它采用了来自各种 Boost 库的许多特性。就本次对话而言,以下 Boost 工具很重要:

bind function reference_wrapper mem_fn

这些都在一个或另一个层面上与一个可调用的东西有关,它可以接受一些类型的一些参数并产生一个特定的返回值。因此,他们都试图以一致的方式处理可调用的事物。 C++11在采用这些工具的时候,根据完善的转发规则,发明了INVOKE的概念,让所有这些都可以指代一种一致的处理方式。

mem_fn唯一目的是获取指向成员的指针并将其转换为可直接使用() 调用的事物。对于指向成员函数的指针,显而易见的事情是调用被指向的成员函数,给定对象和该函数的任何参数。对于指向成员变量的指针,最明显的做法是在给定要访问的对象的情况下返回该成员变量的值。

将数据成员指针转换为返回变量本身的一元函子的能力非常有用。您可以使用std::transform 之类的东西,将数据成员指针的mem_fn 传递给它以生成访问特定可访问数据成员的值序列。添加到 C++20 的范围功能将使其更加有用,因为您可以创建转换范围,只需获取成员指针即可操作子对象序列。

但事情是这样的:您希望它不管该成员子对象的类型如何。如果该子对象恰好是可调用的,mem_fn 应该能够访问可调用对象,就好像它是任何其他对象一样。它恰好是可调用的这一事实与mem_fn 的目的无关。

但是boost::functionboost::bind 可以接受成员指针。它们都基于boost::mem_fn 的成员指针行为。因此,如果mem_fn 将指向数据成员的指针视为返回该成员值的一元仿函数,那么所有这些都必须以这种方式对待指向数据成员的指针。

因此,当 C++11 将所有这些编码为一个统一的概念时,它直接将该实践编码为 INVOKE

所以从词源的角度来看,这就是INVOKE 以这种方式工作的原因:因为所有这些都旨在以相同的方式处理可调用对象,而mem_fn 关于数据成员指针的全部意义在于将它们视为一元返回其值的函数。所以其他人也必须这样对待他们。

这不是一件好事吗?这不是正确的行为吗?如果成员指针指向的类型恰好是可调用的,那么您是否真的希望指向数据成员的指针的行为完全不同?这将使得不可能编写采用数据成员指针然后对某些对象序列进行操作的通用代码。如果您不确定它会获取被引用的子对象或调用子对象本身,您将如何能够一般地访问数据成员指针?

【讨论】:

以上是关于为啥 INVOKE 总是取消引用数据成员而不是尽可能地调用?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我没有收到关于在 C# 8 中使用结构的类成员可能取消引用 null 的警告?

为啥取消引用称为取消引用的指针?

为啥打印指针与打印取消引用的指针打印相同的东西?

为啥 gorm db.First() 会因“无效的内存地址或 nil 指针取消引用”而恐慌? [复制]

为啥 Theme.of(context) 告诉我在取消引用之前检查该值是不是为“null”?

为啥 C++ 编译器不优化对结构数据成员的读写而不是不同的局部变量?