打开导航 打开导航 Open menu
量化交易系统开发(C++)/ 期权策略与风控 / 行业研究分析
量化交易系统開發(C++)/ 期权策略与风控 / 行业研究分析
C++ trading systems / options strategy & risk controls / equity research
adrian@adrianxv.cn
文章目录

C++:模板 C++:模板 C++:模板

template 提供:

  1. 参数化类型:用类型参数生成具体类型
  2. 参数化操作:函数模板、函数对象、Lambda
  3. 编译期实例化
  4. 类型检查(特别是有 C++ 20 引入的 Concept 之后)
  5. 零运行时抽象成本:template 不会因为泛化带来运行时开销(因为 template 是编译期机制)

template 当中的参数

类型参数 template

无需多言

值参数 template<typename T, int N>

模板参数可以是编译期值,而不只是类型

template<typename T, int N>
struct Buffer {
    T elem[N];
}

Buffer<int, 10> b;

注意和构造函数当中的参数区分开来。

std::vector<int> v(10, 0);

这里的 (10, 0) 是构造函数的参数,而不是模板参数

注意 C 风格字符串的类型,即 const char* 类型,不能直接作为 typename 的值。具体原因不必深究,要使用字符串作为类型的时候,使用 std::string 即可。

参数推导:编译器如何得到 T / N

如果每次使用模板时都要写类型,会比较 tedious。好消息是编译器有参数推导机制,让编译器自己推导类型。

可以从构造函数推导:

pair p = {1, 1.89};
vector v = {1, 2, 3, 4};

这里的 pair 会被推导为 pair<int, double>; vector 被推导为 vector

但是有时推导会导致歧义,以下是一个由 vector 的两种构造函数导致的歧义(initialization-list constructor 和 iterator-pair constructor 两种)

std::vector<int> src{1, 2, 3, 4, 5};
// 获得两个迭代器
auto b = src.begin();
auto e = b + 2;

std::vector<int> v1(b, e);  // 被看作 iterator-pair,得到 {1, 2}
std::vector<int> v2{b, e};  // 被看作迭代器类型的 initializer-list,得到包含两个迭代器的 vector

使用 {} 的时候,如果存在可行的 initializer_list 构造函数,列表初始化会优先选择它;否则才考虑其它构造函数。

参数化操作

上面讲的都是如何在一个容器或者自定义类型当中使用参数,使得它们承载的具体类型可以在编译期推导。而这里参数化操作讲的是如何把参数用于函数当中,让同一算法适用于不同类型。

函数模板

函数的形式参数的类型模板化,具体类型根据实际参数进行推导。以下这个函数允许用 double 累加 int,避免溢出。因为 int 的范围大约 -2.1x10^9 ~ 2.1x10^9,而 double 最大约 1.8x10^308,double 的范围远远大于 int,用 double 作为累加的基础,能累加的数字的范围更大。

template<typename Seq, typename Val>
Val sum(Seq& seq, Val init) {
    for (auto x: seq) {
        init += x;
    }
    return init;
}

sum(v, 0);  // 这样这个函数处理与返回 int
sum(v, 0.0) // 这样这个函数处理与返回 double

函数模板不能是 virtual 成员函数,因为

函数对象

函数对象本质上是定义了 operator() 的对象。因为它定义了括号 () 这个 operator,所以它可以像函数一样调用。因为它是对象,所以它可以携带状态。

函数对象(以及下面要讲的 Lambda)之所以属于参数化操作,是因为它们把 “具体操作” 作为参数传给另一个通用算法。这一点在 Lambda 上体现最明显,因为 Lambda 经常被作为谓词传给 STL 算法。

struct Square {
    int count = 0;

    int operator() (int x) {
        count++;
        return x * x;
    }
};

Square f;
f(3);  // 返回 9
f(4);  // 返回 16

std::cout << f.count; // 输出 2

函数对象作为算法谓词参数。谓词对象的本质上是一个 “可调用的对象”。换句话说,函数对象和 lambda 之所以可以当作谓词使用,是因为他们本质上还是对象。

