c++中文件操作的坑

  • 使用ofstream读写文件,记得一定要关闭,否则同进程看不到这个文件的修改内容
std::ofstream f(config_file);
f.write(content.data(), content.size());
// f.close(); // missing!
SomeConfig.Load(config_file); // error, it's empty!

当然,更推荐scope_guard 或者gsl::finally来管理

  • stream的继承关系
void foo(const std::istream&) {
    puts("istream");
}
void foo(const std::ifstream&) {
    puts("ifstream");
}
int main() {
    std::fstream t;
    foo(t);
}

猜猜调用那个?第一个

继承关系,深坑

  • stream是有状态的

  • 占用很大

using namespace std;
cout<<"std::fstream = "<<sizeof(fstream)<<endl
  <<"std::ifstream = "<<sizeof(ifstream)<<endl
  <<"std::ofstream = "<<sizeof(ofstream)<<endl;

  //  std::fstream = 528
  //  std::ifstream = 520
  //  std::ofstream = 512
  • 检验文件是否存在 ifstream并不怎么快。不过确实挺好用的
#include <sys/stat.h>
#include <unistd.h>
#include <string>
#include <fstream>

inline bool exists_test0 (const std::string& name) {
    ifstream f(name.c_str());
    return f.good();
}

inline bool exists_test1 (const std::string& name) {
    if (FILE *file = fopen(name.c_str(), "r")) {
        fclose(file);
        return true;
    } else {
        return false;
    }   
}

inline bool exists_test2 (const std::string& name) {
    return ( access( name.c_str(), F_OK ) != -1 );
}

inline bool exists_test3 (const std::string& name) {
  struct stat buffer;   
  return (stat (name.c_str(), &buffer) == 0); 
}
ifstream 0.485s
FILE fopen 0.302s
posix access() 0.202s
posix stat() 0.134s

ref

  • https://quuxplusone.github.io/blog/2018/11/26/remember-the-ifstream/
  • https://zhuanlan.zhihu.com/p/90194868
  • 检查文件的benchmark代码在这里https://stackoverflow.com/questions/12774207/fastest-way-to-check-if-a-file-exist-using-standard-c-c11-c
  • finally https://www.bfilipek.com/2017/04/finalact-follow-up.html

Read More

rust学习笔记(c++ based)

为什么又要写一个?不写一遍真的记不住,写的也没有参考中的文章写得好。详细的版本直接跳到参考。

rust这个语言真是热闹。很多特性的融合,对标c++,必须要探究一下! 首先,这个社区+api文档 + 手把手demo 真的非常好,上手难度还好。写好的程序不好说,这文档是真方便,随便点一下就跳到实现上了。 这也说明新语言没那么大包袱。c++打开头文件看实现,天书,还是点开cppreference看接口文档吧。这东西和cppcoreguideline文档也是新搞的。c++也有人注意到了这个问题吧。

列几点我看书(rust book)过程中记录的好玩的地方。

  • 默认不可变语义,这个和c++是正好相反的,c/c++ 想要保证不可变,程序员自觉不改,或者加const,加上const也会有阴招(const_cast)绕过去,c/c++原则,充分信任程序员,程序员要知道自己在做什么,rust不信任,默认不可变,可变得手动标mut,不然分分钟报错
  • 引用语义。c++中,值语义,引用语义的切换非常自然。这个代价就是类型转换天花乱坠,但是自然。rust的引用语义(以及衍生的slice)需要显式指定,传参数得带&,这语法太难看。
  • 接上面,引用在rust中有借用语义,编译器强保证,所以用起来会很痛苦。
  • 宏,rust的宏有点像c++的变参模板,但是语法很邪恶,像perl shell这种脚本语法。很邪恶。
  • enum,这个东西就是个std::variant,配合rust本身的match语法,更变态一些。match这东西从函数式语言中抄过来的。也是先进性体现了。
  • traits 好东西,和c++的type_traits也有点像,也像c++ concept这种静态接口。rust直接去掉了继承,大家实现接口就好了,or enum
  • 这个函数声明风格挺像go的。说起go,go也有interface这种东西,也有magic方法,magic方法有点不适应,就比如make defer这种,没规律。难受。感觉还是c风格,指针回调的感觉。相比rust的traits方法还算系统,c++ concept当初也是这个计划,
  • 最后一句可以当返回值,这是ruby perl语言的优势,不用return。这个有利有弊
  • 很多语法和go很像,但是更系统一些(或者是我对go有偏见)

