Oceanbase代码有关语言和编译器的奇技淫巧(二)

Oceanbase代码有关语言和编译器的奇技淫巧(二)

–to_cstring的Sfinae魔术

我安装了代码高亮的插件,看上去会爽一些了。

这次的奇技淫巧是关于打印日志的,先提一个需求:有一个int类型的ipv4地址,想按照点分字符串的形式打印到日志中。很常见的需求,在收到网络请求或者处理分布式调度的时候可能需要把网络地址打印出来,也许你和我一样第一次会先写出下面这样的代码:

TBSYS_LOG(INFO "addr=%d.%d.%d.%d",
(ip & 0xFF),
(ip >> 8) & 0xFF,
(ip >> 16) & 0xFF,
(ip >> 24) & 0xFF);

每当我写代码需要打印某个ipv4地址的时候,就会去找这段代码,把它复制粘贴。当这种事情做多了以后,我开始变得不耐烦了,是时候需要封转一个函数来帮我搞定这件事了,就起名叫ip2str好了,不过当我开始写这个函数的时候,又遇到一个新的问题,任何stl的容器在oceanbase项目中都是不被允许的,std::string也在其列,因此不能使用std::string传递结果,当然在ip2str里使用一个static char数组作为结果返回也是不被允许的,这意味着你放弃了线程安全性。所以一个名为ip2str_r的函数诞生了,它的签名如下:

int ip2str_r(const int ip, char *buffer, const int length);

当我需要打印多个ip地址时,才发现它的难用程度令人发指:

char buffer1[32];

char buffer2[32];

ip2str_r(ip1, buffer1, 32);

ip2str_r(ip2, buffer2, 32);

TBSYS_LOG(INFO, "addr1=%s addr2=%s", buffer1, buffer2);

那么有没有办法在保证线程安全的情况下实现一个这样的函数呢:

const char *ip2str_r(const int ip);

这里又要轮到上次提到的static __thread出场了,先来看看代码:

const char *ip2str_r(const int ip)
{
            static const int64_t BUFFER_SIZE = 32;
            static __thread char buffers[2][BUFFER_SIZE];
            static __thread uint64_t i = 0;
            char *buffer = buffers[i++ % 2];
            buffer[0] = '\0';
            unsigned char *bytes = (unsigned char *) &ip;
            snprintf(buffer, BUFFER_SIZE, "%d.%d.%d.%d",
            bytes[0], bytes[1], bytes[2], bytes[3]);
            return buffer;
}

每个线程维护一个buffer数组用来处理在一行日志中打印多个ip地址的需求,不过这里也有一个局限性,当你需要在一行日志中打印超过2个ip地址的话,依赖于snprintf的压栈顺序,多个ip地址会被相互覆盖,最终只能显示出两个有效结果。要想简单处理这种情况就把buffer的数组增大吧,4个、8个或更多,相信你在一行日志里也不会打印太多的地址。

打ipv4地址的需求解决了,下面让我们再来看看一个更复杂的需求,在我们的代码中,除了有把ip地址转化为字符串的需求外,还有很多对象也希望能够方便的打印出内部信息以方便调试或跟踪,譬如解析SQL后产生的物理执行计划是由一个一个的物理运算符对象嵌套而成的,我们希望将整个物理执行计划以文本方式展现出来,这就需要每个物理运算符都实现类似to_string的方法将自己的信息和嵌套的物理运算符都打印出来。我们当然可以在每个物理运算符都用上述static __thread的方法实现to_string,但是又意味着众多被复制粘贴的重复代码,我们希望抽出重复逻辑,让每个需要打印文本信息的类都只实现格式化打印的代码,而将buffer的维护抽取到公共的逻辑中。

由于历史原因,有些类实现了签名如下的to_string方法:

int64_t to_string(char *buffer, const int64_t buffer_size);

而有些类实现了如下的to_cstring方法:

const char* to_cstring();

而不同类打印出文本串的长度也不尽相同,可能打印在一行日志中的个数也不相同。

总结需求如下:实现一个to_cstring的模板方法,传入T类型的对象。第一,如果T实现了to_cstring就直接调用;否则就使用线程局部buffer调用to_string方法;第二,buffer长度和个数可使用默认值,但是如果T以某种方式指定了buffer长度和个数,则使用指定的值。下面只分析第一个需求,搞清楚了sfinae的原理,对于第二个需求请同学们去看oceanbase开源的代码库即可明白。