谓词是一个较广泛的概念,表示能判断条件的可调用实体。为了简化理解,我们认为 “一般的谓词” 指的就是一个普通布尔函数;“谓词对象” 指的是函数对象和 Lambda,因为它们的本质是对象。

struct IsPositive {
    bool operator() (int x) {
        return x > 0;
    }
}

std::find_if(v.begin(), v.end(), is_positive{});

一般是这样做的,在需要谓词的地方使用 {} 调用构造函数,临时构造一个对象。注意不要调用成 () 了,因为 () 操作符意味着立刻调用 operator(),这不是谓词的需求。

把 “谓词对象” 的概念推广,可以得到 policy object,表示 “提供某种可替换的行为策略” 的对象。这包括比较策略、内存分配策略、重试策略、日志策略等。正式、可复用的策略一般用一般的函数对象;一次性的策略一般用 lambda。但是这只是惯例而已。

Lambda

Lambda 其实是函数对象的简写。因此它也可看作 “会保存状态的函数”。

Lambda 的 [] 用来写要从环境中捕获的对象。这个对象实质上是函数对象所保存的状态。() 用来写每次调用时需要传入的参数。[]() 的区别在于,前者当作的对象在创建 lambda 的时候就已经被保存为状态了,无需每次传入;而后者当作的参数,在每次调用时都需要传入。

但是 lambda 最常见的用途还是作为 “当场定义,当场调用” 的简易函数,用在 STL 算法当中。很多时候不需要从环境中捕获状态,因此 [] 为空。lambda 并不需要显式规定返回类型,实际返回类型由编译期推导,效果类似 auto。

std::sort(v.begin(), v.end(), [](int a, int b) { return a < b; });

必须从环境中捕获状态的例子:

int threshold = 10;

auto it = std::find_if(v.begin(), v.end(), [threshold](int x) { return x >  threshold; });

上述这样在 [] 当中直接写明要捕获的对象名 x,实际上是按值捕获。除此之外还可以: 0. 仍然用 [x] 这样捕获,但是加上 mutable 允许修改副本:auto f = x mutable {x++;};

  1. [&x] 表示按照引用捕获环境中的 x,可以在 lambda 内直接修改原对象
  2. [=] 表示按值捕获 lambda 需要用到的所有外部变量,effectively 相当于按值捕获所有外部变量
  3. [&] 表示按引用捕获 lambda 需要用到的所有外部变量
  4. [this] 表示获取指向当前对象的指针
  5. [*this] 表示把当前整个对象复制一份进 lambda

前面在函数模板当中讲过函数模板的参数类型由调用场景决定,从而让函数算法适用于不同具体类型,与lambda 也可做到这一点,这称为泛型 lambda:

[](auto& x) { x->do_something(); };

这样同一个 lambda 可以被多种操作使用,只要涉及的 x 支持 ->do_something()

lambda 本身没有名字,但是可以赋值给一个对象。在赋值给一个对象之后,完全可以像使用普通函数对象那样使用这个对象。

auto cmp = [](int a, int b){ return a < b;};
std::sort(v.begin(), v.end(), cmp);

的效果与下面的代码的效果完全一致

struct Cmp {
    bool operator() (int a, int b) { return a < b; };
}
Cmp cmp{};
std::sort(v.begin(), v.end(), cmp);

编译期选择与组合

变量模板

这里与 std::vector / std::list 那一套模板的不同之处在于,std::vector / std::list 是把类/容器里的元素类型参数化,这些叫类模板;而此处讨论的变量模板是把某个变量的类型或值参数化。

把类型做成模板,而不把值做成模板,则要求选定的值对于多种类型都有效。最简单的例子莫过于选定 0:

template<typename T>
T zero = T{0};

int int_zero = zero<int>;
double double_zero = zero<double>;

if constexpr

if constexpr 在编译期选择分支

template<typename T>
void f(T x) {
    if constexpr (std::is_pointer(x)) {
        // 在编译期判断,如果是一个指针的话做某些操作
    }
    else {
        // 如果不是指针的话做某些操作
    }
}