暂时就这么多。具体还是要写代码才懂。

reference

  • 一个英语博客 https://github.com/nrc/r4cppp
  • 一个不错的总结,针对c++语法 写的不错,基本把我想写的写了。
    • https://xr1s.me/2018/03/01/rust-learning-notes-for-cxx-programmer-part-one/
    • https://xr1s.me/2018/03/01/rust-learning-notes-for-cxx-programmer-part-two/
    • https://xr1s.me/2018/03/01/rust-learning-notes-for-cxx-programmer-part-three/
Read More

Apache Kafka源码剖析笔记



特点

  • Kafka具有近乎实时性的消息处理能力,即使面对海量消息也能够高效地存储消息和查询消息。
  • Kafka将消息保存在磁盘中,在其设计理念中并不惧怕磁盘操作,它以顺序读写的方式访问磁盘, 从而避免了随机读写磁盘导致的性能瓶颈。
  • Kafka支持批量读写消息,并且会对消息进行批量压缩,这样既提高了网络的利用率,也提高了压 缩效率。
  • Kafka支持消息分区,每个分区中的消息保证顺序传输,而分区之间则可以并发操作,这样就提高 了Kafka的并发能力。
  • Kafka也支持在线增加分区,支持在线水平扩展。
  • Kafka支持为每个分区创建多个副本,其中只会有一个Leader副本负责读写,其他副本只负责与 Leader副本进行同步,这种方式提高了数据的容灾能力。Kafka会将Leader副本均匀地分布在集群 中的服务器上,实现性能最大化

为什么引入mq中间件,以及,原有方案的弊端

  • 由于子系统之间存在的耦合性,两个存储之间要进行数据交换的话,开发人员就必须了解这两个 存储系统的API,不仅是开发成本,就连维护成本也会很高。一旦其中一个子系统发生变化,就可 能影响其他多个子系统,这简直就是一场灾难。
  • 在某些应用场景中,数据的顺序性尤为重要,一旦数据出现乱序,就会影响最终的计算结果,降 低用户体验,这就提高了开发的难度。
  • 除了考虑数据顺序性的要求,还要考虑数据重传等提高可靠性的机制,毕竟通过网络进行传输并 不可靠,可能出现丢失数据的情况。
  • 进行数据交换的两个子系统,无论哪一方宕机,重新上线之后,都应该恢复到之前的传输位置, 继续传输。尤其是对于非幂等性的操作,恢复到错误的传输位置,就会导致错误的结果。
  • 随着业务量的增长,系统之间交换的数据量会不断地增长,水平可扩展的数据传输方式就显得尤 为重要。

应对的解决方案

  • 中间件解耦
  • 数据持久化
  • 扩展与容灾
    • topic,partition,replica
    • 多个consumer消费,各自记录消费标签信息,由consumer决定cursor

核心概念

  • 消息
  • topic/partition/log
    • partition offset保证顺序性
    • partition 用Log表述,大小有限,可以分节,顺序io追加,不会有性能问题
      • 索引文件 稀疏索引 内存中定位加速
    • 保留策略 顺序写总会写满,设置消息保留时间或者总体大小
    • 日志压缩 类似rocksdb compaction
  • Broker kafka server概念,接受生产者的消息,分配offset保存到磁盘中
  • 副本(并不是实时同步)
    • ISR集合,可用的没有lag太多的可升leader的节点的集合
    • HighWatermar LEO,描述可消费的属性
      • ①Producer向此Partition推送消息。
      • ②Leader副本将消息追加到Log中,并递增其LEO。
      • ③Follower副本从Leader副本拉取消息进行同步。
      • ④Follower副本将拉取到的消息更新到本地Log中,并递增其LEO。
      • ⑤当ISR集合中所有副本都完成了对offset=11的消息的同步,Leader副本会递增HW。
      • 在①~⑤步完成之后,offset=11的消息就对生产者可见了。
  • 消费者,消费组、

image-20200914163714353

