回调
- 回调的含义是:对一个库,用户希望库能够调用用户自定义的某些函数,这种调用称为回调。C++中用于回调的类型统称为函数对象类型,它们能直接用作函数实参
#include <iostream>
#include <vector>
template<typename Iter, typename Callable>
void foreach(Iter current, Iter end, Callable op)
{
while (current != end)
{
op(*current);
++current;
}
}
void func(int i)
{
std::cout << i << '\n';
}
struct FuncObj {
void operator()(int i) const
{
std::cout << i << '\n';
}
};
int main()
{
std::vector<int> primes = { 2, 3, 5, 7, 11, 13, 17, 19 };
foreach(primes.begin(), primes.end(), func);
foreach(primes.begin(), primes.end(), &func);
foreach(primes.begin(), primes.end(), FuncObj());
foreach(primes.begin(), primes.end(), [] (int i) { std::cout << i << '\n'; });
}
处理成员函数和附加实参
// basics/foreachinvoke.hpp
#include <utility>
#include <functional>
template<typename Iter, typename Callable, typename... Args>
void foreach (Iter current, Iter end, Callable op, const Args&... args)
{
while (current != end)
{
std::invoke(op, args..., *current);
++current;
}
}
- 这里除了函数对象,还能接收任意数量的附加参数。如果函数对象是一个类成员指针,使用第一个附加实参作为this对象,其余作为实参传递给函数对象,否则所有附加参数都只传递给函数对象
#include <iostream>
#include <vector>
#include <string>
#include "foreachinvoke.hpp"
class A {
public:
void f(int i) const
{
std::cout << i << '\n';
}
};
int main()
{
std::vector<int> primes = { 2, 3, 5, 7, 11, 13, 17, 19 };
foreach(primes.begin(), primes.end(), // 范围内的元素是lambda的第二个参数
[](std::string const& prefix, int i) {
std::cout << prefix << i << '\n';
},
"value: "); // lambda的第一个参数
A obj;
foreach(primes.begin(), primes.end(), &A::f, obj);
}
包裹函数调用(Wrapping Function Call)
-
std::invoke的一个常见应用是包裹单个函数调用,为了支持返回引用(如std::ostream&),这里使用decltype(auto)替代auto
template<typename Callable, typename... Args>
decltype(auto) call(Callable&& op, Args&&... args)
{
return std::invoke(std::forward<Callable>(op),
std::forward<Args>(args)...);
}
template<typename Callable, typename... Args>
decltype(auto) call(Callable&& op, Args&&... args)
{
decltype(auto) ret{std::invoke(std::forward<Callable>(op),
std::forward<Args>(args)...)};
return ret;
}
- 注意,把ret声明为auto&&是不正确的,auto&&作为一个引用,生命周期不会超出return语句
- 但使用decltype(auto)也有一个问题,如果函数对象返回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实现不同的分支
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>)
{
std::invoke(std::forward<Callable>(op), std::forward<Args>(args)...);
return;
}
else
{
decltype(auto) ret{std::invoke(std::forward<Callable>(op),
std::forward<Args>(args)...)};
return ret;
}
}
实现泛型库的其他工具
#include <type_traits>
template<typename T>
class C {
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>)
{
... // 如果T是引用类型
}
if constexpr(std::is_convertible_v<std::decay_t<V>,T>)
{
... // 如果V能转换为T
}
if constexpr(std::has_virtual_destructor_v<V>)
{
... // 如果V有析构函数
}
}
};
std::remove_const_t<const int&> // 生成const int&
- 这里是引用不是const,所以调用没有效果。移除引用和const的顺序不同会导致不同的结果
std::remove_const_t<std::remove_reference_t<const int&>> // int
std::remove_reference_t<std::remove_const_t<const int&>> // const int
std::decay_t<const int&> // int
make_unsigned_t<int> // unsigned int
make_unsigned_t<const int&> // undefined behavior (hopefully error)
add_rvalue_reference_t<int> // int&&
add_rvalue_reference_t<const int> // const int&&
add_rvalue_reference_t<const int&> // const int&(由于引用折叠,左值引用仍为左值引用)
is_copy_assignable_v<int> // true(一把可以把int赋给int)
is_assignable_v<int, int> // false(不能调用42 = 42)
is_assignable_v<int&, int&> // true
is_swappable_v<int> // true(假设是左值)
is_swappable_v<int&, int&> // true(等价于上一行)
is_swappable_with_v<int, int> // false(考虑值类型)
std::addressof
template<typename T>
void f(T&& x)
{
auto p = &x; // 如果重载了operator&就可能失败
auto q = std::addressof(x); // 即使重载了operator&也能工作
...
}
std::declval
struct Default { int foo() const { return 1; } };
struct NonDefault
{
NonDefault(const NonDefault&) {}
int foo() const { return 1; }
};
int main()
{
decltype(Default().foo()) n1 = 1; // n1类型为int
decltype(NonDefault().foo()) n2 = n1; // 错误:无默认构造函数
decltype(std::declval<NonDefault>().foo()) n2 = n1; // n2类型为int
}
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;
}
完美转发临时对象
template<typename T>
void f(T&& x)
{
g(std::forward<T>(x)); // 完美转发实参x给g()
}
template<typename T>
void f(T x)
{
g(doSomething(x));
}
- 如果想在转发前修改要转发的值,可以用auto&&存储结果,修改后再转发
template<typename T>
void f(T x)
{
auto&& res = doSomething(x);
doSomethingElse(res);
set(std::forward<decltype(res)>(res));
}
模板参数为引用的情况
#include <iostream>
template<typename T>
void tmplParamIsReference(T)
{
std::cout << std::is_reference_v<T> << '\n';
}
int main()
{
std::cout << std::boolalpha; // 之后打印true将为true而不是1
int i;
int& r = i;
tmplParamIsReference(i); // false
tmplParamIsReference(r); // false
tmplParamIsReference<int&>(i); // true
tmplParamIsReference<int&>(r); // true
}
- 而显式指定则可以强制T为引用,一些模板设计时没有考虑这个问题,就可能引发错误和未定义行为
template<typename T, T Z = T{}>
class RefMem {
public:
RefMem() : zero{Z} {}
private:
T zero;
};
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
}
#include <vector>
#include <iostream>
template<typename T, int& SZ> // 注意:SZ是引用
class Arr {
public:
Arr() : elems(SZ) {}
void print() const
{
for (int i = 0; i < SZ; ++i)
{
std::cout << elems[i] << ' ';
}
}
private:
std::vector<T> elems;
};
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(...);
...
};
}
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; // 只有Cont能接受不完整类型时可行
};
template<typename T>
class Cont {
public:
std::conditional_t<std::is_move_constructible_v<T>, T&&, T&> foo();
private:
T* elems;
};
template<typename T>
class Cont {
public:
template<typename U = T>
std::conditional_t<std::is_move_constructible_v<U>, T&&, T&> foo();
private:
T* elems;
};
编写泛型库的考虑事项
- 使用转发引用完美转发模板中的值。如果值需要改动,使用auto&&存储值
- 当参数被声明为转发引用,传递左值时,模板参数会被推断为引用类型
- 需要一个依赖于模板参数的地址时,使用std::addressof以防参数被绑定到一个重载了
operator&
的类型
- 确保成员函数模板不是比默认的拷贝/移动构造函数或赋值运算符更好的匹配
- 当模板参数可能是字符串字面值并且不是按值传递时,考虑使用std::decay
- 如果需要一个输入输出参数,它返回一个新对象或允许修改实参,传non-const引用(也可以按指针传递),但注意要考虑意外接收const对象的情况
- 考虑模板参数为引用的情况,尤其是想确保返回类型不能变成一个引用时
- 考虑对不完整类型的支持,比如递归的数据结构
- 对所有数组类型重载,而不只是
T[SZ]
网友评论