Callable
- 标准库定义了许多可调用实例的组件,这里有一个术语叫回调(callback),回调的含义是:对一个库,客户端希望库能够调用客户端自定义的某些函数,这种调用称为回调,比如一个排序函数可能包含一个回调参数作为排序规则。回调的概念原本是为仿函数保留的,仿函数通常以函数调用实参的形式传递给库
- C++中有几种用于回调的类型,它们能直接使用语法f(...)作为函数调用实参传递,这些类型统称为函数对象类型,这种类型的一个值就是一个函数对象
- 函数指针
- 带有重载operator()的类类型(有时叫functor)
- 带有产生一个函数指针或函数引用的转换函数的类类型
支持函数对象
// basics/foreach.hpp
template<typename Iter, typename Callable>
void foreach (Iter current, Iter end, Callable op)
{
while (current != end) { //as long as not reached the end
op(*current); // call passed operator for current element
++current; // and move iterator to next element
}
}
// basics/foreach.cpp
#include <iostream>
#include <vector>
#include "foreach.hpp"
// a function to call:
void func(int i)
{
std::cout << "func() called for: " << i << '\n';
}
// a function object type (for objects that can be used as functions):
class FuncObj {
public:
void operator() (int i) const { // Note: const member function
std::cout << "FuncObj::op() called for: " << i << '\n';
}
};
int main()
{
std::vector<int> primes = { 2, 3, 5, 7, 11, 13, 17, 19 };
foreach(primes.begin(), primes.end(), // range
func); // function as callable (decays to pointer)
foreach(primes.begin(), primes.end(), // range
&func); // function pointer as callable
foreach(primes.begin(), primes.end(), // range
FuncObj()); // function object as callable
foreach(primes.begin(), primes.end(), // range
[] (int i) { // lambda as callable
std::cout << "lambda called for: " << i << '\n';
});
}
- 将函数名称作为函数实参传递时,传递的其实是它的指针或引用,就像数组一样,函数可以被不衰退地传递为引用,然而函数类型不能被const修饰,如果用Callable const&类型,const会被忽略
- 第二个显式指定指针的调用与第一个等价(函数名称隐式衰退为指针值)但可能更清晰
- 传递仿函数时,传递的是一个类类型对象作为回调,调用一个类类型通常是调用它的operator(),注意定义operator()时应该将其定义为const成员函数
op(*current);
// 通常会转换为
op.operator()(*current); // call operator() with parameter *current for op
- 一个类类型对象也可能饮食转换为一个代理调用函数的指针或引用,下面的F是类类型对象能转换为的函数指针或函数引用类型,这是很不寻常的
op(*current);
// 将转换为
(op.operator F())(*current);
- C++11开始,可以方便地用lambda表达式产生仿函数(被称为闭包)。有趣的是,[]开头(没有捕获)的lambda产生一个函数指针的转换运算符,但总不会被选为代理调用函数,因为它在匹配时总是被闭包的operator()更差
处理成员函数和附加实参
- 之前的例子中没有用到成员函数,因为通常调用一个non-static成员函数只要直接X.f(...),这不符合函数对象的模式
- C++17开始,标准库提供了std::invoke(),方便地将这种情况与普通函数调用语法结合
// basics/foreachinvoke.hpp
#include <utility>
#include <functional>
template<typename Iter, typename Callable, typename... Args>
void foreach (Iter current, Iter end, Callable op, Args const&...
args)
{
while (current != end) {
std::invoke(op, //call passed callable with
args..., // any additional args
*current); // and the current element
++current;
}
}
- 这里除了callable参数,也能接收任意数量的附加参数。如果callable是一个指向成员的指针,使用第一个附加实参作为this对象,其余所有的附加参数都只作为实参传递给callable
- 否则,所有的附加参数只被作为实参传递给callable
- 注意,这里不能为callable或附加参数使用完美转发,第一个调用可能窃取它们的值,导致随后迭代调用op的非预期行为
- 使用这个实现
#include <iostream>
#include <vector>
#include <string>
#include "foreachinvoke.hpp"
// a class with a member function that shall be called
class MyClass {
public:
void memfunc(int i) const {
std::cout << "MyClass::memfunc() called for: " << i << '\n';
}
};
int main()
{
std::vector<int> primes = { 2, 3, 5, 7, 11, 13, 17, 19 };
// pass lambda as callable and an additional argument:
foreach(primes.begin(), primes.end(), // elements for 2nd arg of lambda
[](std::string const& prefix, int i) { // lambda to call
std::cout << prefix << i << '\n';
},
"- value: "); // 1st arg of lambda
// call obj.memfunc() for/with each elements in primes passed as argument
MyClass obj;
foreach(primes.begin(), primes.end(), // elements used as args
&MyClass::memfunc, // member function to call
obj); // object to call memfunc() for
}
包裹函数调用
- std::invoke()的一个常见应用是包裹单个函数调用,现在可以通过完美转发callable和所有传递的实参支持移动语义
#include <utility> // for std::invoke()
#include <functional> // for std::forward()
template<typename Callable, typename... Args>
decltype(auto) call(Callable&& op, Args&&... args)
{
return std::invoke(std::forward<Callable>(op), // passed callable with
std::forward<Args>(args)...); // any additional args
}
- 另一个有趣的方面是如何处理被调用函数的返回值,完美转发给调用者,为了支持返回引用(如std::ostream&),使用decltype(auto)代替auto
template<typename Callable, typename... Args>
decltype(auto) call(Callable&& op, Args&&... args)
- 如果想临时存储std::invoke()返回的值,也必须用decltype(auto)声明临时变量
decltype(auto) ret{std::invoke(std::forward<Callable>(op),
std::forward<Args>(args)...)};
...
return ret;
- 注意,把ret声明为auto&&是不正确的,auto&&作为一个引用,生命周期不会超出return语句
- 但使用decltype(auto)也有一个问题,如果callable有void的返回类型,把ret初始化为decltype(auto)是不允许的,因为void是一个不完整的类型
- 一个解决方法是在那条语句之前声明一个对象,该对象的析构函数执行你希望实现的可观察的行为
struct cleanup {
~cleanup() {
... // code to perform on return
}
} dummy;
return std::invoke(std::forward<Callable>(op),
std::forward<Args>(args)...);
- 另一个方法是使用if constexpr不同地实现void和non-void
#include <utility> // for std::invoke()
#include <functional> // for std::forward()
#include <type_traits> // for std::is_same<> and invoke_result<>
template<typename Callable, typename... Args>
decltype(auto) call(Callable&& op, Args&&... args)
{
if constexpr(std::is_same_v<std::invoke_result_t<Callable, Args...>, void>) {
// return type is void:
std::invoke(std::forward<Callable>(op),
std::forward<Args>(args)...);
//...
return;
}
else {
// return type is not void:
decltype(auto) ret{std::invoke(std::forward<Callable>(op),
std::forward<Args>(args)...)};
// ...
return ret;
}
}
其他实现泛型库的实用工具
Type Traits
- 标准库提供了称为type traits的各种实用工具,允许评估和修饰类型
#include <type_traits>
template<typename T>
class C
{
// ensure that T is not void (ignoring const or volatile):
static_assert(!std::is_same_v<std::remove_cv_t<T>,void>,
"invalid instantiation of class C for void type");
public:
template<typename V>
void f(V&& v) {
if constexpr(std::is_reference_v<T>) {
... // special code if T is a reference type
}
if constexpr(std::is_convertible_v<std::decay_t<V>,T>) {
... // special code if V is convertible to T
}
if constexpr(std::has_virtual_destructor_v<V>) {
... // special code if V has virtual destructor
}
}
};
- 然而,注意type traits使用时可能与预期表现不符
std::remove_const_t<int const&> // yields int const&
- 这里是引用不是const,所以调用没有效果,因此移除引用和const的顺序是
std::remove_const_t<std::remove_reference_t<int
const&>> // int
std::remove_reference_t<std::remove_const_t<int
const&>> // int const
std::decay_t<int const&> // yields int
- 但会转化原始数组和函数为对应的指针类型
- 也会有type traits有要求的情况,不满足要求会导致未定义行为
make_unsigned_t<int> // unsigned int
make_unsigned_t<int const&> // undefined behavior (hopefully error)
add_rvalue_reference_t<int> // int&&
add_rvalue_reference_t<int const> // int const&&
add_rvalue_reference_t<int const&> // int const& (lvalueref
remains lvalue-ref)
- 上例中因为引用折叠规则,产生了左值引用。另一个例子如下
is_copy_assignable_v<int> // yields true (generally, you can assign an int to an int)
is_assignable_v<int, int> // yields false (can't call 42 = 42)
- is_copy_assignable只检查能否把int赋给另一个(检查左值操作),is_assignable则考虑到值类型(这里检查能否把右值赋给右值),因此第一个表达式等价于
is_assignable_v<int&,int&> // yields true
is_swappable_v<int> // yields true (assuming lvalues)
is_swappable_v<int&,int&> // yields true (equivalent to the previous check)
is_swappable_with_v<int,int> // yields false (taking value category into account)
std::addressof()
- std::addressof<>()函数模板产生一个函数或对象的地址,即使对象类型有一个重载运算符&,因此如果需要任何类型的对象的地址都推荐使用std::addressof()
template<typename T>
void f (T&& x)
{
auto p = &x; // might fail with overloaded operator &
auto q = std::addressof(x); // works even with overloaded operator &
...
}
std::declval()
- std::declval<>()函数模板能用于一个具体类型的对象的引用,这个函数没有定义因此不能被调用(并且不创建一个对象),因此只能被未评估的操作数(如decltype和sizeof构造)
- 比如下面的声明从T1和T2推断默认返回类型RT,为了避免调用T1和T2的构造函数,使用std::declval获取对应对象但不创建。不要忘记使用std::decay<>确保默认返回类型不能为引用,因为std::declval()本身产生右值引用
#include <utility>
template<typename T1, typename T2,
typename RT = std::decay_t<decltype(true ?
std::declval<T1>() : std::declval<T2>())>>
RT max (T1 a, T2 b)
{
return b < a ? a : b;
}
完美转发临时对象
- 可以使用转发引用和std::forward>?完美转发泛型参数
template<typename T>
void f (T&& t) // t是转发引用
{
g(std::forward<T>(t)); // 完美转发实参t给g()
}
- 然而有时必须完美转发不来自参数的数据,这种情况下可以使用auto&&创建变量用于转发。比如假设把get()的返回值完美转发给set()
template<typename T>
void foo(T x)
{
set(get(x));
}
- 如果以后想改动代码来对get()产生的值执行操作,可以把值保存在一个auto&&声明的变量中
template<typename T>
void foo(T x)
{
auto&& val = get(x);
...
// perfectly forward the return value of get() to set():
set(std::forward<decltype(val)>(val));
}
引用作为模板参数
#include <iostream>
template<typename T>
void tmplParamIsReference(T) {
std::cout << "T is reference: " << std::is_reference_v<T> <<
'\n';
}
int main()
{
std::cout << std::boolalpha;
int i;
int& r = i;
tmplParamIsReference(i); // false
tmplParamIsReference(r); // false
tmplParamIsReference<int&>(i); // true
tmplParamIsReference<int&>(r); // true
}
- 即使引用变量传给模板,T还是被推断为引用类型的原有类型(因为对于引用变量v,表达式v是引用类型,而表达式的类型则不会是引用)。而显式指定则可以强制T为引用,一些模板可能设计时没有考虑这个可能性,于是引发错误和未定义行为
template<typename T, T Z = T{}>
class RefMem {
private:
T zero;
public:
RefMem() : zero{Z} {
}
};
int null = 0;
int main()
{
RefMem<int> rm1, rm2;
rm1 = rm2; // OK
RefMem<int&> rm3; // ERROR: invalid default value for N
RefMem<int&, 0> rm4; // ERROR: invalid default value for N
extern int null;
RefMem<int&,null> rm5, rm6;
rm5 = rm6; // ERROR: operator= is deleted due to reference member
}
- 对非类型模板参数使用引用类型也很tricky和危险
#include <vector>
#include <iostream>
template<typename T, int& SZ> // Note: size is reference
class Arr {
private:
std::vector<T> elems;
public:
Arr() : elems(SZ) { // use current SZ as initial vector size
}
void print() const {
for (int i=0; i<SZ; ++i) { // loop over SZ elements
std::cout << elems[i] << ' ';
}
}
};
int size = 10;
int main()
{
Arr<int&,size> y; // compile-time ERROR deep in the code of class std::vector<>
Arr<int,size> x; // initializes internal vector with 10 elements
x.print(); // OK
size += 100; // OOPS: modifies SZ in Arr<>
x.print(); // run-time ERROR: invalid memory access: loops over 120 elements
}
- 上面这个例子有些牵强,但在更复杂的情况下确实可能发生,在C++17中非类型参数可以被推断,比如
template<typename T, decltype(auto) SZ>
class Arr;
- 使用decltype(auto)很容易产生引用类型。因此通常在这里会默认使用auto,标准库因此也有一些令人惊讶的规约限制,比如即使模板参数初始化为引用,为了仍然有赋值运算符,std::pair<>和std::tuple<>实现了赋值运算符,而不是使用默认行为
namespace std {
template<typename T1, typename T2>
struct pair {
T1 first;
T2 second;
...
// default copy/move constructors are OK even with references:
pair(pair const&) = default;
pair(pair&&) = default;
...
// but assignment operator have to be defined to be
available with references:
pair& operator=(pair const& p);
pair& operator=(pair&& p) noexcept(...);
...
};
}
- 又比如因为可能造成的副作用的复杂性,C++17的类模板std::optional<>和std::variant<>对引用是非法的
- 为了禁用引用,简单的static断言就足够了
template<typename T>
class optional
{
static_assert(!std::is_reference<T>::value,
"Invalid instantiation of optional<T> for references");
...
};
延迟评估(Defer Evaluation)
- 实现模板时,有时代码是否能处理不完整类型也会引发问题
template<typename T>
class Cont {
private:
T* elems;
public:
...
};
struct Node
{
std::string value;
Cont<Node> next; // only possible if Cont accepts
incomplete types
};
- 然而如果使用一些traits,可能就会失去处理不完整类型的能力
template<typename T>
class Cont {
private:
T* elems;
public:
...
typename
std::conditional<std::is_move_constructible<T>::value,
T&&,
T&
>::type
foo();
};
- 这里用trait std::conditional决定返回类型为T&&还是T&,这依赖于T是否支持移动语义。问题在于is_move_constructible要求实参是完整类型(且不是void或一个数组的未知绑定),因此这个声明失败,带有这个声明的struct node声明也会失败
- 可以用一个成员模板替代foo()解决问题,这样std::is_move_constructible的评估会延迟到foo()的实例化点
template<typename T>
class Cont {
private:
T* elems;
public:
template<typename D = T>
typename
std::conditional<std::is_move_constructible<D>::value,
T&&,
T&
>::type
foo();
};
编写泛型库时需要考虑的东西
- 使用转发引用转发模板中的值,如果值不依赖于模板参数,使用auto&&
- 当参数被声明为转发引用,传递左值时,准备好模板参数会是引用类型
- 需要一个依赖于模板参数的地址时,使用std::addressof()以防参数被绑定到一个重载了operator&的类型
- 对成员函数模板,确保它们不是比预定义拷贝/移动构造函数或赋值运算符更好的匹配
- 当模板参数可能是字符串字面值和不是传值传递时,考虑使用std::decay
- 如果有依赖于模板参数的out或inout参数,准备好处理const模板实参
- 准备好处理模板参数为引用的副作用,尤其是想确保返回类型不能变成一个引用时
- 准备好处理对不完整类型的支持,比如递归数据结构
- 对所有数组类型重载,而不只是T[SZ]
网友评论