细心的读者可能会问,为什么GZIP压缩方式会直接使用new创建,而Snappy则使用反射方式呢?这主要是因为GZIP使用的GZIPOutputStream是JDK自带的包,而Snappy则需要引入额外的依赖包,为了在不使用Snappy压缩方式时,减少依赖包,这里使用反射的方式动态创建。这种设计的小技巧,值得读者积累。在Compressor中还提供了wrapForInput()方法,用于创建解压缩输入流,逻辑与wrapForOutput()类似,不再赘述。

生产者KafkaProducer分析

同步还是异步就差在future本身调用不调用get

image-20200916095525486

结构很清晰

KafkaProducer

  • send()方法:发送消息,实际是将消息放入RecordAccumulator暂存,等待发送。
  • flush()方法:刷新操作,等待RecordAccumulator中所有消息发送完成,在刷新完成之前会阻塞调用的线程。
  • partitionsFor()方法:在KafkaProducer中维护了一个Metadata对象用于存储Kafka集群的元数据,Metadata中的元数据会定期更新。partitionsFor()方法负责从Metadata中获取指定Topic中的分区信息。
    • topic, verison, timestamp….
  • close()方法:关闭此Producer对象,主要操作是设置close标志,等待RecordAccumulator中的消息清 空,关闭Sender线程

RecordAccumulator

RecordAccumulator中有一个以TopicPartition为key的ConcurrentMap,每个value是ArrayDeque<RecordBatch>(ArrayDeque并不是线程安全的集合,后面会详细介绍其加锁处理过程),其中缓存了发往对应TopicPartition的消息。每个RecordBatch拥有一个MemoryRecords对象的引用

(1)Deque中有多个RecordBatch或是第一个RecordBatch是否满了。 (2)是否超时了。 (3)是否有其他线程在等待BufferPool释放空间(即BufferPool的空间耗尽了)。 (4)是否有线程正在等待flush操作完成。 (5)Sender线程准备关闭。

BufferPool

  • 有锁。全局分配器

Sender

据RecordAccumulator的缓存情况,筛选出可以向哪些Node节点发送消息,RecordAccumulator.ready();然后,根据生产者与各个节点的连接情况(由NetworkClient管理),过滤Node节点;之后,生成相应的请求,这里要特别注意的是,每个Node节点只生成一个请求;最后,调用NetWorkClient将请求发送出去

image-20200921165610705

controller

http://www.sirann.cn/blog/kafka-controller-%E6%A8%A1%E5%9D%97%E4%B8%80%E6%A6%82%E8%BF%B0/

删除消息,支持按照offset SO,按照topic来删

支持时间删除和大小删除

痛点

对于单纯运行Kafka的集群而言,首先要注意的就是为Kafka设置合适(不那么大)的JVM堆大小。从上面的分析可知,Kafka的性能与堆内存关系并不大,而对page cache需求巨大。根据经验值,为Kafka分配6~8GB的堆内存就已经足足够用了,将剩下的系统内存都作为page cache空间,可以最大化I/O效率。

另一个需要特别注意的问题是lagging consumer,即那些消费速率慢、明显落后的consumer。它们要读取的数据有较大概率不在broker page cache中,因此会增加很多不必要的读盘操作。比这更坏的是,lagging consumer读取的“冷”数据仍然会进入page cache,污染了多数正常consumer要读取的“热”数据,连带着正常consumer的性能变差。在生产环境中,这个问题尤为重要。

解决方案,调整page cache https://www.jianshu.com/p/92f33aa0ff52

实时消费与延迟消费的作业在 PageCache 层次产生竞争,导致实时消费产生非预期磁盘读。线上存在 20%的延迟消费作业,污染cache

传统 HDD 随着读并发升高性能急剧下降。

解决方案

  • 引入SSD 做cache https://www.infoq.cn/article/k6dqfqqihpjfepl3y3hs
  • 重新设计kafka cache系统 https://www.jiqizhixin.com/articles/2019-07-23-11

优化细节

zero-copy技术

减少甚至避免用户空间和内核空间之间的数据拷贝:在一些场景下,用户进程在数据传输过程中并不需要对数据进行访问和处理,那么数据在 Linux 的 Page Cache 和用户进程的缓冲区之间的传输就完全可以避免,让数据拷贝完全在内核里进行,甚至可以通过更巧妙的方式避免在内核里的数据拷贝。这一类实现一般是通过增加新的系统调用来完成的,比如 Linux 中的 mmap(),sendfile() 以及 splice()

