2020年4月

std::variant应当是用来替代union来使用的,后者从C时代就有了,但是它缺乏类型安全的保障。boost库很早之前就有了实现(其实从C++11开始,感觉boost不知道多少东西被借鉴走了)。 std::variant的类长这个样子:

template <class... Types>
class variant;

Usage

使用起来也是比较方便的

std::variant<int, double> v1;
v1 = 1; // activate the "int" member
assert(v1.index() == 0);
assert(std::get<0>(v1) == 1);

v1 = 3.14; // activate the "double" member
assert(v1.index() == 1);
assert(std::get<1>(v1) == 3.14);
assert(std::get<double>(v1) == 3.14);

assert(std::holds_alternative<int>(v1) == false);
assert(std::holds_alternative<double>(v1) == true);

assert(std::get_if<int>(&v1) == nullptr);
assert(*std::get_if<double>(&v1) == 3.14);

上面代码里的几个好用的constexpr函数:

  • std::get<>:可以传入index或是type。如果index或者类型不对的话,会跑出std::bad_variant_access异常;
  • std::get_if<>:这个是不抛异常的版本。注意参数和返回值类型都是指针——如果没有有效元素,那就会返回nullptr;
  • std::holds_alternative<Type>:判断一个Type在不在定义的variant参数类里面;

Visiting variants

如果要根据当前存储的类型来对variant对象做一些操作,第一会想到写一些if语句

if (std::holds_alternative<int>(v1) == true) {
  // handle int value
}
else {
  // handle double value
}

但是如果variant的可选类型比较多的话,定义一个Visitor类会有助于按类型来执行操作,逻辑会显得清晰

struct Visitor {
  double operator() (double d) { return d; }
  double operator() (int i) { return double(i); }
};

void show(std::variant<double, int> v) {
  std::cout >> std::visit(Visitor{}, v) << std::endl;
}

void test() {
  show(3.14);
  show(1);
}

std::generate是“遍历——执行”工具函数的一种

#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

int main()
{
  std::vector<std::string> v(4);
  std::generate(v.begin(), v.end(), [i=0]() mutable {
      return ++i % 2 ? "hello" : "world";
  });

  for (auto&& s : v) {
    std::cout << s << " ";
  }
  return 0;
}

上面的函数输出是hello world hello world.

std::generate(beginIt, endIt, generator)函数接受起止的迭代器以及一个生成方法。

看代码说话

template <typename Iterator>
auto distance(Iterator begin, Iterator end) {
  using Traits = std::iterator_traits<Iterator>;
  if constexpr (std::is_base_of_v<std::random_access_iterator_tag, typename Traits::iterator_category>) {
    return end - begin;
  } else {
    auto result = typename Traits::difference_type();
    for (auto it = begin; it != end; ++it) {
      ++result;
    }
    return result;
  }
}

上面的代码用来判断两个iterator之间的距离。主要逻辑是判断iterator是否是可随机访问的iterator然后作分支处理,可以随机访问的迭代器直接把iterator相减。 在constexpr if语句中,条件的值必须是可按语境转换到 bool 类型的经转换常量表达式。若其值为 true,则舍弃 false分支语句(若存在),否则舍弃 true分支语句。有意思的一点是,被舍弃语句中的 return 语句不参与函数返回类型推导,也就是说两个分支语句中的return推倒出来的类型允许不一样。

如果没有C++17这种怎么写呢?就是比较麻烦。 一可以加一个参数,但是每个tag写一个

template <typename Iterator>
auto distance(Iterator begin, Iterator end, std::bidirectional_iterator_tag); // one for each tag type

或者用SFINAE写两个

template <typename Iterator>
auto distance(Iterator begin, Iterator end, 
              typename std::enable_if_t<std::is_base_of_v<一大坨>>* = nullptr);

这个域名也是突发奇想买到的,作为unique-ptr.com,为了对得起这个名儿,那就说一下智能指针算了。

前传。裸指针和auto_ptr

裸指针就是高度自治(要自己管)的指针,在符合基本法(误)的前提下,使用裸指针的人可以为所欲为。裸指针的一个问题是,它有点不符合现代C++内存管理的指导思想,使用者必须对它的生命周期负责,否则有可能发生:

  • 内存泄漏(memory leak):如果new一大片内存但是由于代码复杂到头来忘了释放,就有可能耗尽内存资源;内存泄漏还有一种情况:如果申请内存做事情做了一半抛出了异常——代码跑到了exception的花括号里之后,原来申请的内存直接没办法拿到并且释放;
  • 悬挂指针(dangling reference):指针已经被删除了,但其内存仍然在使用中;另外还有双重删除(double delete)的问题,当程序的某部分要删除一个已经被删除的指针时,即可出现这种情况;还有一种,指针所指的内存已经失效了(比如出了作用域了),但是后面的代码还用着指针;

应该是为了缓解这类问题,C++ 98标准搞出来了个智能指针:auto_ptr。相当于加了一个“管家”,在对象的内部保存那个裸指针,在管家析构的时候自动释放裸指针的内容,如果赋值的话那就把裸指针的所有权转交给新的管家。 但是由于移动(move)语义是C++11才有的,auto_ptr在实现的时候,如果在两个智能指针对象之间作赋值(operator=)或者拷贝构造,它的参数都是非const类型的。为啥呢?因为它是会调用original_auto_ptr.release()方法拿到原始裸指针。 这个有点无奈的操作是为了实现指针所指对象的“占有权”管理:两个auto_ptr不允许管理同一个目标裸指针。在“拷贝”的时候,相当于做了“主权移交”。