说到这里,本次要介绍的关键特性sfinae就要出场了,它是一个C++的语言特性,全称是Substitution failure is not an error,直译就是替换失败并非错误,即在匹配重载函数的时候,使用类型T匹配函数参数时发现类型错误(如T的某个成员不存在),不认为是编译错误,而是将这个重载函数从备选列表中去掉,再去尝试匹配下一个重载函数。有关sfinae词条,wikipedia有比较详细的说明,不过我认为那上面的第一个示例并不典型,判断类是否包含某个typedef类型的需求可以用另外一种称为traits的技术来实现,不一定非要用sfinae,所以我修改了一下,举例如下:

template <typename U, U>
struct type_check;

template <typename T>
bool have_con_member(type_check<const int*, &T::CON> *)
{
  printf("T has a const int member named CON\n");
  return true;
}

template <typename>
bool have_con_member(...)
{
  printf("T does not have any const int member named CON\n");
  return false;
}

struct Test
{
  static const int CON = 10;
};

struct Test1
{
  static const int64_t CON = 1;
};

int main()
{
  have_con_member<Test>(0);
  have_con_member<Test1>(0);
  have_con_member<int>(0);
}

这段代码的作用是检查调用函数f的时候,模板参数类型是否包含一个名为CON的整数型常量成员,根据检查结果分别打印不同的信息。对于Test类型作为模板参数,可以正确的匹配替换,绑定第一个f函数;而对于Test1类型为模板参数时,由于两个模板参数类型不一致,无法具现化type_check类型,因此将第一个f函数从备选函数中删除,继续看第二个f函数,发现可以匹配,编译通过;再看int类型为模板参数时,不存在int::CON,同样也无法具现化type_check类型。

到此为止,在编译期静态检查某个类型有没有包含指定成员的功能已经实现。现在我们有了这个功能后,如何应用在上面提到的实际需求中呢,先回顾下需求,实现to_cstring函数,在T类型有to_cstring方法时就直接调用,否则调用另一个称为to_string的方法。对上面示例代进行少量修改,我们应该比较容易的写出bool have_to_cstring_func<T>()这样的模板函数:

template <typename U, U>
struct type_check;

template <typename T>
bool have_to_cstring_func(type_check<const char* (T::*) (), &T::to_cstring> *)
{
  return true;
}

template <typename>
bool have_to_cstring_func(...)
{
  return false;
}

调用这个函数是可以通过返回值来判断结果,但是调用函数返回的结果是程序运行期间的变量,要执行这个函数才行,所以你可能会写出这样的代码:

template <typename T>
const char *to_cstring(T &obj)
{
  if (have_to_cstring_func<T>(0))
  {
    return obj.to_cstring();
  }
  else
  {
    static char __thread buffer[4096];
    obj.to_string(buffer, 4096);
    return buffer;
  }
}

但是很遗憾,这段代码是没法通过编译的,它要求T类型即要包含to_cstring函数,也要包含to_string参数,因为它没有办法在编译期知道针对某个T类型一定会进入哪个分支。既然不能通过函数的返回值绑定,那么我们就换个思路,用函数的返回类型来区分到底哪个重载函数被调用了,定如下模板类型BoolType,修改重载函数返回值类型,第一个返回BoolType<true>,第二个返回BoolType<false>。实现to_cstring模板函数如下:

template <bool c>
struct BoolType
{
  static const bool value = c;
};

template <class T>
const char *to_cstring(T &obj, BoolType<true>)
{
  return obj.to_cstring();
}

template <class T>
const char *to_cstring(T &obj, BoolType<false>)
{
  static char __thread buffer[4096];
  obj.to_string(buffer, 4096);
  return buffer;
}

template <class T>
const char *to_cstring(T &obj)
{
  return to_cstring(obj, have_to_cstring_func<T>(0));
}

好了,现在使用宏来整理一下这段代码,你就能得到一个通用的HAS_XXX_MEMBER宏了,我就不再粘代码了,想到具体实现的同学可以去copy oceanbase开源代码库中src/common/utility.h来找完整的to_cstring实现。

这是一个奇技淫巧的系列,后续内容预告一下:

大小为0对象的妙用

弱类型的应用

宏的变长模板

epoll与多对一唤醒器

Loading

Oceanbase代码有关语言和编译器的奇技淫巧(二)》有2个想法

回复 tom 取消回复