这里用的sendfile


Read More


memcached源码剖析笔记



memcache特点

  • 协议简单
    • add/set/replace/get/get/delete
  • 基于libevent
  • 内存存储,LRU,抗干扰?

内存存储机制

slab allocation,感觉像linux系统的slab

  • page 1M chunk内存空间slab 特定大小的chunk组
  • 解决内存碎片但是空间利用存在浪费
    • chunk可以动态増缩?
  • 增长因子可以设定 growth factor

image-20200911173653707


ref


Read More


nginx源码剖析笔记


linux内核参数优化

;最大句柄数
fs.file-max = 99999 
;time_wait状态的socket重新用于新的tcp链接
net.ipv4.tcp_tw_reuse = 1
;tcp发送keeptime的时间,调小可以快速清除无效连接(?单位是什么)
net.ipv4.tcp_keepalive_time = 600
;服务器主动关闭保持FIN_WAIT_2的最大时间
net.ipv4.tcp_fin_timeout = 30
;TIME_WAIT的socket最大值,上限,超过这个值会清掉所有TIME_WAIT TIME_WAIT过多会卡
net.ipv4.tcp_max_tw_buckets = 5000
net.ipv4.ip_local_port_range = 1024 61000
net.ipv4.tcp_rmem = 4096 32768 262142
net.ipv4.tcp_wmem = 4096 32768 262142
;内核处理接收包队列的长度上限
net.core.netdev_max_backlog = 8096
net.core.rmem_default = 262144
net.core.wmem_default = 262144
net.core.rmem_max = 2097152
net.core.wmem_max = 2097152
;tcp syn攻击
net.ipv4.tcp_syncookies =1
;正在三次握手建立阶段的请求队列,可以调高以免丢失客户端连接
net.ipv4.tcp_max_syn.backlog = 1024

滑动窗口大小与套接字缓存设置会在一定程度上影响并发 每个tcp链接都会为了维护滑动窗口而消耗内存

命令行相关

快速退出进程

kill -s SIGTERM <pid>

kill -s SIGINT <pid>

优雅退出

kill -s SIGQUIT <master pid>

kill -s SIGWINCH <worker pid>

重读配置

kill -s SIGHUP <master pid>

日志回滚

kill -s SIGUSR1 <master pid>

还真有这么实现日志回滚的。。。我惊了

平滑升级

kill -s SIGUSR2 <master pid>

这些是对信号的handler做自定义了

性能调优

  • 指定worker个数
    • worker绑核 (代码层怎么实现的?)
  • SSL硬件加速
  • 系统调用gettimeofday执行频率 (现在开销没那么大,也可以限制)
  • worker优先级 nice值设定

事件类配置型

  • 是否打开accept锁 连接负载均衡锁
    • 延迟时间设定

Nginx基础架构

  • 通用的ngx_module_t

  • TCP_DEFER_ACCEPT以及post_accept_timeout 如果连接很久没事件就踢掉

  • ngx_cycle_t

    • void ****conf_ctx 所有模块配置项结构体指针(数组->指针->指针数组->指针)

    image-20200910174704792

讲了很多模块知识。。我对模块不太感兴趣。不看了先


ref

  • 关键字 陶辉的博客。

Read More

overloaded trick

why

这篇就是参考链接2的总结,还是从参考链接1中单独拎出来说一下


之前学std::variant 和 std::visit 学到了overloaded这个模板,

#include <variant>
#include <cstdio>
#include <vector>

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; }; // (1)
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;  // (2)

using var_t = std::variant<int, const char*>;

int main() {
    std::vector<var_t> vars = {1, 2, "Hello, World!"};

    for (auto& v : vars) {
        std::visit(overloaded {  // (3)
            [](int i) { printf("%d\n", i); },
            [](const char* str) { puts(str); }
        }, v);
    }

    return 0;
}

(2)是c++17引入的新特性,乍一看看不懂,咱们一点一点顺一下

首先,这个overloaded模板就是一个转发继承而来的operator (), 一个粗暴的版本,需要基类实现operator()

