本章包含的配方如下:
- 默认和删除的功能
- 使用标准算法的 lambdas
- 使用通用 lambdas
- 编写递归 lambda
- 用可变数量的参数编写函数模板
- 使用折叠表达式简化变量函数模板
- 实现高阶函数映射和折叠
- 将函数组合成更高阶的函数
- 统一调用任何可调用的东西
在 C++ 中,类有特殊的成员(构造函数、析构函数和运算符),这些成员可以由编译器默认实现,也可以由开发人员提供。然而,什么可以默认实现的规则有点复杂,可能会导致问题。另一方面,开发人员有时希望防止对象以特定的方式被复制、移动或构造。这可以通过使用这些特殊成员实现不同的技巧来实现。C++ 11 标准通过允许以我们将在下一节中看到的方式删除或默认函数,简化了其中的许多功能。
对于这个食谱,你需要知道什么是特殊的成员函数,什么是可复制和可移动的手段。
使用以下语法指定如何处理函数:
- 要默认一个函数,使用
=default代替函数体。只能默认具有默认值的特殊类成员函数:
struct foo
{
foo() = default;
};- 要删除一个函数,用
=delete代替函数体。可以删除任何函数,包括非成员函数:
struct foo
{
foo(foo const &) = delete;
};
void func(int) = delete;使用默认和删除的功能来实现各种设计目标,例如以下示例:
- 要实现不可复制且隐式不可移动的类,请将复制操作声明为已删除:
class foo_not_copyable
{
public:
foo_not_copyable() = default;
foo_not_copyable(foo_not_copyable const &) = delete;
foo_not_copyable& operator=(foo_not_copyable const&) = delete;
};- 若要实现不可复制但可移动的类,请将复制操作声明为已删除,并显式实现移动操作(并提供所需的任何附加构造函数):
class data_wrapper
{
Data* data;
public:
data_wrapper(Data* d = nullptr) : data(d) {}
~data_wrapper() { delete data; }
data_wrapper(data_wrapper const&) = delete;
data_wrapper& operator=(data_wrapper const &) = delete;
data_wrapper(data_wrapper&& o) :data(std::move(o.data))
{
o.data = nullptr;
}
data_wrapper& operator=(data_wrapper&& o)
{
if (this != &o)
{
delete data;
data = std::move(o.data);
o.data = nullptr;
}
return *this;
}
};- 为了确保只使用特定类型的对象调用函数,并且可能防止类型升级,请为函数提供已删除的重载(以下带有自由函数的示例也可以应用于任何类成员函数):
template <typename T>
void run(T val) = delete;
void run(long val) {} // can only be called with long integers默认情况下,一个类有几个可以由编译器实现的特殊成员。这些是默认构造函数、复制构造函数、移动构造函数、复制赋值、移动赋值和析构函数。如果您不实现它们,那么编译器会这样做,以便可以创建、移动、复制和析构类的实例。但是,如果您显式地提供了这些特殊方法中的一个或多个,那么编译器将不会根据以下规则生成其他方法:
- 如果存在用户定义的构造函数,默认情况下不会生成默认构造函数。
- 如果存在用户定义的虚拟析构函数,默认情况下不会生成默认构造函数。
- 如果存在用户定义的移动构造函数或移动赋值运算符,则默认情况下不会生成复制构造函数和复制赋值运算符。
- 如果存在用户定义的复制构造函数、移动构造函数、复制赋值运算符、移动赋值运算符或析构函数,则默认情况下不会生成移动构造函数和移动赋值运算符。
- 如果存在用户定义的复制构造函数或析构函数,则默认情况下会生成复制赋值运算符。
- 如果存在用户定义的复制赋值运算符或析构函数,则默认情况下会生成复制构造函数。
Note that the last two rules in the preceding list are deprecated rules and may no longer be supported by your compiler.
有时,开发人员需要提供这些特殊成员的空实现或隐藏它们,以防止以特定方式构造类的实例。一个典型的例子是一个不应该被复制的类。这方面的经典模式是提供默认构造函数,并隐藏复制构造函数和复制赋值运算符。虽然这是可行的,但是显式定义的默认构造函数确保该类不再被认为是微不足道的,因此不再是 POD 类型。现代的替代方法是使用删除的函数,如前一节所示。
当编译器在函数定义中遇到=default时,会提供默认实现。前面提到的特殊成员函数的规则仍然适用。函数可以在类体之外声明=default,当且仅当它们是内联的:
class foo
{
public:
foo() = default;
inline foo& operator=(foo const &);
};
inline foo& foo::operator=(foo const &) = default;当编译器遇到函数定义中的=delete时,会阻止函数的调用。但是,在重载解析过程中仍然会考虑该函数,并且只有当删除的函数是最佳匹配时,编译器才会生成错误。例如,通过给出先前为run()函数定义的重载,只可能调用长整数。带有任何其他类型参数的调用,包括int,对其存在到long的自动类型提升,将确定被删除的重载被认为是最佳匹配,因此编译器将生成一个错误:
run(42); // error, matches a deleted overload
run(42L); // OK, long integer arguments are allowed请注意,先前声明的函数不能删除,因为=delete定义必须是翻译单元中的第一个声明:
void forward_declared_function();
// ...
void forward_declared_function() = delete; // errorThe rule of thumb (also known as The Rule of Five) for class special member functions is that, if you explicitly define any copy constructor, move constructor, copy assignment operator, move assignment operator, or destructor, then you must either explicitly define or default all of them.
C++ 最重要的现代特性之一是 lambda 表达式,也称为 lambda 函数或简称 lambda。Lambda 表达式使我们能够定义匿名函数对象,这些对象可以捕获作用域中的变量,并被调用或作为参数传递给函数。Lambdas 有许多用途,在本食谱中,我们将看到如何使用标准算法。
在本食谱中,我们讨论了标准算法,该算法接受一个参数,该参数是应用于它所迭代的元素的函数或谓词。你需要知道什么是一元函数和二元函数,什么是谓词和比较函数。您还需要熟悉函数对象,因为 lambda 表达式是函数对象的语法糖。
您应该更喜欢使用 lambda 表达式将回调传递给标准算法,而不是函数或函数对象:
- 如果只需要在一个地方使用 lambda,请在调用的地方定义匿名 lambda 表达式:
auto numbers =
std::vector<int>{ 0, 2, -3, 5, -1, 6, 8, -4, 9 };
auto positives = std::count_if(
std::begin(numbers), std::end(numbers),
[](int const n) {return n > 0; });- 如果需要在多个地方调用 lambda,定义一个命名的 lambda,即分配给变量的 lambda(通常带有类型的
auto说明符):
auto ispositive = [](int const n) {return n > 0; };
auto positives = std::count_if(
std::begin(numbers), std::end(numbers), ispositive);- 如果需要参数类型不同的 lambda,请使用泛型 lambda 表达式(从 C++ 14 开始提供):
auto positives = std::count_if(
std::begin(numbers), std::end(numbers),
[](auto const n) {return n > 0; });前面第二个项目符号中显示的非泛型 lambda 表达式采用了一个常量整数,如果大于0则返回true,否则返回false。编译器使用具有 lambda 表达式签名的调用运算符定义了一个未命名的函数对象:
struct __lambda_name__
{
bool operator()(int const n) const { return n > 0; }
};编译器定义未命名函数对象的方式取决于我们定义 lambda 表达式的方式,该表达式可以捕获变量,使用mutable说明符或异常规范,或者具有尾随返回类型。前面显示的__lambda_name__函数对象实际上是编译器生成内容的简化,因为它还定义了默认的复制和移动构造函数、默认的析构函数和删除赋值运算符。
It must be well understood that the lambda expression is actually a class. In order to call it, the compiler needs to instantiate an object of the class. The object instantiated from a lambda expression is called a lambda closure.
在下一个示例中,我们希望计算大于或等于 5 且小于或等于 10 的范围内的元素数量。在这种情况下,lambda 表达式如下所示:
auto numbers = std::vector<int>{ 0, 2, -3, 5, -1, 6, 8, -4, 9 };
auto start{ 5 };
auto end{ 10 };
auto inrange = std::count_if(
std::begin(numbers), std::end(numbers),
[start, end](int const n) {
return start <= n && n <= end;});这个λ通过复制(即值)捕获两个变量start和end。编译器创建的未命名函数对象看起来很像我们前面定义的对象。使用前面提到的默认和删除的特殊成员,该类看起来如下所示:
class __lambda_name_2__
{
int start_;
int end_;
public:
explicit __lambda_name_2__(int const start, int const end) :
start_(start), end_(end)
{}
__lambda_name_2__(const __lambda_name_2__&) = default;
__lambda_name_2__(__lambda_name_2__&&) = default;
__lambda_name_2__& operator=(const __lambda_name_2__&)
= delete;
~__lambda_name_2__() = default;
bool operator() (int const n) const
{
return start_ <= n && n <= end_;
}
};lambda 表达式可以通过复制(或值)或引用来捕获变量,两者的不同组合也是可能的。但是一个变量不可能多次捕获,只能在捕获列表的开头有&或=。
A lambda can only capture variables from an enclosing function scope. It cannot capture variables with static storage duration (that is, variables declared in a namespace scope or with the static or external specifier).
下表显示了 lambda 捕获语义的各种组合。
| λ | 描述 |
| [](){} | 不捕捉任何东西 |
| [&](){} | 通过引用获取所有内容 |
| [=](){} | 通过拷贝捕获所有内容 |
| [&x](){} | 仅通过引用捕获x |
| [x](){} | 仅通过复制捕获x |
| [&x...](){} | 通过引用获取包扩展x |
| [x...](){} | 复制捕获包扩展x |
| [&, x](){} | 通过引用捕获所有内容,除了通过拷贝捕获的x |
| [=, &x](){} | 通过复制捕获所有内容,除了通过引用捕获的x |
| [&, this](){} | 通过引用捕获除指针this以外的所有内容,该指针由副本捕获(this始终由副本捕获) |
| [x, x](){} | 错误,x被捕获两次 |
| [&, &x](){} | 错误,所有内容都是引用捕获的,不能再次指定引用捕获x |
| [=, =x](){} | 错误,所有内容都被复制捕获,不能再次指定通过复制捕获x |
| [&this](){} | 错误,指针this总是被拷贝捕获 |
| [&, =](){} | 错误,无法通过复制和引用捕获所有内容 |
从 C++ 17 开始,lambda 表达式的一般形式如下:
[capture-list](params) mutable constexpr exception attr -> ret
{ body }该语法中显示的所有部分实际上都是可选的,除了捕获列表(可以是空的)和正文(也可以是空的)。如果不需要参数,参数列表实际上可以省略。不需要指定返回类型,因为编译器可以从返回表达式的类型中推断出它。mutable说明符(告诉编译器 lambda 实际上可以修改通过复制捕获的变量)constexpr说明符(告诉编译器生成一个constexpr调用运算符),以及异常说明符和属性都是可选的。
The simplest possible lambda expression is []{}, though it is often written as [](){}.
在某些情况下,lambda 表达式只在参数类型上有所不同。在这种情况下,lambdas 可以用通用的方式编写,就像模板一样,但是使用类型参数的auto说明符(不涉及模板语法)。这将在下一个配方中解决,在部分中提到。
- 使用通用 lambdas
- 编写递归λ
在前面的配方中,我们看到了如何编写 lambda 表达式并将其与标准算法一起使用。在 C++ 中,lambdas 基本上是未命名函数对象的语法糖,这些函数对象是实现调用运算符的类。然而,就像任何其他功能一样,这可以用模板来实现。C++ 14 利用了这一点,引入了不需要为参数指定实际类型的泛型 lambdas,而是使用auto说明符。虽然没有用这个名字来称呼,但是通用 lambda 基本上是 lambda 模板。在我们希望使用相同的 lambda 但参数类型不同的情况下,它们非常有用。
建议您先阅读前面的食谱*,使用带有标准算法的 lambdas】,然后再继续这一个。*
编写通用 lambdas:
- 通过使用
auto说明符代替 lambda 表达式参数的实际类型。 - 当需要使用多个仅参数类型不同的 lambdas 时。
以下示例显示了与std::accumulate()算法一起使用的通用 lambda,首先是整数向量,然后是字符串向量。
auto numbers =
std::vector<int>{0, 2, -3, 5, -1, 6, 8, -4, 9};
auto texts =
std::vector<std::string>{"hello"s, " "s, "world"s, "!"s};
auto lsum = [](auto const s, auto const n) {return s + n;};
auto sum = std::accumulate(
std::begin(numbers), std::end(numbers), 0, lsum);
// sum = 22
auto text = std::accumulate(
std::begin(texts), std::end(texts), ""s, lsum);
// sum = "hello world!"s在上一节的示例中,我们定义了一个名为 lambda 的表达式,即一个 lambda 表达式,它的闭包被赋给了一个变量。该变量然后作为参数传递给std::accumulate()函数。这种通用算法采用定义范围的开始和结束迭代器、要累加的初始值,以及一个将范围内每个值累加到总数的函数。该函数将表示当前累计值的第一个参数和表示当前值的第二个参数累加到一起,并返回新的累计值。
请注意,我没有使用术语add,因为这可以用于其他事情,而不仅仅是添加。它还可以用于计算乘积、连接或其他将值聚合在一起的操作。
本例中对std::accumulate()的两次调用几乎相同,只是参数的类型不同:
- 在第一个调用中,我们将迭代器传递给一个整数范围(从
vector<int>开始),0 表示初始和,λ表示两个整数相加并返回它们的和。这将产生该范围内所有整数的总和;对于这个例子,它是 22。 - 在第二次调用中,我们将迭代器传递给一系列字符串(从
vector<string>)、一个用于初始值的空字符串和一个通过将两个字符串相加并返回结果来连接它们的 lambda。这会生成一个字符串,该字符串包含该范围内一个接一个放在一起的所有字符串;对于这个例子,结果是*“你好世界!”*。
虽然泛型 lambda 可以在它们被调用的地方匿名定义,但这并没有真正的意义,因为泛型 lambda(基本上,如前所述,是一个 lambda 表达式模板)的目的是被重用,如*中的示例所示...*段。
当定义这个用于多次调用std::accumulate()的 lambda 表达式时,我们使用了auto说明符并让编译器推导出类型,而不是为 lambda 参数指定具体的类型(如int或std::string)。当遇到参数类型具有auto说明符的 lambda 表达式时,编译器会生成一个具有调用运算符模板的未命名函数对象。对于本例中的通用 lambda 表达式,函数对象如下所示:
struct __lambda_name__
{
template<typename T1, typename T2>
auto operator()(T1 const s, T2 const n) const { return s + n; }
__lambda_name__(const __lambda_name__&) = default;
__lambda_name__(__lambda_name__&&) = default;
__lambda_name__& operator=(const __lambda_name__&) = delete;
~__lambda_name__() = default;
};调用运算符是一个模板,它为λ中的每个参数都指定了一个类型参数,该参数是用auto指定的。调用运算符的返回类型也是auto,这意味着编译器将从返回值的类型中推导出来。该运算符模板将使用编译器在使用泛型 lambda 的上下文中识别的实际类型进行实例化。
- 使用标准算法的 lambdas】
- 尽可能使用自动第八章的食谱学习现代核心语言功能
Lambdas 基本上是未命名的函数对象,这意味着应该可以递归调用它们。的确,它们可以递归调用;但是,这样做的机制并不明显,因为它需要将 lambda 分配给函数包装器,并通过引用捕获包装器。虽然可以说递归 lambda 没有真正的意义,函数可能是更好的设计选择,但在这个食谱中,我们将看看如何编写递归 lambda。
为了演示如何编写递归 lambda,我们将考虑著名的斐波那契函数示例。这通常在 C++ 中递归实现,如下所示:
constexpr int fib(int const n)
{
return n <= 2 ? 1 : fib(n - 1) + fib(n - 2);
}为了编写递归 lambda 函数,必须执行以下操作:
- 在函数范围内定义 lambda。
- 将λ分配给
std::function包装器。 - 在 lambda 中通过引用捕获
std::function对象,以便递归调用它。
以下是递归 lambdas 的示例:
- 函数范围内的递归斐波那契λ表达式,从定义它的范围调用:
void sample()
{
std::function<int(int const)> lfib =
[&lfib](int const n)
{
return n <= 2 ? 1 : lfib(n - 1) + lfib(n - 2);
};
auto f10 = lfib(10);
}- 函数返回的递归斐波那契λ表达式,可以从任何范围调用:
std::function<int(int const)> fib_create()
{
std::function<int(int const)> f = [](int const n)
{
std::function<int(int const)> lfib = [&lfib](int n)
{
return n <= 2 ? 1 : lfib(n - 1) + lfib(n - 2);
};
return lfib(n);
};
return f;
}
void sample()
{
auto lfib = fib_create();
auto f10 = lfib(10);
}编写递归 lambda 时需要考虑的第一件事是,lambda 表达式是一个函数对象,为了从 lambda 的主体递归调用它,lambda 必须捕获它的闭包(即 lambda 的实例化)。换句话说,lambda 必须捕获自身,这有几个含义:
- 首先,lambda 必须有一个名称;无法捕获未命名的 lambda 以便再次调用。
- 其次,lambda 只能在函数范围内定义。这样做的原因是 lambda 只能从函数范围捕获变量;它无法捕获任何具有静态存储持续时间的变量。在命名空间范围内或使用静态或外部说明符定义的对象具有静态存储持续时间。如果 lambda 是在命名空间范围内定义的,它的闭包将具有静态存储持续时间,因此 lambda 不会捕获它。
- 第三个含义是 lambda 闭包的类型不能保持未指定,也就是说,不能用 auto 说明符声明。用自动类型说明符声明的变量不可能出现在自己的初始值设定项中,因为在处理初始值设定项时,变量的类型是未知的。因此,您必须指定 lambda 闭包的类型。我们可以这样做的方法是使用通用函数包装器
std::function。 - 最后但同样重要的是,lambda 闭包必须通过引用来捕获。如果我们通过复制(或值)来捕获,那么就会生成一个函数包装的副本,但是当捕获完成时,包装没有初始化。我们最终得到了一个我们无法调用的对象。即使编译器不会抱怨按值捕获,当调用闭包时,也会抛出
std::bad_function_call。
第一个例子来自*怎么做...*部分,递归λ是在另一个名为sample()的函数中定义的。lambda 表达式的签名和主体与介绍部分定义的常规递归函数fib()相同。lambda 闭包被分配给一个名为lfib的函数包装器,然后被 lambda 引用捕获并从其主体递归调用。因为闭包是由引用捕获的,所以它将在必须从 lambda 的主体中调用时被初始化。
在第二个示例中,我们定义了一个函数,该函数返回 lambda 表达式的闭包,该表达式又定义并调用一个递归 lambda,其参数又被调用。当需要从函数返回递归 lambda 时,必须实现这种模式。这是必要的,因为在调用递归 lambda 时,lambda 闭包必须仍然可用。如果在此之前被销毁,我们就剩下一个悬空的引用,调用它会导致程序异常终止。以下示例说明了这种错误情况:
// this implementation of fib_create is faulty
std::function<int(int const)> fib_create()
{
std::function<int(int const)> lfib = [&lfib](int const n)
{
return n <= 2 ? 1 : lfib(n - 1) + lfib(n - 2);
};
return lfib;
}
void sample()
{
auto lfib = fib_create();
auto f10 = lfib(10); // crash
}解决方法是创建两个嵌套的 lambda 表达式,如*所示...*节。fib_create()方法返回一个函数包装器,当调用该函数包装器时,它会创建捕获自身的递归 lambda。这与前面示例中显示的实现略有不同,但本质上是不同的。外部fλ不捕捉任何东西,尤其是通过引用;因此,我们没有悬空引用的问题。然而,当被调用时,它会创建嵌套 lambda 的闭包,也就是我们感兴趣调用的实际 lambda,并返回将该递归lfib lambda 应用于其参数的结果。
编写具有可变数量参数的函数或具有可变数量成员的类有时很有用。典型的例子包括像printf这样的函数,它采用一种格式和可变数量的参数,或者像tuple这样的类。在 C++ 11 之前,前者只有在使用变量宏(只允许编写类型不安全的函数)的情况下才有可能,而后者根本不可能。C++ 11 引入了变量模板,变量模板是具有可变数量参数的模板,这使得既可以编写具有可变数量参数的类型安全函数模板,也可以编写具有可变数量成员的类模板。在这个食谱中,我们将研究如何编写函数模板。
变元数的函数称为变元函数。具有可变数量参数的函数模板称为可变函数模板。C++ 变量宏(va_start、va_end、va_arg、va_copy、va_list)的知识对于学习如何编写变量函数模板来说并不是必须的,但它代表了一个很好的起点。
我们已经在之前的食谱中使用了变量模板,但是这次将提供详细的解释。
为了编写变量函数模板,必须执行以下步骤:
- 如果变量函数模板的语义需要,用固定数量的参数定义一个重载来结束编译时递归(参见下面代码中的
[1])。 - 定义一个模板参数包,引入一个可以容纳任意数量参数的模板参数,包括零;这些参数可以是类型、非类型或模板(参见
[2])。 - 定义一个函数参数包来保存任意数量的函数参数,包括零;模板参数包和对应的功能参数包大小相同,可以用
sizeof...操作符确定(参考[3])。 - 展开参数包,以便用提供的实际参数替换它(参见
[4])。
下面的例子说明了前面的所有要点,是一个变量函数模板,它使用operator+添加了可变数量的参数:
template <typename T> // [1] overload with fixed
T add(T value) // number of arguments
{
return value;
}
template <typename T, typename... Ts> // [2] typename... Ts
T add(T head, Ts... rest) // [3] Ts... rest
{
return head + add(rest...); // [4] rest...
}乍一看,前面的实现看起来像递归,因为函数add()调用自己,从某种程度上来说的确如此,但它是一个编译时递归,不会导致任何类型的运行时递归和开销。编译器实际上根据变量函数模板的使用生成了几个具有不同参数数量的函数,因此只涉及函数重载,而不涉及任何类型的递归。但是,实现就好像参数会以带有结束条件的递归方式进行处理一样。
在前面的代码中,我们可以识别以下关键部分:
Typename... Ts是一个模板参数包,表示可变数量的模板类型参数。Ts... rest是一个函数参数包,表示可变数量的函数参数。Rest...是功能参数包的扩展。
The position of the ellipsis is not syntactically relevant. typename... Ts, typename ... Ts, and typename ...Ts are all equivalent.
在add(T head, Ts... rest)参数中,head是参数列表的第一个元素,...rest是列表中剩余参数的一个包(可以是零或更多)。在函数体中,rest...是函数参数包的扩展。这意味着编译器用参数包元素的顺序来替换参数包。在add()函数中,我们基本上是将第一个参数添加到其余参数的总和中,这给人一种递归处理的印象。当只剩下一个参数时,递归结束,在这种情况下,调用第一个add()重载(带有一个参数)并返回其参数值。
函数模板add()的这个实现使我们能够编写代码,如下所示:
auto s1 = add(1, 2, 3, 4, 5);
// s1 = 15
auto s2 = add("hello"s, " "s, "world"s, "!"s);
// s2 = "hello world!"当编译器遇到add(1, 2, 3, 4, 5)时,会生成如下函数(arg1、arg2等等,都不是编译器生成的实际名称),说明这实际上只是对重载函数的调用,而不是递归:
int add(int head, int arg1, int arg2, int arg3, int arg4)
{return head + add(arg1, arg2, arg3, arg4);}
int add(int head, int arg1, int arg2, int arg3)
{return head + add(arg1, arg2, arg3);}
int add(int head, int arg1, int arg2)
{return head + add(arg1, arg2);}
int add(int head, int arg1)
{return head + add(arg1);}
int add(int value)
{return value;}With GCC and Clang, you can use the __PRETTY_FUNCTION__ macro to print the name and the signature of the function.
通过在我们编写的两个函数的开头添加一个std::cout << __PRETTY_FUNCTION__ << std::endl,我们在运行代码时得到如下结果:
T add(T, Ts ...) [with T = int; Ts = {int, int, int, int}]
T add(T, Ts ...) [with T = int; Ts = {int, int, int}]
T add(T, Ts ...) [with T = int; Ts = {int, int}]
T add(T, Ts ...) [with T = int; Ts = {int}]
T add(T) [with T = int]因为这是一个函数模板,所以可以和任何支持operator+的类型一起使用。另一个例子,add("hello"s, " "s, "world"s, "!"s),产生了*“你好世界!”*弦。然而,std::basic_string类型对于operator+有不同的重载,包括一个可以将字符串连接到一个字符的重载,因此我们应该也能够编写以下内容:
auto s3 = add("hello"s, ' ', "world"s, '!');
// s3 = "hello world!"但是,这将产生如下编译器错误(注意,为了简单起见,我实际上用字符串*“hello world”*替换了std::basic_string<char, std::char_traits<char>, std::allocator<char> >):
In instantiation of 'T add(T, Ts ...) [with T = char; Ts = {string, char}]':
16:29: required from 'T add(T, Ts ...) [with T = string; Ts = {char, string, char}]'
22:46: required from here
16:29: error: cannot convert 'string' to 'char' in return
In function 'T add(T, Ts ...) [with T = char; Ts = {string, char}]':
17:1: warning: control reaches end of non-void function [-Wreturn-type]发生的事情是编译器生成下面显示的代码,其中返回类型与第一个参数的类型相同。然而,第一个参数不是std::string就是char(同样,为了简单起见,std::basic_string<char, std::char_traits<char>, std::allocator<char> >被string代替)。如果char是第一个参数的类型,返回值head+add(...)的类型std::string与函数返回类型不匹配,并且没有隐式转换:
string add(string head, char arg1, string arg2, char arg3)
{return head + add(arg1, arg2, arg3);}
char add(char head, string arg1, char arg2)
{return head + add(arg1, arg2);}
string add(string head, char arg1)
{return head + add(arg1);}
char add(char value)
{return value;}我们可以通过修改变量函数模板使返回类型为auto而不是T来解决这个问题。在这种情况下,返回类型总是从返回表达式中推断出来,在我们的示例中,在所有情况下都是std::string:
template <typename T, typename... Ts>
auto add(T head, Ts... rest)
{
return head + add(rest...);
}还应补充的是,参数包可以出现在括号初始化中,其大小可以使用sizeof...运算符来确定。此外,变量函数模板不一定意味着编译时递归,正如我们在本食谱中所展示的那样。所有这些都显示在下面的例子中,在这个例子中,我们定义了一个函数来创建一个成员数为偶数的元组。我们首先使用sizeof...(a)来确保我们有偶数个参数,否则通过生成编译器错误来断言。sizeof...操作符可用于模板参数包和功能参数包。sizeof...(a)和sizeof...(T)会产生相同的价值。然后,我们创建并返回一个元组。模板参数包T展开(带T...)为std::tuple类模板的类型参数,函数参数包a展开(带a...)为使用括号初始化的元组成员的值:
template<typename... T>
auto make_even_tuple(T... a)
{
static_assert(sizeof...(a) % 2 == 0,
"expected an even number of arguments");
std::tuple<T...> t { a... };
return t;
}
auto t1 = make_even_tuple(1, 2, 3, 4); // OK
// error: expected an even number of arguments
auto t2 = make_even_tuple(1, 2, 3);- 使用折叠表达式简化变量函数模板
- 创建原始用户定义的文字第 9 章、使用数字和 字符串
在这一章中,我们讨论了几次折叠;这是一种将二进制函数应用于一系列值以产生单个值的操作。我们在讨论变量函数模板时已经看到了这一点,我们将在讨论高阶函数时再次看到这一点。事实证明,在很多情况下,变量函数模板中参数包的扩展基本上是一个折叠操作。为了简化这种变量函数模板的编写,C++ 17 引入了折叠表达式,将参数包的扩展折叠到二进制运算符上。在这个食谱中,我们将看到如何使用 fold 表达式来简化变量函数模板的编写。
本配方中的示例基于我们在上一个配方中编写的可变函数模板add(),编写一个具有可变参数数量的函数模板。这个实现是一个左折叠操作。为简单起见,我们再次展示该函数:
template <typename T>
T add(T value)
{
return value;
}
template <typename T, typename... Ts>
T add(T head, Ts... rest)
{
return head + add(rest...);
}要在二元运算符上折叠参数包,请使用以下形式之一:
- 用一元形式左折叠
(... op pack):
template <typename... Ts>
auto add(Ts... args)
{
return (... + args);
}- 用二进制形式左折叠
(init op ... op pack):
template <typename... Ts>
auto add_to_one(Ts... args)
{
return (1 + ... + args);
}- 用一元形式右折叠
(pack op ...):
template <typename... Ts>
auto add(Ts... args)
{
return (args + ...);
}- 用二进制形式右折叠
(pack op ... op init):
template <typename... Ts>
auto add_to_one(Ts... args)
{
return (args + ... + 1);
}The parentheses shown above are part of the fold expression and cannot be omitted.
当编译器遇到 fold 表达式时,它会以下列表达式之一展开它:
| 表达式 | 膨胀 |
| (... op pack) | (“打包 1 美元到打包 2 美元”)上我不知道)打包课程$n |
| (init op ... op pack) | ((以$1 开头)以$2 开头)我不知道)打包课程$n |
| (pack op ...) | 包$1 op(-我...。op(打包课程$n-1 打包课程$n)) |
| (pack op ... op init) | 打包 1 美元(我不知道上(打包$n-1)(打包$n 到初始) |
当使用二进制形式时,椭圆左侧和右侧的运算符必须相同,并且初始值不得包含未展开的参数包。
fold 表达式支持以下二进制运算符:
| + | - | * | / | % | ^ | & | | | = | < | > | << | | >> | += | -= | = | /= | %= | = | &= | |= | <<= | >>= | == | | != | <= | >= | && | || | , | 。 | ->*. | | | | |
使用一元形式时,空参数包只允许使用*、+、&、|、&&、||、,(逗号)等运算符。在这种情况下,空包装的值如下:
| + | 0 |
| * | 1 |
| & | -1 |
| | | 0 |
| && | true |
| || | false |
| , | void() |
现在我们已经有了前面实现的函数模板(让我们考虑左折叠版本),我们可以编写以下代码:
auto sum = add(1, 2, 3, 4, 5); // sum = 15
auto sum1 = add_to_one(1, 2, 3, 4, 5); // sum = 16考虑到add(1, 2, 3, 4, 5)调用,它将产生以下函数:
int add(int arg1, int arg2, int arg3, int arg4, int arg5)
{
return ((((arg1 + arg2) + arg3) + arg4) + arg5);
}Due to the aggressive ways modern compilers do optimizations, this function can be inlined and eventually end up with an expression such as auto sum = 1 + 2 + 3 + 4 + 5.
Fold 表达式适用于支持的二进制运算符的所有重载,但不适用于任意二进制函数。通过提供一个包装类型来保存一个值,并为该包装类型提供一个重载运算符,可以实现一种变通方法:
template <typename T>
struct wrapper
{
T const & value;
};
template <typename T>
constexpr auto operator<(wrapper<T> const & lhs,
wrapper<T> const & rhs)
{
return wrapper<T> {
lhs.value < rhs.value ? lhs.value : rhs.value};
}
template <typename... Ts>
constexpr auto min(Ts&&... args)
{
return (wrapper<Ts>{args} < ...).value;
}在前面的代码中,wrapper是一个简单的类模板,它保存对类型为T的值的常量引用。为此类模板提供了一个重载的operator<;这个重载不会返回一个布尔值来指示第一个参数小于第二个参数,而是实际上是一个wrapper类类型的实例来保存两个参数的最小值。变量函数模板min()使用这个重载的operator<来折叠扩展到包装类模板实例的参数包:
auto m = min(1, 2, 3, 4, 5); // m = 1- 实现高阶函数映射和折叠
在本书前面的食谱中,我们在几个例子中使用了通用算法std::transform()和std::accumulate(),例如实现字符串实用程序来创建字符串的大写或小写副本,或者对一个范围的值求和。这些基本上都是高阶函数map和fold的实现。高阶函数是将一个或多个其他函数作为参数,并将它们应用于一个范围(列表、向量、地图、树等)的函数,生成新的范围或值。在这个食谱中,我们将看到如何实现map和fold函数来使用 C++ 标准容器。
Map 是一个高阶函数,它将一个函数应用于一个范围的元素,并以相同的顺序返回一个新的范围。
Fold 是一个高阶函数,它将组合函数应用于范围的元素,产生单个结果。由于处理的顺序可能很重要,所以这个函数通常有两个版本- foldleft,从左到右处理元素,以及 foldright ,从右到左组合元素。
Most descriptions of the function map indicate that it is applied to a list, but this is a general term that can indicate different sequential types, such as list, vector, and array, and also dictionaries (that is, maps), queues, and so on. For this reason, I prefer to use the term range when describing these higher-order functions.
要实现map功能,您应该:
- 在支持元素迭代和赋值的容器上使用
std::transform,如std::vector或std::list:
template <typename F, typename R>
R mapf(F&& f, R r)
{
std::transform(
std::begin(r), std::end(r), std::begin(r),
std::forward<F>(f));
return r;
}- 对于不支持元素赋值的容器,使用其他方式,如显式迭代和插入,如
std::map:
template<typename F, typename T, typename U>
std::map<T, U> mapf(F&& f, std::map<T, U> const & m)
{
std::map<T, U> r;
for (auto const kvp : m)
r.insert(f(kvp));
return r;
}
template<typename F, typename T>
std::queue<T> mapf(F&& f, std::queue<T> q)
{
std::queue<T> r;
while (!q.empty())
{
r.push(f(q.front()));
q.pop();
}
return r;
}要实现fold功能,您应该:
- 在支持迭代的容器上使用
std::accumulate():
template <typename F, typename R, typename T>
constexpr T foldl(F&& f, R&& r, T i)
{
return std::accumulate(
std::begin(r), std::end(r),
std::move(i),
std::forward<F>(f));
}
template <typename F, typename R, typename T>
constexpr T foldr(F&& f, R&& r, T i)
{
return std::accumulate(
std::rbegin(r), std::rend(r),
std::move(i),
std::forward<F>(f));
}- 使用其他方式明确处理不支持迭代的容器,如
std::queue:
template <typename F, typename T>
constexpr T foldl(F&& f, std::queue<T> q, T i)
{
while (!q.empty())
{
i = f(i, q.front());
q.pop();
}
return i;
}在前面的例子中,我们以一种功能性的方式实现了地图,没有副作用。这意味着它会保留原来的范围并返回一个新的范围。函数的参数是要应用的函数和范围。为了避免与std::map容器混淆,我们调用了这个函数mapf。mapf有几个重载,如前所示:
- 第一个重载用于支持迭代和元素赋值的容器;这包括
std::vector、std::list和std::array,但也包括类似 C 的数组。该函数引用一个函数和一个定义了std::begin()和std::end()的范围。该范围通过值传递,因此修改本地副本不会影响原始范围。通过使用标准算法std::transform()将给定函数应用于每个元素来转换范围;然后返回转换后的范围。 - 第二个重载专门用于不支持直接分配给其元素(
std::pair<T, U>)的std::map。因此,此重载创建一个新地图,然后使用基于范围的 for 循环遍历其元素,并将输入函数应用于原始地图的每个元素的结果插入到新地图中。 - 第三个重载专门用于
std::queue,这是一个不支持迭代的容器。可以说,队列不是一个典型的映射结构,但是为了演示不同的可能实现,我们正在考虑它。为了迭代一个队列的元素,必须改变队列——你需要从前面弹出元素,直到列表为空。这就是第三个重载的作用——它处理输入队列的每个元素(通过值传递),并将给定函数的结果推送到剩余队列的前面元素。
现在我们已经实现了这些重载,我们可以将它们应用于许多容器,如下面的示例所示(请注意,这里使用的 map 和 fold 函数是在本书附带的代码中名为 funclib 的命名空间中实现的,因此以完全限定名显示):
- 保留向量的绝对值。在本例中,向量包含负值和正值。应用映射后,结果是一个只有正值的新向量。
auto vnums =
std::vector<int>{0, 2, -3, 5, -1, 6, 8, -4, 9};
auto r = funclib::mapf([](int const i) {
return std::abs(i); }, vnums);
// r = {0, 2, 3, 5, 1, 6, 8, 4, 9}- 将列表的数值平方。在本例中,列表包含整数值。应用映射后,结果是包含初始值平方的列表。
auto lnums = std::list<int>{1, 2, 3, 4, 5};
auto l = funclib::mapf([](int const i) {
return i*i; }, lnums);
// l = {1, 4, 9, 16, 25}- 浮点的舍入数量。对于这个例子,我们需要使用
std::round();但是,所有浮点类型都有重载,这使得编译器无法选择正确的类型。因此,我们要么必须编写一个 lambda,该 lambda 接受特定浮点类型的参数并返回应用于该值的std::round()值,要么创建一个包装std::round()的函数对象模板,并仅针对浮点类型启用其调用运算符。该技术用于以下示例:
template<class T = double>
struct fround
{
typename std::enable_if<
std::is_floating_point<T>::value, T>::type
operator()(const T& value) const
{
return std::round(value);
}
};
auto amounts =
std::array<double, 5> {10.42, 2.50, 100.0, 23.75, 12.99};
auto a = funclib::mapf(fround<>(), amounts);
// a = {10.0, 3.0, 100.0, 24.0, 13.0}- 大写单词映射的字符串键(其中键是单词,值是文本中出现的次数)。请注意,创建字符串的大写副本本身就是一个映射操作。因此,在本例中,我们使用
mapf将toupper()应用于表示密钥的字符串元素,以便生成大写副本:
auto words = std::map<std::string, int>{
{"one", 1}, {"two", 2}, {"three", 3}
};
auto m = funclib::mapf(
[](std::pair<std::string, int> const kvp) {
return std::make_pair(
funclib::mapf(toupper, kvp.first),
kvp.second);
},
words);
// m = {{"ONE", 1}, {"TWO", 2}, {"THREE", 3}}- 对优先级队列中的值进行规范化-最初,值是从 1 到 100,但是我们希望将它们规范化为两个值,1 =高,2 =正常。所有初始优先级值不超过 30 的优先级都成为高优先级,其他优先级为正常优先级:
auto priorities = std::queue<int>();
priorities.push(10);
priorities.push(20);
priorities.push(30);
priorities.push(40);
priorities.push(50);
auto p = funclib::mapf(
[](int const i) { return i > 30 ? 2 : 1; },
priorities);
// p = {1, 1, 1, 2, 2}要实现fold,我们实际上要考虑两种可能的折叠类型,即从左到右和从右到左。因此,我们提供了两个名为foldl(用于左折叠)和foldr(用于右折叠)的功能。上一节中显示的实现非常相似——它们都接受一个函数、一个范围和一个初始值,并调用std::algorithm()将该范围的值折叠成一个值。然而,foldl使用直接迭代器,而foldr使用反向迭代器来遍历和处理范围。第二个重载是类型std::queue的专门化,它没有迭代器。
基于折叠的这些实现,我们可以做以下示例:
- 将整数向量的值相加。在这种情况下,左右折叠将产生相同的结果。在下面的例子中,我们要么传递一个取和的 lambda 和一个数字并返回一个新的和,要么传递标准库中的函数对象
std::plus<>,该函数对象将operator+应用于两个相同类型的操作数(基本上类似于 lambda 的闭包):
auto vnums =
std::vector<int>{0, 2, -3, 5, -1, 6, 8, -4, 9};
auto s1 = funclib::foldl(
[](const int s, const int n) {return s + n; },
vnums, 0); // s1 = 22
auto s2 = funclib::foldl(
std::plus<>(), vnums, 0); // s2 = 22
auto s3 = funclib::foldr(
[](const int s, const int n) {return s + n; },
vnums, 0); // s3 = 22
auto s4 = funclib::foldr(
std::plus<>(), vnums, 0); // s4 = 22- 将向量中的字符串连接成单个字符串:
auto texts =
std::vector<std::string>{"hello"s, " "s, "world"s, "!"s};
auto txt1 = funclib::foldl(
[](std::string const & s, std::string const & n) {
return s + n;},
texts, ""s); // txt1 = "hello world!"
auto txt2 = funclib::foldr(
[](std::string const & s, std::string const & n) {
return s + n; },
texts, ""s); // txt2 = "!world hello"- 将字符数组连接成字符串:
char chars[] = {'c','i','v','i','c'};
auto str1 = funclib::foldl(std::plus<>(), chars, ""s);
// str1 = "civic"
auto str2 = funclib::foldr(std::plus<>(), chars, ""s);
// str2 = "civic"- 根据在
map<string, int>中可用的已经计算好的外观,计算文本中的字数:
auto words = std::map<std::string, int>{
{"one", 1}, {"two", 2}, {"three", 3} };
auto count = funclib::foldl(
[](int const s, std::pair<std::string, int> const kvp) {
return s + kvp.second; },
words, 0); // count = 6这些函数可以流水线化,也就是说,它们可以用一个函数的结果调用另一个函数。以下示例通过对其元素应用std::abs()函数,将整数范围映射为正整数范围。然后将结果映射到另一个正方形范围。然后,通过在范围上应用左折叠将它们相加:
auto vnums = std::vector<int>{ 0, 2, -3, 5, -1, 6, 8, -4, 9 };
auto s = funclib::foldl(
std::plus<>(),
funclib::mapf(
[](int const i) {return i*i; },
funclib::mapf(
[](int const i) {return std::abs(i); },
vnums)),
0); // s = 236作为一个练习,我们可以将 fold 函数实现为一个变量函数模板,就像前面的方法一样。执行实际折叠的函数作为参数提供:
template <typename F, typename T1, typename T2>
auto foldl(F&&f, T1 arg1, T2 arg2)
{
return f(arg1, arg2);
}
template <typename F, typename T, typename... Ts>
auto foldl(F&& f, T head, Ts... rest)
{
return f(head, foldl(std::forward<F>(f), rest...));
}当我们将其与我们在配方中编写的add()函数模板进行比较时,我们可以注意到几个不同之处:
- 第一个参数是一个函数,递归调用
foldl时完美转发。 - 最后一种情况是需要两个参数的函数,因为我们用于折叠的函数是二进制函数(取两个参数)。
- 我们编写的两个函数的返回类型被声明为
auto,因为它必须与所提供的二进制函数f的返回类型相匹配,这在我们调用foldl之前是不知道的:
auto s1 = foldl(std::plus<>(), 1, 2, 3, 4, 5);
// s1 = 15
auto s2 = foldl(std::plus<>(), "hello"s, ' ', "world"s, '!');
// s2 = "hello world!"
auto s3 = foldl(std::plus<>(), 1); // error, too few arguments- 创建一个字符串助手库第 9 章、处理数字和字符串
- 用可变数量的参数编写函数模板
- 将函数组合成更高阶的函数
在前面的食谱中,我们实现了两个更高阶的函数,map 和 fold,并看到了使用它们的各种例子。在配方的最后,我们看到了如何对原始数据进行几次转换后,将它们流水线化以产生最终值。流水线是一种组合形式,这意味着从两个或多个给定函数创建一个新函数。在上面提到的例子中,我们实际上并没有组合函数;我们只调用了一个函数,结果由另一个函数产生,但是在这个食谱中,我们将看到如何将函数组合成一个新的函数。为了简单起见,我们将只考虑一元函数(只接受一个参数的函数)。
在继续之前,建议您阅读前面的配方,实现更高阶的函数映射和 fol d,理解这个配方并不是强制性的,但是我们会参考这里实现的映射和折叠函数。
要将一元函数组合成高阶函数,您应该:
- 对于组合两个函数,提供一个函数,该函数以两个函数
f和g作为参数,并返回一个新函数(λ),该函数返回f(g(x)),其中x是组合函数的参数:
template <typename F, typename G>
auto compose(F&& f, G&& g)
{
return [=](auto x) { return f(g(x)); };
}
auto v = compose(
[](int const n) {return std::to_string(n); },
[](int const n) {return n * n; })(-3); // v = "9"- 为了组成可变数量的函数,提供前面描述的函数的可变模板重载:
template <typename F, typename... R>
auto compose(F&& f, R&&... r)
{
return [=](auto x) { return f(compose(r...)(x)); };
}
auto n = compose(
[](int const n) {return std::to_string(n); },
[](int const n) {return n * n; },
[](int const n) {return n + n; },
[](int const n) {return std::abs(n); })(-3); // n = "36"将两个一元函数组合成一个新函数相对来说比较简单。创建一个我们在前面的例子中称为compose()的模板函数,用两个参数- f和g -表示函数,并返回一个接受一个参数x并返回f(g(x))的函数。重要的是g函数返回的值的类型与f函数的参数的类型相同。compose 函数的返回值是一个闭包,也就是 lambda 的一个实例。
实际上,能够将两个以上的功能结合在一起是非常有用的。这可以通过编写compose()函数的变量模板版本来实现。可变模板在用可变数量的参数编写函数模板中有更详细的解释。变量模板通过扩展参数包来暗示编译时递归。这个实现与第一版compose()非常相似,除了以下几点:
- 它采用可变数量的函数作为参数。
- 返回的闭包用扩展的参数包递归调用
compose();当只剩下两个函数时,递归结束,在这种情况下,调用之前实现的重载。
Even if the code looks like recursion is happening, this is not true recursion. It could be called compile-time recursion, but with every expansion, we get a call to another method with the same name but a different number of arguments, which does not represent recursion.
现在我们已经实现了这些变量模板重载,我们可以重写前面配方的最后一个例子,实现更高阶的函数映射和折叠。有了整数的初始向量,我们通过在每个元素上应用std::abs()将其映射到一个只有正值的新向量。然后,通过将每个元素的值加倍,将结果映射到一个新的向量。最后,通过将结果向量中的值与初始值 0:
auto s = compose(
[](std::vector<int> const & v) {
return foldl(std::plus<>(), v, 0); },
[](std::vector<int> const & v) {
return mapf([](int const i) {return i + i; }, v); },
[](std::vector<int> const & v) {
return mapf([](int const i) {return std::abs(i); }, v); })(vnums);构图通常用点(.)或星号(*)表示,如f . g或f * g。我们实际上可以在 C++ 中通过重载operator*来做类似的事情(试图重载操作符点没有什么意义)。类似于compose()函数,operator*应该可以处理任意数量的参数;因此,我们将有两个重载,就像compose()的情况一样:
- 第一个重载接受两个参数并调用
compose()返回一个新函数。 - 第二个重载是变量模板函数,它通过扩展参数包再次调用
operator*:
template <typename F, typename G>
auto operator*(F&& f, G&& g)
{
return compose(std::forward<F>(f), std::forward<G>(g));
}
template <typename F, typename... R>
auto operator*(F&& f, R&&... r) {
return operator*(std::forward<F>(f), r...);
}我们现在可以通过应用operator*来简化函数的实际组合,而不是更冗长的调用来组合:
auto n =
([](int const n) {return std::to_string(n); } *
[](int const n) {return n * n; } *
[](int const n) {return n + n; } *
[](int const n) {return std::abs(n); })(-3); // n = "36"
auto c =
[](std::vector<int> const & v) {
return foldl(std::plus<>(), v, 0); } *
[](std::vector<int> const & v) {
return mapf([](int const i) {return i + i; }, v); } *
[](std::vector<int> const & v) {
return mapf([](int const i) {return std::abs(i); }, v); };
auto s = c(vnums); // s = 76- 用可变数量的参数编写函数模板
开发人员,尤其是那些实现库的开发人员,有时需要以统一的方式调用可调用对象。这可以是函数、函数指针、成员函数指针或函数对象。这种情况的例子包括std::bind、std::function、std::mem_fn和std::thread::thread。C++ 17 定义了一个名为std::invoke()的标准函数,它可以用提供的参数调用任何可调用的对象。这并不是要取代对函数或函数对象的直接调用,而是在模板元编程中用于实现各种库函数。
对于这个配方,您应该熟悉如何定义和使用函数指针。
为了举例说明std::invoke()如何在不同的上下文中使用,我们将使用以下函数和类:
int add(int const a, int const b)
{
return a + b;
}
struct foo
{
int x = 0;
void increment_by(int const n) { x += n; }
};std::invoke()函数是一个变量函数模板,它将可调用对象作为第一个参数,并将变量列表传递给调用方。std::invoke()可用于调用以下内容:
- 免费功能:
auto a1 = std::invoke(add, 1, 2); // a1 = 3- 通过指向函数的指针释放函数:
auto a2 = std::invoke(&add, 1, 2); // a2 = 3
int(*fadd)(int const, int const) = &add;
auto a3 = std::invoke(fadd, 1, 2); // a3 = 3- 成员函数通过指向成员函数的指针:
foo f;
std::invoke(&foo::increment_by, f, 10);- 数据成员:
foo f;
auto x1 = std::invoke(&foo::x, f); // x1 = 0- 功能对象:
foo f;
auto x3 = std::invoke(std::plus<>(),
std::invoke(&foo::x, f), 3); // x3 = 3- Lambda 表达式:
auto l = [](auto a, auto b) {return a + b; };
auto a = std::invoke(l, 1, 2); // a = 3实际上,应该在模板元编程中使用std:invoke()来调用具有任意数量参数的函数。为了举例说明这种情况,我们给出了我们的std::apply()函数的一个可能的实现,也是 C++ 17 标准库的一部分,它通过将元组的成员解包成函数的参数来调用函数:
namespace details
{
template <class F, class T, std::size_t... I>
auto apply(F&& f, T&& t, std::index_sequence<I...>)
{
return std::invoke(
std::forward<F>(f),
std::get<I>(std::forward<T>(t))...);
}
}
template <class F, class T>
auto apply(F&& f, T&& t)
{
return details::apply(
std::forward<F>(f),
std::forward<T>(t),
std::make_index_sequence<
std::tuple_size<std::decay_t<T>>::value> {});
}在我们了解std::invoke()如何工作之前,让我们先简单了解一下不同的可调用对象是如何被调用的。显然,给定一个函数,无处不在的调用方法是直接向它传递必要的参数。但是,我们也可以使用函数指针来调用函数。函数指针的问题在于定义指针的类型可能很麻烦。使用auto可以简化事情(如下代码所示),但在实际中,通常需要先定义指向函数的指针的类型,然后定义一个对象,用正确的函数地址初始化。以下是几个例子:
// direct call
auto a1 = add(1, 2); // a1 = 3
// call through function pointer
int(*fadd)(int const, int const) = &add;
auto a2 = fadd(1, 2); // a2 = 3
auto fadd2 = &add;
auto a3 = fadd2(1, 2); // a3 = 3当需要通过作为类实例的对象调用类函数时,通过函数指针调用变得更加麻烦。定义指向成员函数的指针并调用它的语法并不简单:
foo f;
f.increment_by(3);
auto x1 = f.x; // x1 = 3
void(foo::*finc)(int const) = &foo::increment_by;
(f.*finc)(3);
auto x2 = f.x; // x2 = 6
auto finc2 = &foo::increment_by;
(f.*finc2)(3);
auto x3 = f.x; // x3 = 9不管这种调用看起来有多麻烦,实际的问题是编写能够以统一的方式调用这些类型的可调用对象的库组件(函数或类)。这就是标准函数在实践中的好处,例如std::invoke()。
std::invoke()的实现细节比较复杂,但其工作方式可以用简单的术语来解释。假设呼叫的形式为invoke(f, arg1, arg2, ..., argN),则考虑以下情况:
- 如果
f是指向T类的成员函数的指针,那么调用相当于:(arg1.*f)(arg2, ..., argN),如果arg1是T的实例(arg1.get().*f)(arg2, ..., argN),如果arg1是reference_wrapper的专精((*arg1).*f)(arg2, ..., argN),如果不是
- 如果
f是指向T类的数据成员的指针,并且有一个参数,换句话说,调用的形式是invoke(f, arg1),那么调用相当于:arg1.*f如果arg1是实例类Targ1.get().*f如果arg1是reference_wrapper的专精(*arg1).*f,如果不是
- 如果
f是一个函数对象,那么调用就相当于f(arg1, arg2, ..., argN)
- 用可变数量的参数编写函数模板