Copying an auto_ptr copies the pointer and transfers ownership to the destination: both copy construction and copy assignment of auto_ptr modify their right hand arguments, and the "copy" is not equal to the original. -- https://en.cppreference.com/w/cpp/memory/auto_ptr

这种操作并不符合常人对“拷贝“的理解,更像是”剪切“操作。且正是因为这个问题,切不可把auto_ptr放到stl容器里面,否则会得到各种编译错误。典型案例是这个:https://stackoverflow.com/questions/111478/why-is-it-wrong-to-use-stdauto-ptr-with-standard-containers 另外由于析构的时候是用delete,auto_ptr里也不能放指针数组(那种需要用delete[]).

unique_ptr

不同于auto_ptr,unique_ptr 的赋值运算符只接受典型地由 std::move 生成的右值。刚才说了,“右值”是C++11移动语义的产物,它比较好地解决了主权移交时的尴尬问题。这怎么说呢,一个智能指针的命运啊,当然要靠自我奋斗,但是也要考虑到历史的行程。 回顾看之前说的裸指针的两个问题:

  • 内存泄漏:由于析构函数里会delete管理的裸指针对象,所以在跑出对象作用域的时候,编译器会帮我们解决好问题,不用操心了;
  • 悬挂指针:缓解,但不能完全避免。

一个例子是

#include <iostream>
#include <memory>
#include <string>

int main()
{
  std::unique_ptr<std::string> str_ptr;
  {
      std::string tmp("bbbababa");
      str_ptr.reset(&tmp);
      std::cout << str_ptr.get() << std::endl;
      std::cout << *str_ptr << std::endl;
  }
  std::string tmp("xxxxxxx");
  std::cout << str_ptr.get() << std::endl;
  std::cout << *str_ptr << std::endl;
  return 0;
}
// Output:
//   0x728562b4f060
//   bbbababa
//   0x728562b4f060
//   xxxxxxx

此外,数组的问题也通过特化std::unique_ptr<T[]>得到了解决. 可能有些同学会有性能方面的顾虑,觉得智能指针会比裸指针的操作性能差一点。坦白讲这个是有可能的,但是在实际使用的过程中,往往作profiling后会发现性能出现瓶颈的地方几乎不是智能指针。作为unique_ptr来讲,它不需要维护额外的状态,只是对象的创建和销毁要作一些特殊处理,平时使用的过程中完全可以当作裸指针来用。真正可能要担心性能问题的,是shared_ptr这一类允许共享所有权的智能指针。

作为非金融科班的学生,发现有一些术语其实看上去不知所以然,但是理解了之后就会感叹一下:

这tm不就是xxx么~

记得刚开始搞量化的时候,经常碰到的词汇就是各种因子,以及因子暴露(factor exposure)因子载荷(factor loading)。然后我就去百度知乎Google搜啊,立马出来一大堆长篇大论告诉你它的由来,各种多因子模型。窃以为对于刚入门的我,最想知道的就是这个东西是什么意思,然后才是它背后的模型。

那么问题来了

因子暴露(aka. 因子载荷)是什么?因子暴露就是因子载荷。假设你的投资组合里有p只股票,并且你有m个因子,那么想象因子暴露是一个p * m的矩阵——每只股票在每个因子上的因子值。打个比方,如果你的因子是32个行业分类,那么这个矩阵的元素非0即1——1代表股票属于这个行业. 如果假定一只股票只能属于一个行业,那么这个矩阵的每一行都只可能有一个1,其余都是0. 这个东西一般是归一化或者说标准化过的值,不然会无法做统计分析。

那么,这东西能拿来做什么?

结构化风险模型的假设是,投资组合的收益率是可以由下列公式解释

r = X * b + u

如果依旧假设有p只股票,m个因子,投资窗口时间长是l天 其中

  • r是投资组合收益,p*l矩阵
  • X是因子暴露,p*m矩阵
  • b是因子收益率,m*l矩阵
  • u是特异收益(又叫残差收益),相当于前面Xb不能解释的一部分,`pl`矩阵 简而言之就是一个投资组合的收益可以被几个因子来解释,解释不了的部分另归另说。 如果假定个股残差收益是互不相关的,那么 i. 比如我假定股票的收益(r)是由不同的行业因子决定的,那么只要有不同行业ETF的收益率(上面的b),我就可以拿来做回归分析得知股票对应的行业(上面的X); ii. 再如假定股票的收益(r)和股票的Barra风格因子值(X)是已知的,那么也可以通过回归分析的方法拿到每个风格因子的收益率(b);

题外话:感觉这个有点像期权定价里的implied volatility和realized volatility。如果是从underlying的价格用Black-Scholes公式算出来的叫隐含波动率,而已实现波动率可以通过观测期权价得到。上面通过ETF收益率算出来的因子暴露有点类似,其实股票的行业归属也可以从基本面的数据里拿到……