struct PrintInt { //(1)
    void operator() (int i) {
        printf("%d\n", i);
    }
};

struct PrintCString { // (2)
    void operator () (const char* str) {
        puts(str);
    }
};

struct Print : PrintInt, PrintCString { // (3)
    using PrintInt::operator();
    using PrintCString::operator();
};

如果写成模板形式,那就是

template <class... Ts> // (1)
struct Print : Ts... {
    using Ts::operator()...;
};

int main() {
    std::vector<var_t> vars = {1, 2, "Hello, World!"};

    for (auto& v : vars) {
        std::visit(Print<PrintCString, PrintInt>{}, v); // (2)
    }

    return 0;
}

注意到这个写法特别重,考虑到开头这个优雅的用法,使用lambda,代码写起来就更难看了

int main() {
    std::vector<var_t> vars = {1, 2, "Hello, World!"};
    auto PrintInt = [](int i) { printf("%d\n", i); }; // (1)
    auto PrintCString = [](const char* str) { puts(str); };

    for (auto& v : vars) {
        std::visit(
            Print<decltype(PrintCString), decltype(PrintInt)>{PrintCString, PrintInt}, // (2)
            v);
    }

    return 0;
}

所以理所当然,推导动作应该放在一个helper函数里, 上面这个调用模式还是很容写出一个推导helper的

template <class... Ts> // (1)
auto MakePrint(Ts... ts) {
    return Print<Ts...>{ts...};
}

int main() {
    std::vector<var_t> vars = {1, 2, "Hello, World!"};

    for (auto& v : vars) {
        std::visit(
            MakePrint( // (2)
                [](const char* str) { puts(str); },
                [](int i) { printf("%d\n", i); }
                ),
            v);
    }

    return 0;
}

这已经和overload非常接近了,回到一开始我们提到的,如何写成开头那个样子呢,这就需要c++

17 的新特性,类模板实参推导, 自定义推导指引,User-defined deduction guides,简单说,就是构造函数能做helper的活(make_tuple, make_pair),只要定义好规则就可以

在c++17中,可以干净的写出

std::tuple t(4, 3, 2.5); // same as auto t = std::make_tuple(4, 3, 2.5);

在tuple中,写好了推导规则

#ifndef _LIBCPP_HAS_NO_DEDUCTION_GUIDES
// NOTE: These are not yet standardized, but are required to simulate the
// implicit deduction guide that should be generated had libc++ declared the
// tuple-like constructors "correctly"
template <class _Alloc, class ..._Args>
tuple(allocator_arg_t, const _Alloc&, tuple<_Args...> const&) -> tuple<_Args...>;
template <class _Alloc, class ..._Args>
tuple(allocator_arg_t, const _Alloc&, tuple<_Args...>&&) -> tuple<_Args...>;
#endif

make_tuple就下岗了

类似的,只要为Print写好推导,就可以省掉MakePrint

#include <variant>
#include <cstdio>
#include <vector>

using var_t = std::variant<int, const char*>;

template <class... Ts>
struct Print : Ts... {
    using Ts::operator()...;
};

template <class...Ts> Print(Ts...) -> Print<Ts...>; // (1)

int main() {
    std::vector<var_t> vars = {1, 2, "Hello, World!"};
    for (auto& v : vars) {
        std::visit(
            Print{ // (2)
                [](const char* str) { puts(str); },
                [](int i) { printf("%d\n", i); }
            },
            v);
    }
    return 0;
}

到此,overloaded trick就解释完了

reference

Read More

std::variant 与 std::visit

why

因为rust的enum让我回想起union和 variant,决定找找文档仔细说一下这个variant,做个笔记

std::variant是c++17加入的新容器,主要就是safe union。用来和enum比较也算合适,都叫做sum type,类型是线程(求和)的,只表现出线性数目的类别实例,product type是乘积的(比如结构体),这个是函数式概念了,先做个科普

下面是一个std::visit+ std::variant的例子,同比rust中的enum match

std::variant<double, bool, std::string> var;

struct {
    void operator()(int) { std::cout << "int!\n"; }
    void operator()(std::string const&) { std::cout << "string!\n"; }
} visitor;

std::visit(visitor, var);
#![allow(unused_variables)]
fn main() {
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}
}

