从C++11开始,标准库中引入了std::hexfloat和std::defaultfloat来修改浮点输入和输出的默认格式化,其中std::hexfloat可以将浮点数格式化为十六进制的字符串,而std::defaultfloat可以将格式还原到十进制,以输出为例:
#include <iostream>
int main()
{
double float_array[]{ 5.875, 1000, 0.117 };
for (auto elem : float_array) {
std::cout << std::hexfloat << elem
<< " = " << std::defaultfloat << elem << std::endl;
}
}上面的代码分别使用std::hexfloat和std::defaultfloat格式化输出了数组x里的元素,输出结果如下:
0x1.780000p+2 = 5.875
0x1.f40000p+9 = 1000
0x1.df3b64p-4 = 0.117这里有必要简单说明一下十六进制浮点数的表示方法,以0x1.f40000p+9为例:其中0x1.f4是一个十六进制的有效数,p+9是一个以2为底数,9为指数的幂。其中底数一定为2,指数使用的是十进制。也就是说0x1.f40000p+9可以表示为: 0x1.f4 * 29。
虽然C++11已经具备了在输入输出的时候将浮点数格式化为十六进制的能力,但遗憾的是我们并不能在源代码中使用十六进制浮点字面量来表示一个浮点数。幸运的是,这个问题在C++17标准中得到了解决:
#include <iostream>
int main()
{
double float_array[]{ 0x1.7p+2, 0x1.f4p+9, 0x1.df3b64p-4 };
for (auto elem : float_array) {
std::cout << std::hexfloat << elem
<< " = " << std::defaultfloat << elem << std::endl;
}
}使用十六进制浮点字面量的优势显而易见,它可以更加精准地表示浮点数。例如,IEEE-754标准最小的单精度值很容易写为0x1.0p−126。当然了,十六进制浮点字面量的劣势也很明显,它不便于代码的阅读理解。总之,我们在C++17中可以根据实际需求选择浮点数的表示方法,当需要精确表示某个浮点数的时候可以采用十六进制浮点字面量,其他情况使用十进制浮点字面量即可。
在C++14标准中定义了二进制整数字面量,正如十六进制(0x,0X)和八进制(0)都有固定前缀一样,二进制整数字面量也有前缀0b和0B。实际上GCC的扩展早已支持了二进制整数字面量,只不过到了C++14才作为标准引入:
auto x = 0b11001101L + 0xcdl + 077LL + 42;
std::cout << "x = " << x << ", sizeof(x) = " << sizeof(x) << std::endl;除了添加二进制整数字面量以外,C++14标准还增加了一个用单引号作为整数分隔符的特性,目的是让比较长的整数阅读起来更加容易。单引号整数分隔符对于十进制、八进制、十六进制、二进制整数都是有效的,比如:
constexpr int x = 123'456;
static_assert(x == 0x1e'240);
static_assert(x == 036'11'00);
static_assert(x == 0b11'110'001'001'000'000);值得注意的是,由于单引号在过去有用于界定字符的功能,因此这种改变可能会引起一些代码的兼容性问题,比如:
#include <iostream>
#define M(x, …) __VA_ARGS__
int x[2] = { M(1'2,3'4) };
int main()
{
std::cout << "x[0] = "<< x[0] << ", x[1] = " << x[1] << std::endl;
}上面的代码在C++11和C++14标准下编译运行的结果不同,在C++11标准下输出结果为x[0] = 0, x[1] = 0,而在C++14标准下输出结果为x[0] = 34, x[1] = 0。这个现象很容易解释,在C++11中1'2,3'4是一个参数,所以__VA_ARGS__为空,而在C++14中它是两个参数12和34,所以__VA_ARGS__为34。虽然会引起一点兼容性问题,但是读者不必过于担心,上面这种代码很少会出现在真实的项目中,大部分情况下我们还是可以放心地将编程环境升级到C++14或者更高标准的,只不过如果真的出现了编译错误,不妨留意一下是不是这个问题造成的。
过去想在C++中嵌入一段带格式和特殊符号的字符串是一件非常令人头痛的事情,比如在程序中嵌入一份HTML代码,我们不得不写成这样:
char hello_world_html[] =
"<!DOCTYPE html>\r\n"
"<html lang = \"en\">\r\n"
" <head>\r\n"
" <meta charset = \"utf-8\">\r\n"
" <meta name = \"viewport\" content = \"width=device-width, initial-scale=1, user-scalable=yes\">\r\n"
" <title>Hello World!</title>\r\n"
" </head>\r\n"
" <body>\r\n"
" Hello World!\r\n"
" </body>\r\n"
"</html>\r\n";可以看到上面代码里的字符串非常难以阅读和维护,这是因为它包含的大量转义字符影响了阅读的流畅性。为了解决这种问题,C++11标准引入原生字符串字面量的概念。
原生字符串字面量并不是一个新的概念,比如在Python中已经支持在字符串之前加R来声明原生字符串字面量了。使用原生字符串字面量的代码会在编译的时候被编译器直接使用,也就是说保留了字符串里的格式和特殊字符,同时它也会忽略转移字符,概括起来就是所见即所得。
声明原生字符串字面量的语法很简单,即prefix R"delimiter(raw_ characters)delimiter",这其中prefix和delimiter是可选部分,我们可以忽略它们,所以最简单的原生字符串字面量声明是R"(raw_characters)"。以上面的HTML字符串为例:
char hello_world_html[] = R"(<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes">
<title>Hello World!</title>
</head>
<body>
Hello World!
</body>
</html>
)";从上面的代码可以看到,原生字符串中不需要\r\n,也不需要对引号使用转义字符,编译后字符串的内容和格式与代码里的一模一样。读者在这里可能会有一个疑问,如果在声明的字符串内部有一个字符组合正好是)",这样原生字符串不就会被截断了吗?没错,如果出现这样的情况,编译会出错。不过,我们也不必担心这种情况,C++11标准已经考虑到了这个问题,所以有了delimiter(分隔符)这个元素。delimiter可以是由除括号、反斜杠和空格以外的任何源字符构成的字符序列,长度至多为16个字符。通过添加delimiter可以改变编译器对原生字符串字面量范围的判定,从而顺利编译带有)"的字符串,例如:
char hello_world_html[] = R"cpp(<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes">
<title>Hello World!</title>
</head>
<body>
"(Hello World!)"
< / body >
< / html>
)cpp";在上面的代码中,字符串虽然包含"(Hello World!)"这个比较特殊的子字符串,但是因为我们添加了cpp这个分隔符,所以编译器能正确地获取字符串的真实范围,从而顺利地通过编译。
C++11标准除了让我们能够定义char类型的原生字符串字面量外,对于wchar_t、char8_t(C++20标准开始)、char16_t和char32_t类型的原生字符串字面量也有支持。要支持这4种字符类型,就需要用到另外一个可选元素prefix了。这里的prefix实际上是声明4个类型字符串的前缀L、u、U和u8。
char8_t utf8[] = u8R"(你好世界)"; // C++20标准开始
char16_t utf16[] = uR"(你好世界)";
char32_t utf32[] = UR"(你好世界)";
wchar_t wstr[] = LR"(你好世界)";最后,关于原生字符串字面量的连接规则实际上和普通字符串字面量是一样的,唯一需要注意的是,原生字符串字面量除了能连接原生字符串字面量以外,还能连接普通字符串字面量。
在C++11标准中新引入了一个用户自定义字面量的概念,程序员可以通过自定义后缀将整数、浮点数、字符和字符串转化为特定的对象。这个特性往往用在需要大量声明某个类型对象的场景中,它能够减少一些重复类型的书写,避免代码冗余。一个典型的例子就是不同单位对象的互相操作,比如长度、重量、时间等,举个例子:
#include <iostream>
template<int scale, char … unit_char>
struct LengthUnit {
constexpr static int value = scale;
constexpr static char unit_str[sizeof…(unit_char) + 1] = { unit_char…, '\0' };
};
template<class T>
class LengthWithUnit {
public:
LengthWithUnit() : length_unit_(0) {}
LengthWithUnit(unsigned long long length) : length_unit_(length * T::value) {}
template<class U>
LengthWithUnit<std::conditional_t<(T::value > U::value), U, T>> operator+(const LengthWithUnit<U> &rhs)
{
using unit_type = std::conditional_t<(T::value > U::value), U, T>;
return LengthWithUnit<unit_type>((length_unit_ + rhs.get_length()) / unit_type::value);
}
unsigned long long get_length() const { return length_unit_; }
constexpr static const char* get_unit_str() { return T::unit_str; }
private:
unsigned long long length_unit_;
};
template<class T>
std::ostream& operator<< (std::ostream& out, const LengthWithUnit<T> &unit)
{
out << unit.get_length() / T::value << LengthWithUnit<T>::get_unit_str();
return out;
}
using MMUnit = LengthUnit<1, 'm', 'm'>;
using CMUnit = LengthUnit<10, 'c', 'm'>;
using DMUnit = LengthUnit<100, 'd', 'm'>;
using MUnit = LengthUnit<1000, 'm'>;
using KMUnit = LengthUnit<1000000, 'k', 'm'>;
using LengthWithMMUnit = LengthWithUnit<MMUnit>;
using LengthWithCMUnit = LengthWithUnit<CMUnit>;
using LengthWithDMUnit = LengthWithUnit<DMUnit>;
using LengthWithMUnit = LengthWithUnit<MUnit>;
using LengthWithKMUnit = LengthWithUnit<KMUnit>;
int main()
{
auto total_length = LengthWithCMUnit(1) + LengthWithMUnit(2) + LengthWithMMUnit(4);
std::cout << total_length;
}上面的代码定义了两个类模板,一个是长度单位LengthUnit,另外一个是带单位的长度LengthWithUnit,然后基于这两个类模板生成了毫米、厘米、分米、米和千米单位类以及它们对应的带单位的长度类。为了不同单位的数据相加,我们在类模板LengthWithUnit中重载了加号运算符,函数中总是会将较大的单位转换到较小的单位进行求和,比如千米和厘米相加得到的结果单位为厘米。最后,我们在main函数中对不同单位的对象求和并且输出求和结果。类模板的编写用到了一些模板元编程的知识,我们暂时可以忽略它们,现在需要关注的是main函数里的代码。我们发现每增加一个求和的操作数就需要重复写一个类型LengthWithXXUnit,当操作数很多的时候代码会变得很长,难以阅读和维护。当遇到这种情况的时候,我们可以考虑使用用户自定义字面量来简化代码,比如:
LengthWithMMUnit operator "" _mm(unsigned long long length)
{
return LengthWithMMUnit(length);
}
LengthWithCMUnit operator "" _cm(unsigned long long length)
{
return LengthWithCMUnit(length);
}
LengthWithDMUnit operator "" _dm(unsigned long long length)
{
return LengthWithDMUnit(length);
}
LengthWithMUnit operator "" _m(unsigned long long length)
{
return LengthWithMUnit(length);
}
LengthWithKMUnit operator "" _km(unsigned long long length)
{
return LengthWithKMUnit(length);
}
int main()
{
auto total_length = 1_cm + 2_m + 4_mm;
std::cout << total_length;
}上面的代码定义了5个字面量运算符函数,这些函数返回不同单位的长度对象,分别对应于毫米、厘米、分米、米和千米。字面量运算符函数的函数名会作为后缀应用于字面量。在main函数中,我们可以看到现在的代码省略了LengthWithXXUnit的类型声明,取而代之的是一个整型的字面量紧跟着一个以下画线开头的后缀_cm、_m或者_mm。在这里编译器会根据字面量的后缀去查找对应的字面量运算符函数,并根据函数形式对字面量做相应处理后调用该函数,如果编译器没有找到任何对应的函数,则会报错。所以这里的1_cm、2_m和4_mm分别等于调用了LengthWithCM- Unit(1)、LengthWithMUnit(2)和LengthWithMMUnit(4)。
接下来让我们看一看字面量运算符函数的语法规则,字面量运算符函数的语法和其他运算符函数一样都是由返回类型、operator关键字、标识符以及函数形参组成的:
retrun_type operator "" identifier (params)值得注意的是在C++11的标准中,双引号和紧跟的标识符中间必须有空格,不过这个规则在C++14标准中被去除。在C++14标准中,标识符不但可以紧跟在双引号后,而且还能使用C++的保留字作为标识符。标准中还建议用户定义的字面量运算符函数的标识符应该以下画线开始,把没有下画线开始的标识符保留给标准库使用。虽然标准并没有强制规定自定义的字面量运算符函数标识符必须以下画线开始,但是我们还是应该尽量遵循标准的建议。这一点编译器也会提示我们,如果使用了非下画线开始的标识符,它会给出明确的警告信息。
上文曾提到,用户自定义字面量支持整数、浮点数、字符和字符串4种类型。虽然它们都通过字面量运算符函数来定义,但是对于不同的类型字面量运算符函数,语法在参数上有略微的区别。
对于整数字面量运算符函数有3种不同的形参类型unsigned long long、const char *以及形参为空。其中unsigned long long和const char *比较简单,编译器会将整数字面量转换为对应的无符号long long类型或者常量字符串类型,然后将其作为参数传递给运算符函数。而对于无参数的情况则使用了模板参数,形如operator "" identifier<char…c>(),这个稍微复杂一些,我们在后面的例子中详细介绍。
对于浮点数字面量运算符函数也有3种形参类型long double、const char *以及形参为空。和整数字面量运算符函数相比,除了将unsigned long long换成了long double,没有其他的区别。
对于字符串字面量运算符函数目前只有一种形参类型列表const char * str, size_t len。其中str为字符串字面量的具体内容,len是字符串字面量的长度。
对于字符字面量运算符函数也只有一种形参类型char,参数内容为字符字面量本身:
#include <string>
unsigned long long operator "" _w1(unsigned long long n)
{
return n;
}
const char * operator "" _w2(const char *str)
{
return str;
}
unsigned long long operator "" _w3(long double n)
{
return n;
}
std::string operator "" _w4(const char* str, size_t len)
{
return str;
}
char operator "" _w5(char n)
{
return n;
}
unsigned long long operator ""if(unsigned long long n)
{
return n;
}
int main()
{
auto x1 = 123_w1;
auto x2_1 = 123_w2;
auto x2_2 = 12.3_w2;
auto x3 = 12.3_w3;
auto x4 = "hello world"_w4;
auto x5 = 'a'_w5;
auto x6 = 123if;
}在上面的代码中,根据字面量运算符函数的语法规则,后缀_w1和_w2可以用于整数,后缀_w3和_w2可以用于浮点数,而_w4和_w5分别用于字符串和字符。请注意最后一个if后缀,它必须用支持C++14标准的编译器才能编译成功。这个后缀有两点比较特殊,首先它使用保留关键字if作为后缀,其次它没有用下画线开头。前者能够这么做是因为C++14标准中字面量运算符函数双引号后紧跟的标识符允许使用保留字,而对于后者支持C++11标准的编译器通常允许这么做,只是会给出警告。
最后来看一下字面量运算符函数使用模板参数的情况(关于可变参数模板的内容会在第35章详细介绍),在这种情况下函数本身没有任何形参,字面量的内容通过可变模板参数列表<char…>传到函数,例如:
#include <string>
template <char…c> std::string operator "" _w()
{
std::string str;
//(str.push_back(c), …); // C++17的折叠表达式
using unused = int[];
unused{ (str.push_back(c), 0) … };
return str;
}
int main()
{
auto x = 123_w;
auto y = 12.3_w;
}上面这段代码展示了一个使用可变参数模板的字面量运算符函数,该函数通过声明数组展开参数包的技巧将char类型的模板参数push_back到str中。实际上,通常情况下很少会用到这种形式的字面量运算符函数,从易用性和可读性的角度来说它都不是一个好的选择,所以我建议还是采用上面提到的那些带有形参的字面量运算符函数。
本章介绍了C++11到C++17中字面量方面的优化,其中二进制整数字面量和十六进制浮点字面量增强了字面量的表达能力,让单引号作为整数的分隔符优化了长整数的可读性,用户自定义字面量让代码库作者能够为客户提供更加简洁的调用对象的方法,最实用的应该要数原生字符串字面量了,它让我们摆脱了复杂字符串中转义字符的干扰,让字符串所见即所得,在类似代码或者正则表达式等字符串上十分有用。