在C++编程的世界里,C++11引入了许多令人瞩目的新特性,其中用户定义的字面量(User-Defined Literals,简称UDLs)无疑是一项强大且实用的功能。它为程序员提供了前所未有的灵活性和便利性,允许我们根据自己的需求定义字面量,从而使代码更加直观、易读且富有表现力。本文将带领你从入门开始,逐步深入了解C++11 User-Defined Literals,直至精通并能在实际项目中熟练运用。
在C++中,字面量是程序中直接使用的固定值,它们是源代码中用于表示数据的常量形式。常见的字面量包括整数(如42)、浮点数(如3.14)、字符串(如"hello")等。在C++11之前,这些字面量的类型和值都是预定义好的,程序员无法对其进行自定义。
C++11引入了用户定义的字面量,这一特性允许程序员定义自己的字面量运算符,从而创建具有特定含义和行为的字面量。例如,我们可以定义一个字面量运算符 _km
,使得 10_km
不仅是一个数值,而是明确表示10公里的距离。这种自定义字面量的能力,为代码的可读性和可维护性带来了显著提升。
用户定义的字面量是通过定义字面量运算符来实现的。字面量运算符是一种特殊的函数,其名称以 operator ""
开头,后面紧跟着一个用户自定义的标识符。这个标识符用于区分不同的字面量运算符,同时也为字面量赋予了特定的语义。其基本语法如下:
返回值类型 operator "" 自定义后缀 (参数);
如果字面量运算符是一个模板,它必须有一个空的参数列表,并且只能有一个模板参数,这个模板参数必须是一个元素类型为 char 的非类型模板参数包。在这种情况下它被称为数字字面量运算符模板,语法如下:
template < char ...> 返回值类型 operator "" 自定义后缀 ();
需要注意的是,自定义后缀必须以下划线 _
开头,并符合标识符命名规范。虽然“以下划线开头”并非是从语法上强制性的,但如果坚持不以下划线开头,编译器会给出警告。另外,由于字面量的特殊语法结构,自定义的后缀其实可以同时是C++关键字而不产生冲突,这是合法的。
对于字面量运算符的参数,C++有语法上严格的规定。参数列表仅允许以下几种类型:
unsigned long long int
:用于表示整数,是用户定义整型字面量运算符的首选方式。long double
:用于表示浮点数,是用户定义浮点型字面量运算符的首选方式。char
:用于表示单个字符。const char *
:用于表示字符串。const char *, size_t size
:也用于表示字符串,其中第二个参数会自动传入字符串长度。inline
或 constexpr
,可能具有内部或外部链接,可以显式调用,其地址也可以被获取。以下是一个定义整数字面量运算符的示例,我们定义一个 _dozen
后缀,用于将输入的整数乘以12:
// 定义整数字面量运算符
int operator "" _dozen(unsigned long long d) {
return d * 12;
}
在这个例子中,当使用 10_dozen
时,它会将10乘以12,结果为120。这里的参数类型 unsigned long long
是C++11为整数字面量运算符提供的专用类型,它可以确保在编译时捕获整数字面量。
对于浮点数字面量运算符,定义方式类似,但参数类型有所不同。例如,我们定义一个 _percent
后缀,用于将输入的浮点数转换为百分比形式:
// 定义浮点数字面量运算符
long double operator "" _percent(long double p) {
return p / 100.0;
}
使用 50.0_percent
时,结果为0.5。
字符串字面量运算符用于处理字符串字面量,其定义方式与数值字面量运算符略有不同,主要体现在参数类型上。以下是一个将字符串字面量转换为 std::string
对象的示例:
std::string operator "" _path(const char* str, size_t len) {
return std::string(str, len);
}
在这个例子中,_path
是一个字符串字面量运算符,它将字符串字面量转换为 std::string
对象。参数 const char* str
指向字符串字面量的首字符,size_t len
表示字符串的长度。通过这种方式,我们可以方便地创建具有特定路径格式的字符串对象。
我们还可以为自定义类型定义字面量运算符。例如,定义一个表示二维坐标的类 Point2D
,并为其定义字面量运算符:
class Point2D {
public:
long double x, y;
Point2D(long double x, long double y) : x(x), y(y) {}
// 其他成员函数...
};
Point2D operator "" _point(unsigned long long x, unsigned long long y) {
return Point2D(x, y);
}
使用这个字面量运算符,我们可以方便地创建 Point2D
对象,如 Point2D p = 3_point_4;
,它创建了一个坐标为(3, 4)的点。
在数学和工程领域,复数的使用非常广泛。用户定义的字面量可以方便地定义复数字面量。以下是一个定义复数字面量运算符的示例:
#include
std::complex<long double> operator "" _i(long double x) {
return std::complex<long double>(0, x);
}
使用这个字面量运算符,我们可以轻松创建复数,如 std::complex
,这里的 z
是一个复数 0 + 3i
。
我们可以定义一个字面量运算符来处理二进制字面量,将二进制字符串转换为 std::bitset
对象:
#include
#include
template < char ... Bits >
struct checkbits {
static const bool valid = false ;
} ;
template < char High , char ... Bits >
struct checkbits < High , Bits ... >
{
static const bool valid = ( High == '0' || High == '1' )
&& checkbits < Bits ... > :: valid ;
} ;
template < char High >
struct checkbits < High >
{
static const bool valid = ( High == '0' || High == '1' ) ;
} ;
template < char ... Bits >
inline constexpr std::bitset < sizeof ... ( Bits ) >
operator "" _bits ( ) noexcept {
static_assert ( checkbits < Bits ... > :: valid , "invalid digit in binary string" ) ;
return std::bitset < sizeof ... ( Bits ) > ( ( char [ ] ) { Bits ... , '\0' } ) ;
}
使用示例:
int main ( )
{
auto bits = 0101010101010101010101010101010101010101010101010101010101010101_bits ;
std::cout << bits << std::endl ;
std::cout << "size = " << bits.size() << std::endl ;
std::cout << "count = " << bits.count() << std::endl ;
std::cout << "value = " << bits.to_ullong() << std::endl ;
// 这会在编译时触发静态断言
// auto badbits = 2101010101010101010101010101010101010101010101010101010101010101_bits ;
}
这个示例展示了如何使用用户定义的字面量来创建二进制字面量,并在编译时进行有效性检查。
用户定义的字面量还可以结合模板和常量表达式,实现更强大的功能。例如,我们可以定义一个模板类来处理科学量,并为其定义字面量运算符:
template < int m, int l, int t >
class quantity {
public:
explicit quantity(double val = 0.0) : value(val) {}
quantity(const quantity & x) : value(x.value) {}
quantity & operator += (const quantity & rhs) {
value += rhs.value;
return *this;
}
quantity & operator -= (const quantity & rhs) {
value -= rhs.value;
return *this;
}
double convert(const quantity & rhs) {
return value / rhs.value;
}
double get_value() const {
return value;
}
private:
double value;
};
// 定义长度的字面量运算符
quantity<0, 1, 0> operator "" _m(long double x) {
return quantity<0, 1, 0>(x);
}
这样,我们就可以使用 10.0_m
来表示10米的长度,并且可以进行相应的计算和操作。
在科学计算和工程领域,物理单位的正确使用至关重要。用户定义的字面量可以方便地定义各种物理单位,如长度、质量、时间等,从而使代码更具物理意义。以下是一个简单的示例,定义了长度、质量和时间的单位:
long double operator "" _km(long double x) {
return x * 1000; // 将公里转换为米
}
long double operator "" _kg(long double x) {
return x; // 千克
}
long double operator "" _s(long double x) {
return x; // 秒
}
使用这些字面量运算符,我们可以编写更具物理意义的代码:
long double distance = 10.0_km;
long double mass = 5.0_kg;
long double time = 2.0_s;
在日志和调试过程中,用户定义的字面量可以用于生成带有额外信息的日志条目,例如时间戳、日志级别等。以下是一个简单的示例,定义一个日志级别字面量:
#include
enum class LogLevel { DEBUG, INFO, WARNING, ERROR };
std::ostream& operator<<(std::ostream& os, LogLevel level) {
switch (level) {
case LogLevel::DEBUG: os << "DEBUG"; break;
case LogLevel::INFO: os << "INFO"; break;
case LogLevel::WARNING: os << "WARNING"; break;
case LogLevel::ERROR: os << "ERROR"; break;
}
return os;
}
void log(const char* message, LogLevel level) {
std::cout << "[" << level << "] " << message << std::endl;
}
// 定义日志级别字面量运算符
void operator "" _log(const char* message, LogLevel level) {
log(message, level);
}
使用示例:
int main() {
"This is a debug message"_log(LogLevel::DEBUG);
"This is an error message"_log(LogLevel::ERROR);
return 0;
}
在处理配置文件时,用户定义的字面量可以用于解析特定格式的字符串,例如从一个配置字面量中提取键值对。以下是一个简单的示例,定义一个配置解析字面量:
#include
#include
#include
std::unordered_map<std::string, std::string> parseConfig(const char* str) {
std::unordered_map<std::string, std::string> config;
std::string key, value;
bool inKey = true;
for (const char* p = str; *p; ++p) {
if (*p == '=') {
inKey = false;
} else if (*p == ';') {
config[key] = value;
key.clear();
value.clear();
inKey = true;
} else {
if (inKey) {
key += *p;
} else {
value += *p;
}
}
}
if (!key.empty()) {
config[key] = value;
}
return config;
}
// 定义配置解析字面量运算符
std::unordered_map<std::string, std::string> operator "" _cfg(const char* str) {
return parseConfig(str);
}
使用示例:
int main() {
auto config = "key1=value1;key2=value2"_cfg;
for (const auto& pair : config) {
std::cout << pair.first << " = " << pair.second << std::endl;
}
return 0;
}
用户定义的字面量应该谨慎使用,因为它们可能会使代码的可读性降低,特别是对于不熟悉UDLs的开发者。因此,在定义自定义后缀时,应该选择具有明确语义的名称,并且尽量避免使用过于复杂的逻辑。
在使用时,要注意UDLs可能会与现有的操作符重载冲突。在定义字面量运算符时,应该确保其不会与其他函数或操作符产生歧义。
由于最大吞噬规则,以 e
和 E
(C++17起还有 p
和 P
)结束的用户定义整数和浮点字面量,在后随运算符 +
或 -
时,必须在源码中以空白符或括号与运算符分隔。同样的规则适用于后随整数或浮点用户定义字面量的点运算符。否则会组成单个非法预处理数字记号,导致编译失败。例如:
long double operator "" _E( long double );
long double operator "" _a( long double );
int operator "" _p( unsigned long long );
auto x = 1.0 _E+ 2.0 ; // 错误
auto y = 1.0 _a+ 2.0 ; // OK
auto z = 1.0 _E + 2.0 ; // OK
auto q = ( 1.0 _E)+ 2.0 ; // OK
auto w = 1 _p+ 2 ; // 错误
auto u = 1 _p + 2 ; // OK
C++11引入的用户定义字面量为程序员提供了强大的自定义能力,使代码更加直观、易读且富有表现力。通过合理使用用户定义的字面量,我们可以在单位转换、物理计算、日志调试、配置文件解析等多个方面提高代码的质量和可维护性。然而,在使用过程中,我们也需要注意代码的可读性、操作符重载冲突等问题,确保在不牺牲代码清晰度的前提下发挥用户定义字面量的优势。希望本文能帮助你从入门到精通C++11 User-Defined Literals,并在实际项目中灵活运用这一强大的特性。