差距还好。rust也可以直接调用函数 lambda。对比来说,c++需要手动写visitor有点难看。有没有make_visitor呢

overload

下面的链接有make_visitor, 就是这个overload,在cpp reference std::visit的示例中,也有使用overload这个模板,长这个样子

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

于是,上面的代码就变成这样

std::variant<double, bool, std::string> var;
std::visit(overloaded {
            [](auto arg) { std::cout << arg << ' '; },
            [](double arg) { std::cout << std::fixed << arg << ' '; },
            [](const std::string& arg) { std::cout << std::quoted(arg) << ' '; },
        }, var);

感觉稍微干净了点是不是?手写operator()还是有点难受的?换成lambda只能写一个,也得用overload包装一下

overload原理就是模板推导和转发,变参模板可能看不懂,写成一个继承的就容易明白了

struct overloadInt{ 
    void operator(int arg){
        std::cout<<arg<<' ';
    } 
};
struct overload : overloadInt{
    using overloadInt::operator();
};

不用std::visit行不行

行,又要走SFINAE 老路了,enable_if 糊一个,还要判断variant里到底存了什么,基本上和visit差不多?我糊了半天糊出个这么个玩意儿。

#include <iomanip>
#include <iostream>
#include <string>
#include <type_traits>
#include <variant>
#include <vector>

template<typename T, typename VARIANT_T>
struct is_variant_member_type;

template<typename T, typename... Ts>
struct is_variant_member_type<T, std::variant<Ts...>> 
  : public std::disjunction<std::is_same<T, Ts>...> {};


template <typename V > typename std::enable_if<is_variant_member_type<std::string,V>::value&&
    is_variant_member_type<double,V>::value>&&
    is_variant_member_type<int,V>::value>::type
match (V v)
{
    if  (std::holds_alternative<int>(v))
    	std::cout << std::get<int>(v) << ' '; 
    if (std::holds_alternative<std::string>(v))
        std::cout << std::quoted(std::get<std::string>(v)) << ' ';
    if  (std::holds_alternative<double>(v))
        std::cout<<std::fixed << std::get<double>(v) << ' ';
}

// the variant to visit
using var_t = std::variant<int, double, std::string>;

int main() {
    std::vector<var_t> vec = {10, 15l, 1.5, "hello"};
    for(auto& v: vec) {
        match(v);
    }
}

注意,用if-constexpr不可以,虽然std::holds_alternative是constexpr的。。暂时没搞懂

感觉吧match拆一拆,拆成lambda类似形式的,可以结合overload。这个写的用不用enable_if没什么区别。。我以后再写吧。。这里学的不明白。

reference

  • std::variant https://en.cppreference.com/w/cpp/utility/variant
  • std::visit , 其中这个overlord模板很有意思。https://en.cppreference.com/w/cpp/utility/variant/visit
  • 一个variant介绍,其中里面的 make_visitor就是上面这个overloadedhttps://pabloariasal.github.io/2018/06/26/std-variant/
  • 对overload的解释 https://dev.to/tmr232/that-overloaded-trick-overloading-lambdas-in-c17
  • 对overload的解释和加强,并且有提案。https://arne-mertz.de/2018/05/overload-build-a-variant-visitor-on-the-fly/
  • std::visit 和std::variant https://arne-mertz.de/2018/05/modern-c-features-stdvariant-and-stdvisit/
  • 讲type的,深入浅出(应该写个笔记记录下)https://github.com/CppCon/CppCon2016/blob/master/Tutorials/Using%20Types%20Effectively/Using%20Types%20Effectively%20-%20Ben%20Deane%20-%20CppCon%202016.pdf
  • rust enum+match https://doc.rust-lang.org/beta/book/ch06-02-match.html
  • 观点:std::visit很糟糕 https://bitbashing.io/std-visit.html
  • visit 实现,里面有几个链接很有意思,https://stackoverflow.com/questions/47956335/how-does-stdvisit-work-with-stdvariant
    • https://mpark.github.io/programming/2015/07/07/variant-visitation/
    • https://mpark.github.io/programming/2019/01/22/variant-visitation-v2/
    • http://talesofcpp.fusionfenix.com/post-17/eggs.variant—part-i
    • 上面的链接有两个variant的实现。
Read More


^