User-defined literal in C++

平时的开发中经常要用到json,长久以来我们的C++项目中一直在使用jsoncpp这么一个比较古老的json库。由于还是基于C++03标准的库,API的设计上保留着非常原始的风味。 譬如你要构建一个{"foo": "bar"}这样的json对象。你得这么写

Json::Value root;
root["foo"] = "bar";

经常用javascript的同学一定觉得这样的构造方式傻爆了。的确,在javascript中可以轻松的“字面意义上的(literally)”创建一个json对象

var root = {
    foo: "bar"
};

在python中你也可以“字面意义上的”创建一个json(list)

root = {
    "foo": "bar"
}

当然,python这样创建的只是list对象,你可以近似当作json对象来使用。

那么,以偷懒为进步原动力的工程师们自然就会想,在C++中能不能也实现这样的构造方式。

答案是肯定的。

C++11的标准在今天已经不是什么新鲜事物了,虽然有海量的历史项目仍旧没有或者无法迁移到新的C++标准。但是我们仍然可以一窥新标准给工程师们带来的便利,在初始化对象上,新标准就给我们提供的完美的解决办法。

我们先来看从一个“现代”的json库中介绍的构造上述json对象的方法。

json j = R"({
    "foo": "bar"
})"_json;

是不是已经很接近javascript和python的语法了?那么这个构造方法是什么原理呢,答案就是user-defined literal。 在探究user-defined literal之前,有必要先弄明白一个问题。什么是literal,为什么需要literal,为什么需要user-defined的literal。下面阐述一下我个人的理解。 literal这个词翻译成中文一般翻成“字面上的,字面意义的”。既然有字面上的意义,那么就是相对于字面下的意义。譬如“菊花”的字面上意义和。。。不赘述。 那么在程序语言中的literal的含义怎么解读呢。我们知道,我们写的所有的代码都是给人阅读的而不是给机器读的,机器只能读懂进过编译汇编或者解释后的给机器看的代码。 语法(syntax),便是人的语言和机器的语言之间的桥梁。我们用人的语言(哪门子的人的语言),通过编译器解释器来说给计算机听。 有了基本的语法,在OO世界中,我们变可以用最基础的规则描述一个个对象了。

map m;
m["foo"] = string("bar");

但是如果所有的对象都需要通过这样的方式来描述,生活就变的太难过了。为什么我们不让计算机来读懂我们更多的语言呢?因此大部分编程语言都会提供预置的literal语法,让一些常用的类型能用更加接近人类直觉的方式创建对象。 因此,你可以写

Map m;
m["foo"] = "bar";

看起来这与刚才那样的写法区别不大也没有节约多少字数,但揭示了一个深刻的过程。编译器会帮你把"bar"这样的字面描述转化成为一个正确的string对象。这就是我理解的literal,把人类世界定义好的描述规则变成正确的计算机中的对象。

既然字符串我们可以用双引号加上普通的人类文字来描述并且能正确转换,那么其他类型的事物是不是也能这样呢?譬如我想写m["foo"] = An_angry_bird,计算机能帮我自动完成以下操作吗?

Bird b;
b.mood = ANGRY;
m["foo"] = b;

答案是计算机先天不能,但是你可以告诉他怎么做, 这便是user-defined literal,用户来告诉计算机怎么理解我们人类常用的词语。

再回到我们最初遇到的那个句子。

json j = R"({
    "foo": "bar"
})"_json;

很明显,json是我们需要创建的对象的类型名,j是这个对象的变量名。问题就在后半部分

R"({
    "foo": "bar"
})"_json

这是什么意思?

这个表达式由两部分组成,以R"()"构成的string literal和_json构成的literal operator。 首先string literal就是之前提到的一种用来构建字符串对象的语法。普通的双引号包围着的字母序列就是最常用最熟悉的构造字符串对象的literal的一种。 事实上string literal有其他很多形式,详细的参考http://en.cppreference.com/w/cpp/language/string_literal 这里不赘述。 我们只要明白这里R"()"的句式表达的意思是这是一个raw string,引号以内,圆括号之间的字符序列全部是字面上的字符,不需要转义。所以这部分的意思就是把

{
    "foo": "bar"
}

这么一段纯文字变成了C++中的string对象,准确的说是char16_t *。 第二部分的_json就是我们要说的重点,literal operator。形式上看,这是一个附着在string literal后的后缀,C++正式通过这种方式来实现user-defined literal。 这个literal operator的后缀看着奇怪其实只是一个普通的函数,对于string literal来说,这个operator其实就是个具有如下签名形式的一般函数

T operator "" X(str, size_t)

这里str是可能的字符串类型const char16_t , const char8_t 等,取决与你的string literal转化为哪种字符串对象。T可以是任意类型。 理解了这点,我们不难发现最初的那个表达式其实等价于 json operator "" _json(R"(...)"),也就是说输入一个字符串对象,输出一个json类型的对象。 这和我们手动写一个函数

T makeTfromString(str, size_t)

并无二致。只不过通过string literal的后缀这种语法糖,让整个过程看起来更自然更像人类语言。 除了string literal有operator,其他一些常用类型也可以这么做。譬如整型int,我们可以定义一些decimal literal operator让代码写起来更自然。 我们可以定义一个operator叫_minutes用来表示若干分钟包含的秒数。

unsigned long long int operator "" _minutes(unsigned long long int value) {
    return 60 * value;
}

于是我们可以这么写

unsigned long long int secondsOfHour = 60_minutes;

另外要注意的是,literal operator的命名必须是以下划线开头并且第一个字符不能大写。大写的operator是C++标准的保留operator。

好了。现在你知道怎么写一个user-defined literal让所有的字符串都带上一句“Make American Great Again”了吧。

string operator "" _says_trump(const char16_t *str, size_t len) {
    return string(str, str + len) + " Make American Great Again";
}
string greeting = "Die"_says_trump;

更多的细节可以参考这里

通过user-defined literal这个强大的机制,你可以设计许多自定义的格式来更方便的构造你的对象。这比起通常的用构造函数,初始化列表的方式更加的易用,隐藏了更多的实现细节,让代码字里行间更加符合人的阅读习惯。

links