C++11 User-Defined Literals:从入门到精通
文章目录
- 一、引言
- 二、基本概念
- 2.1 字面量的定义
- 2.2 用户定义的字面量的引入
- 2.3 字面量运算符的定义语法
- 2.4 字面量运算符的参数类型限制
- 三、入门示例
- 3.1 定义数值字面量运算符
- 3.1.1 整数字面量运算符
- 3.1.2 浮点数字面量运算符
- 3.2 定义字符串字面量运算符
- 3.3 定义自定义类型的字面量运算符
- 四、高级应用和复杂示例
- 4.1 定义复数
- 4.2 处理二进制字面量
- 4.3 结合模板和常量表达式
- 五、在实际项目中的使用
- 5.1 单位转换和物理计算
- 5.2 日志和调试
- 5.3 配置文件解析
- 六、注意事项
- 6.1 代码可读性
- 6.2 操作符重载冲突
- 6.3 最大吞噬规则
- 七、总结
一、引言
在C++编程的世界里,C++11引入了许多令人瞩目的新特性,其中用户定义的字面量(User-Defined Literals,简称UDLs)无疑是一项强大且实用的功能。它为程序员提供了前所未有的灵活性和便利性,允许我们根据自己的需求定义字面量,从而使代码更加直观、易读且富有表现力。本文将带领你从入门开始,逐步深入了解C++11 User-Defined Literals,直至精通并能在实际项目中熟练运用。
二、基本概念
2.1 字面量的定义
在C++中,字面量是程序中直接使用的固定值,它们是源代码中用于表示数据的常量形式。常见的字面量包括整数(如42)、浮点数(如3.14)、字符串(如"hello")等。在C++11之前,这些字面量的类型和值都是预定义好的,程序员无法对其进行自定义。
2.2 用户定义的字面量的引入
C++11引入了用户定义的字面量,这一特性允许程序员定义自己的字面量运算符,从而创建具有特定含义和行为的字面量。例如,我们可以定义一个字面量运算符 _km
,使得 10_km
不仅是一个数值,而是明确表示10公里的距离。这种自定义字面量的能力,为代码的可读性和可维护性带来了显著提升。
2.3 字面量运算符的定义语法
用户定义的字面量是通过定义字面量运算符来实现的。字面量运算符是一种特殊的函数,其名称以 operator ""
开头,后面紧跟着一个用户自定义的标识符。这个标识符用于区分不同的字面量运算符,同时也为字面量赋予了特定的语义。其基本语法如下:
返回值类型 operator "" 自定义后缀 (参数);
如果字面量运算符是一个模板,它必须有一个空的参数列表,并且只能有一个模板参数,这个模板参数必须是一个元素类型为 char 的非类型模板参数包。在这种情况下它被称为数字字面量运算符模板,语法如下:
template < char ...> 返回值类型 operator "" 自定义后缀 ();
需要注意的是,自定义后缀必须以下划线 _
开头,并符合标识符命名规范。虽然“以下划线开头”并非是从语法上强制性的,但如果坚持不以下划线开头,编译器会给出警告。另外,由于字面量的特殊语法结构,自定义的后缀其实可以同时是C++关键字而不产生冲突,这是合法的。
2.4 字面量运算符的参数类型限制
对于字面量运算符的参数,C++有语法上严格的规定。参数列表仅允许以下几种类型:
unsigned long long int
:用于表示整数,是用户定义整型字面量运算符的首选方式。long double
:用于表示浮点数,是用户定义浮点型字面量运算符的首选方式。char
:用于表示单个字符。const char *
:用于表示字符串。const char *, size_t size
:也用于表示字符串,其中第二个参数会自动传入字符串长度。
不允许使用默认参数,也不允许使用C语言链接。除了上述限制之外,字面量运算符(和字面量运算符模板)是普通函数(和函数模板),它们可以声明为inline
或constexpr
,可能具有内部或外部链接,可以显式调用,其地址也可以被获取。
三、入门示例
3.1 定义数值字面量运算符
3.1.1 整数字面量运算符
以下是一个定义整数字面量运算符的示例,我们定义一个 _dozen
后缀,用于将输入的整数乘以12:
// 定义整数字面量运算符
int operator "" _dozen(unsigned long long d) {return d * 12;
}
在这个例子中,当使用 10_dozen
时,它会将10乘以12,结果为120。这里的参数类型 unsigned long long
是C++11为整数字面量运算符提供的专用类型,它可以确保在编译时捕获整数字面量。
3.1.2 浮点数字面量运算符
对于浮点数字面量运算符,定义方式类似,但参数类型有所不同。例如,我们定义一个 _percent
后缀,用于将输入的浮点数转换为百分比形式:
// 定义浮点数字面量运算符
long double operator "" _percent(long double p) {return p / 100.0;
}
使用 50.0_percent
时,结果为0.5。
3.2 定义字符串字面量运算符
字符串字面量运算符用于处理字符串字面量,其定义方式与数值字面量运算符略有不同,主要体现在参数类型上。以下是一个将字符串字面量转换为 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
表示字符串的长度。通过这种方式,我们可以方便地创建具有特定路径格式的字符串对象。
3.3 定义自定义类型的字面量运算符
我们还可以为自定义类型定义字面量运算符。例如,定义一个表示二维坐标的类 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)的点。
四、高级应用和复杂示例
4.1 定义复数
在数学和工程领域,复数的使用非常广泛。用户定义的字面量可以方便地定义复数字面量。以下是一个定义复数字面量运算符的示例:
#include <complex>
std::complex<long double> operator "" _i(long double x) {return std::complex<long double>(0, x);
}
使用这个字面量运算符,我们可以轻松创建复数,如 std::complex<long double> z = 3.0_i;
,这里的 z
是一个复数 0 + 3i
。
4.2 处理二进制字面量
我们可以定义一个字面量运算符来处理二进制字面量,将二进制字符串转换为 std::bitset
对象:
#include <bitset>
#include <iostream>
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 ;
}
这个示例展示了如何使用用户定义的字面量来创建二进制字面量,并在编译时进行有效性检查。
4.3 结合模板和常量表达式
用户定义的字面量还可以结合模板和常量表达式,实现更强大的功能。例如,我们可以定义一个模板类来处理科学量,并为其定义字面量运算符:
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米的长度,并且可以进行相应的计算和操作。
五、在实际项目中的使用
5.1 单位转换和物理计算
在科学计算和工程领域,物理单位的正确使用至关重要。用户定义的字面量可以方便地定义各种物理单位,如长度、质量、时间等,从而使代码更具物理意义。以下是一个简单的示例,定义了长度、质量和时间的单位:
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;
5.2 日志和调试
在日志和调试过程中,用户定义的字面量可以用于生成带有额外信息的日志条目,例如时间戳、日志级别等。以下是一个简单的示例,定义一个日志级别字面量:
#include <iostream>
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;
}
5.3 配置文件解析
在处理配置文件时,用户定义的字面量可以用于解析特定格式的字符串,例如从一个配置字面量中提取键值对。以下是一个简单的示例,定义一个配置解析字面量:
#include <iostream>
#include <string>
#include <unordered_map>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;
}
六、注意事项
6.1 代码可读性
用户定义的字面量应该谨慎使用,因为它们可能会使代码的可读性降低,特别是对于不熟悉UDLs的开发者。因此,在定义自定义后缀时,应该选择具有明确语义的名称,并且尽量避免使用过于复杂的逻辑。
6.2 操作符重载冲突
在使用时,要注意UDLs可能会与现有的操作符重载冲突。在定义字面量运算符时,应该确保其不会与其他函数或操作符产生歧义。
6.3 最大吞噬规则
由于最大吞噬规则,以 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,并在实际项目中灵活运用这一强大的特性。