14

这可能是一个哲学问题,但我遇到了以下问题:

如果你定义了一个 std::function,但你没有正确初始化它,你的应用程序将会崩溃,像这样:

typedef std::function<void(void)> MyFunctionType;
MyFunctionType myFunction;
myFunction();

如果函数作为参数传递,如下所示:

void DoSomething (MyFunctionType myFunction)
   {
   myFunction();
   }

然后,当然,它也会崩溃。这意味着我被迫添加这样的检查代码:

void DoSomething (MyFunctionType myFunction)
   {
   if (!myFunction) return;
   myFunction();
   }

要求这些检查让我回想起过去的 C 时代,您还必须明确检查所有指针参数:

void DoSomething (Car *car, Person *person)
   {
   if (!car) return;      // In real applications, this would be an assert of course
   if (!person) return;   // In real applications, this would be an assert of course
   ...
   }

幸运的是,我们可以在 C++ 中使用引用,这使我无法编写这些检查(假设调用者没有将 nullptr 的内容传递给函数:

void DoSomething (Car &car, Person &person)
   {
   // I can assume that car and person are valid
   }

那么,为什么 std::function 实例有默认构造函数呢?如果没有默认构造函数,您就不必添加检查,就像函数的其他普通参数一样。在那些你想传递“可选”std::function 的“罕见”情况下,你仍然可以传递一个指向它的指针(或使用 boost::optional)。

4

7 回答 7

19

是的,但这也适用于其他类型。例如,如果我希望我的班级有一个可选的 Person,那么我将我的数据成员设为 Person-pointer。为什么不对 std::functions 做同样的事情?std::function 有什么特别之处以至于它可以处于“无效”状态?

它没有“无效”状态。它不比这更无效:

std::vector<int> aVector;
aVector[0] = 5;

你所拥有的是一个 function,就像aVector是一个空一样vector。对象处于非常明确的状态:没有数据的状态。

现在,让我们考虑一下您的“函数指针”建议:

void CallbackRegistrar(..., std::function<void()> *pFunc);

你怎么称呼它?好吧,这是你不能做的一件事:

void CallbackFunc();
CallbackRegistrar(..., CallbackFunc);

这是不允许的,因为CallbackFunc它是一个函数,而参数类型是一个std::function<void()>*. 这两个是不可转换的,所以编译器会抱怨。因此,为了进行通话,您必须这样做:

void CallbackFunc();
CallbackRegistrar(..., new std::function<void()>(CallbackFunc));

您刚刚介绍new了图片。您已分配资源;谁来负责?CallbackRegistrar? 显然,您可能想要使用某种智能指针,因此您会更加混乱界面:

void CallbackRegistrar(..., std::shared_ptr<std::function<void()>> pFunc);

这是很多 API 的烦恼和麻烦,只是为了传递一个函数。避免这种情况的最简单方法是允许std::function。就像我们允许std::vector为空一样。就像我们允许std::string为空一样。就像我们允许std::shared_ptr为空一样。等等。

简单地说:std::function 包含一个函数。它是可调用类型的持有人。因此,它可能不包含可调用类型。

于 2011-09-23T09:50:14.400 回答
13

实际上,您的应用程序不应该崩溃。

§ 20.8.11.1 类 bad_function_call [func.wrap.badcall]

1/当函数包装对象没有目标时bad_function_call,(20.8.11.2.4) 会抛出类型异常。function::operator()

行为是完全指定的。

于 2011-09-23T09:48:02.247 回答
6

最常见的用例之一std::function是注册回调,在满足某些条件时调用。允许未初始化的实例可以仅在需要时注册回调,否则您将被迫始终传递至少某种无操作函数。

于 2011-09-23T09:31:56.203 回答
5

答案可能是历史性的:std::function是作为函数指针的替代品,而函数指针有能力成为NULL. 因此,当您想为函数指针提供简单的兼容性时,您需要提供无效状态。

可识别的无效状态并不是真正必要的,因为正如您所提到的,boost::optional这项工作做得很好。所以我会说那std::function只是为了历史。

于 2011-09-23T09:44:16.333 回答
1

在某些情况下,您无法在构造时初始化所有内容(例如,当一个参数取决于对另一个构造的影响时,该构造又取决于对第一个构造的影响......)。

在这种情况下,您必须打破循环,承认可识别的无效状态稍后进行纠正。因此,您将第一个元素构造为“null”,构造第二个元素,然后重新分配第一个元素。

实际上,您可以避免检查,如果 - 在使用函数的地方 - 您在嵌入它的对象的构造函数中授予它,您将始终在有效重新分配后返回。

于 2011-09-23T09:32:50.043 回答
0

与您可以将空状态添加到没有空状态的函子类型相同的方式,您可以用一个不接受空状态的类来包装函子。前者需要添加状态,后者不需要新状态(只是一个限制)。因此,虽然我不知道std::function设计的基本原理,但无论您想要什么,它都支持最精简和最平均的使用。

干杯&hth.,

于 2011-09-23T09:50:10.303 回答
0

您只需将 std::function 用于回调,您可以使用一个简单的模板辅助函数,如果它不为空,则将其参数转发给处理程序:

template <typename Callback, typename... Ts>
void SendNotification(const Callback & callback, Ts&&... vs)
{
    if (callback)
    {
        callback(std::forward<Ts>(vs)...);
    }
}

并以下列方式使用它:

std::function<void(int, double>> myHandler;
...
SendNotification(myHandler, 42, 3.15);
于 2015-09-26T08:25:50.263 回答