许多标准库算法采用谓词函数。但是,这些谓词的类型是用户提供的任意模板参数。为什么 C++11 不指定这些采用特定类型,例如std::function
?例如:
template< class InputIt >
InputIt find_if( InputIt first, InputIt last,
std::function<bool()> p );
使用 this 而不是模板作为参数类型不是更干净吗?
许多标准库算法采用谓词函数。但是,这些谓词的类型是用户提供的任意模板参数。为什么 C++11 不指定这些采用特定类型,例如std::function
?例如:
template< class InputIt >
InputIt find_if( InputIt first, InputIt last,
std::function<bool()> p );
使用 this 而不是模板作为参数类型不是更干净吗?
std::function
用于运行时多态性。任何特定std::function
实例都可以存储任何类型的函子(当然,这种类型适合std::function
' 的签名)。
标准库算法等根据其函数参数的类型进行模板化。因此,他们不需要运行时多态来完成他们的工作;它们依赖于编译时多态性。
最重要的是,此类算法不需要将运行时多态性的成本强加给您。如果你想要运行时多态性,你可以发送一个std::function
或任何东西。如果你想要编译时多态性,你可以为它提供一个不使用多态调度的类型(又名:大多数函子或函数)。
运行时多态性的代价还包括无法内联函数调用。使用适当的函子(甚至函数指针,取决于您的编译器有多好),编译器通常可以根据需要内联函数调用。使用运行时多态性,您不仅要支付运行时调度的成本(可能包括额外的参数转发成本),而且还会失去重要的优化机会。
表现!
模板基础功能非常非常好于std::function
模式。我为你做了这个测试:
template <typename F>
void test1(const F &f)
{
for (unsigned long long i = 0; i < 19000000; i++)
f();
}
void test2(function<void()> &f)
{
for (unsigned long long i = 0; i < 19000000; i++)
f();
}
int main()
{
{
LARGE_INTEGER frequency, start, end;
double interval;
QueryPerformanceFrequency(&frequency);
QueryPerformanceCounter(&start);
unsigned long long x = 0;
test1([&x]()
{
x++;
});
QueryPerformanceCounter(&end);
interval = (double) (end.QuadPart - start.QuadPart) / frequency.QuadPart;
cout << "Template mode: " << interval << " " << x << endl;
}
{
LARGE_INTEGER frequency, start, end;
double interval;
QueryPerformanceFrequency(&frequency);
QueryPerformanceCounter(&start);
unsigned long long x = 0;
function<void() > f = [&x]()
{
x++;
};
test2(f);
QueryPerformanceCounter(&end);
interval = (double) (end.QuadPart - start.QuadPart) / frequency.QuadPart;
cout << "std::function mode:" << interval << " " << x << endl;
}
}
模板模式:2.13861e-006
标准::功能模式:0.220006
Windows7-O2
Core2 Duo CPU 2.40GHz上的 gcc 4.7.2
因为std::function
不完美。
怎么不完美?让我列举一下方法。
{
首先,std::function
不支持完美转发传入的任意对象。而且,在实践中,它不能。 std::function
向调用者公开一个固定签名,可以接受多种被调用者,完美转发需要为每个调用者和被调用者定制一个签名。它确实支持完美转发它在其签名中公开的参数,但这还不够。
想象一个std::function
有两个参数int
的 a 和一个double
。为了让它进行完美的转发,它必须接受int&
,int&&
和int const&
, 乘以相同的集合double
(更不用说它的易变变体了)。每个std::function
人必须接受以实现完美转发的签名数量随着它拥有的参数数量呈指数增长。 std::function
它公开的一组签名(目前为 1 个)在实例化时是固定的,而它公开的一组模板签名是无限的,并且仅在使用时生成。这很重要,因为某些类似函数的对象确实针对这些情况进行了不同的优化!因此,std::function
您已经消除了将呼叫完美地转发到包装类型的机会。
不完美的第二个原因std::function
是编译器很烂。如果您将 lambda 包装在 astd::function
然后用它调用算法,编译器理论上可以意识到这std::function
是包装了一些固定的 lambda - 但在实践中,它忘记了这一事实,并将 视为std::function
一些通用类的包装器. 因此,即使在签名std::function
与算法的用例完全匹配的情况下,防止类型瓶颈std::function
使转发不完美,实际上也会由于 执行的类型擦除而产生开销std::function
,并且编译器将发现很难优化std::function
呼叫“障碍”。
不完美的第三个原因std::function
是它会鼓励算法编写者过度限制可以在算法中传递的参数。如果您检查find_if
,天真的假设是您要查找的内容应该与存储在容器中的类型相同,或者至少是可转换的:但std::find_if
算法只要求它们在传入的函子下是可比较的。
这使您可以编写多类型感知函子并传入与容器上的类型无关的目标对象,并且一切正常。大多数人不需要这个,他们的代码没有它就可以工作——这也很好。
天真的std::find_if
会提取容器的底层类型,比较函数将在该类型的对之间进行 - 或者,这将是容器类型和正在搜索的事物类型之间的 4 路比较。在一种情况下,我们失去了灵活性——在另一种情况下,每个人都为一个奇怪的极端情况买单。而在 C++ 中,您应该只在需要时为需要的功能付费!
不完美的第四个原因std::function
是它本质上是一种类型擦除工具。这些是实现细节,但我不知道与它们相差甚远的编译器。Astd::function
的目的是公开单个签名和返回值,并说“我可以存储与此签名和此返回值匹配的任何内容,您可以调用它”。它公开了一个静态运行时接口和实现来执行此任务。当您初始化 astd::function
时,它会在编译时生成一个帮助对象,该对象将该特定对象包装在统一std::function
接口中,然后将其存储在pImpl
模式中。如果您不需要类型擦除,所有这些都是不必要的工作。
标准算法是关于编写几乎与手工解决方案一样高效的高级代码。即使是指针函数调用的成本也不需要解决大多数这些问题,更不用说通过类型擦除的虚拟调用了std::function
。
}; // enum
std::function
是一个很棒的回调工具,用于替换样板的单一用途virtual
接口,以及当您需要向调用者隐藏实现细节时(例如,您需要跨越编译单元边界进行设计决策)。
好消息是,这个问题的更好解决方案正在酝酿之中。特别是,C++14 或 C++17 的目标之一是它将具有某种“概念”支持,您可以在其中说“此模板参数具有以下属性”。确切的语法是什么还远未确定——C++11 概念提案可能会走得很远——但人们对此充满热情,现在有一个工作组来解决这个问题。
当它完成时或如果它完成,您将能够用有意义的概念信息标记函子,这些信息说“这个参数不仅仅是任何类型,而是一个接受两个值的函子类型(包括包含的数据类型) ),并返回bool
编译器、您的 IDE 和您可以理解的兼容值”,而无需查看函数的文档。