65.9K
CodeProject 正在变化。 阅读更多。
Home

C++11 用户自定义字面量在处理科学量、数字表示和字符串操作中的应用

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (30投票s)

2012 年 8 月 27 日

CPOL

7分钟阅读

viewsIcon

53352

downloadIcon

401

关键词:用户自定义字面量, 模板, 常量表达式, 递归函数

引言

本文解释了用户自定义字面量并提供了其应用示例。使用用户自定义字面量的示例只能在 GCC 4.7.0 及以上版本中编译。

用于处理科学量的模板

为便于演示,我们将仅考虑度量质量、长度和时间的量以及由此导出的量:面积、速度、加速度、频率、体积、力、压力等。这里提出的一些解决方案与 [1,2,3] 类似。

template<int M, int L, int T>
class Quantity
{
. . .
}; 

如果我们尝试添加(或减去)两个基于 Quantity 模板的类对象,但它们的 M、L 或 T 值不同,编译器将发出错误信号,因为它们的类将不兼容。这是 Quantity 模板的完整定义

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 getValue() const
    {
        return value;
    }        
 
private:    
    double value;    
};

我们必须定义额外的函数来操作 Quantity 对象

template <int M, int L, int T>
Quantity<M,L,T> operator+(const Quantity<M,L,T>& lhs, const Quantity<M,L,T>& rhs)
{
    return Quantity<M,L,T>(lhs)+=rhs;
}
template <int M, int L, int T>
Quantity<M,L,T> operator-(const Quantity<M,L,T>& lhs, const Quantity<M,L,T>& rhs)
{
    return Quantity<M,L,T>(lhs)-=rhs;
}
template <int M1, int L1, int T1, int M2, int L2, int T2>
Quantity<M1+M2,L1+L2,T1+T2> operator*(const Quantity<M1,L1,T1>& lhs, const Quantity<M2,L2,T2>& rhs)
{
    return Quantity<M1+M2,L1+L2,T1+T2>(lhs.getValue()*rhs.getValue());
}
template <int M, int L, int T>
Quantity<M,L,T> operator*(const double& lhs, const Quantity<M,L,T>& rhs)
{
    return Quantity<M,L,T>(lhs*rhs.getValue());
}
 
template <int M1, int L1, int T1, int M2, int L2, int T2>
Quantity<M1-M2,L1-L2,T1-T2> operator/(const Quantity<M1,L1,T1>& lhs, 
            const Quantity<M2,L2,T2>& rhs)
{
    return Quantity<M1-M2,L1-L2,T1-T2>(lhs.getValue()/rhs.getValue());
}
 
template <int M, int L, int T>
Quantity<-M, -L, -T> operator/(double x, const Quantity<M,L,T>& rhs)
{
    return Quantity<-M,-L,-T>(x/rhs.getValue());
}
 
template <int M, int L, int T>
Quantity<M, L, T> operator/(const Quantity<M,L,T>& rhs, double x)
{
    return Quantity<M,L,T>(rhs.getValue()/x);
}
 
template <int M, int L, int T>
bool operator==(const Quantity<M,L,T>& lhs, const Quantity<M,L,T>& rhs)
{
    return (lhs.getValue()==rhs.getValue());
}
 
template <int M, int L, int T>
bool operator!=(const Quantity<M,L,T>& lhs, const Quantity<M,L,T>& rhs)
{
    return (lhs.getValue()!=rhs.getValue());
}
 
template <int M, int L, int T>
bool operator<=(const Quantity<M,L,T>& lhs, const Quantity<M,L,T>& rhs)
{
    return lhs.getValue()<=rhs.getValue();
}
template <int M, int L, int T>
bool operator>=(const Quantity<M,L,T>& lhs, const Quantity<M,L,T>& rhs)
{
    return lhs.getValue()>=rhs.getValue();
}
template <int M, int L, int T>
bool operator< (const Quantity<M,L,T>& lhs, const Quantity<M,L,T>& rhs)
{
    return lhs.getValue()<rhs.getValue();
}
template <int M, int L, int T>
bool operator> (const Quantity<M,L,T>& lhs, const Quantity<M,L,T>& rhs)
{
    return lhs.getValue()>rhs.getValue();
}

使用 Quantity 模板,我们现在可以定义物理量类

typedef Quantity<1,0,0> Mass;
typedef Quantity<0,1,0> Length;
typedef Quantity<0,2,0> Area;
typedef Quantity<0,3,0> Volume;
typedef Quantity<0,0,1> Time;
typedef Quantity<0,1,-1> Speed;
typedef Quantity<0,1,-2> Acceleration;
typedef Quantity<0,0,-1> Frequency;
typedef Quantity<1,1,-2> Force;
typedef Quantity<1,-1,-2> Pressure; 

现在我们可以定义实际的物理单位

constexpr Length metre(1.0);
constexpr Length decimetre = metre/10;
constexpr Length centimetre = metre/100;
constexpr Length millimetre = metre/1000;
constexpr Length kilometre = 1000 * metre;
constexpr Length inch = 2.54 * centimetre;
constexpr Length foot = 12 * inch;
constexpr Length yard = 3 * foot;
constexpr Length mile = 5280 * foot;
constexpr Frequency Hz(1.0);
 
constexpr Area kilometre2 = kilometre*kilometre;
constexpr Area metre2 = metre*metre;
constexpr Area decimetre2 = decimetre*decimetre;
constexpr Area centimetre2 = centimetre*centimetre;
constexpr Area millimetre2 = millimetre * millimetre;
constexpr Area inch2 =inch*inch;
constexpr Area foot2 = foot*foot;
constexpr Area mile2 = mile*mile;
 
constexpr Volume kilometre3 = kilometre2*kilometre;
constexpr Volume metre3 = metre2*metre;
constexpr Volume decimetre3 = decimetre2*decimetre;
constexpr Volume litre = decimetre3;
constexpr Volume centimetre3 = centimetre2*centimetre;
constexpr Volume millimetre3 = millimetre2 * millimetre;
constexpr Volume inch3 =inch2*inch;
constexpr Volume foot3 = foot2*foot;
constexpr Volume mile3 = mile2*mile;
 
constexpr Time second(1.0);
constexpr Quantity<0,0,2> second2(1.0);
constexpr Time minute = 60 * second;
constexpr Time hour = 60 * minute;
constexpr Time day = 24 * hour;
 
constexpr Mass kg(1.0);
constexpr Mass gramme = 0.001 * kg;
constexpr Mass tonne = 1000 * kg;
constexpr Mass ounce = 0.028349523125 * kg; 
constexpr Mass pound = 16 * ounce;
constexpr Mass stone = 14 * pound; 

您可能希望定义额外的单位,如加仑、海里等。如果我们想打印一个量的数值,该怎么办?在这种情况下,您可以使用成员函数 getValue,它会以相应的 SI 单位(因为我们在 SI 系统中定义了基本单位)给出该量,或者使用 Convert 函数,在这种情况下我们必须明确指定要转换到的单位。使用 Convert 函数更明确,因此更好。以下是一些关于如何使用此方法的示例

Mass myMass = 80*kg;
cout << "my mass: " << myMass.Convert(kg) << " kg" << endl;
cout << "my mass: " << myMass.Convert(stone) << " stone" << endl;
cout << "my mass: " << myMass.Convert(pound) << " pounds" << endl;

此代码将打印(我设置了 15 位精度)

my mass: 80 kg
my mass: 12.5978435534216 stone
my mass: 176.369809747902 pounds

这是另一个例子

Length distance100 = 100*kilometre;
Time time = 2*hour;
Speed sp1 = distance100 / time;               
cout << "100 km in 2 hours: " << sp1.Convert(kilometre/hour) << " km/hour" << endl;            
cout << "100 km in 2 hours: " << sp1.Convert(mile/hour) << " miles/hour" << endl;

这将打印

100 km in 2 hours: 50 km/hour
100 km in 2 hours: 31.0685596118667 miles/hour

以上所有代码将在 Visual C++ 2008/2010/2011/2012 和 GCC 4.5 及以上版本中执行。

C++11 中的用户自定义字面量

在 C++11(2011 年通过的新标准)中,可以使用后缀表示法定义新的字面量。通过这种方法,我们可以编写字面量 10.0_kg20.0_g50.5_s 等。让我们考虑如何定义这样的字面量。如果我们想使用浮点数,通用定义将如下所示(方括号表示令牌是可选的)

[constexpr]  return_type  operator""  literal_suffix(long double parameter)
 { statements; return expression; };  

它看起来类似于函数定义。constexpr 令牌不是强制性的,但如果您希望在编译时评估字面量,建议使用它。返回类型将是创建的字面量的类型(在本例中为 Mass、Time 等)。literal_suffix 令牌是一个标识符:_kg、_g_sparameter 令牌代表将接收浮点数值的参数。如果我们写 10.0_kg,则值 10.0 将赋给参数。

类型 long double 是浮点字面量唯一允许的类型(可用的最大浮点数)。此处 expression 代表包含将被评估以产生结果的参数的表达式。_kg 字面量的定义可以如下

constexpr Mass operator"" _kg(long double x)  { return Mass(x); } 

现在我们可以以类似的方式定义其他字面量

constexpr Length operator"" _mm(long double x) { return x*millimetre; }
constexpr Length operator"" _cm(long double x)  { return x*centimetre; }
constexpr Length operator"" _m(long double x)  { return x*metre; }
constexpr Speed operator"" _mps(long double x)  { return Speed(x); }
constexpr Speed operator"" _miph(long double x) { return x*mile/hour; }
constexpr Speed operator"" _kmph(long double x) { return x*kilometre/hour; } 

您可能会问下划线是否真的有必要。答案是否定的。标准并不正式禁止您定义不带下划线的字面量,但仅包含字母的字面量可能用于将来的扩展。例如,标准前缀优于用户自定义前缀。例如,您尝试重新定义 LL 后缀将被忽略。因此,加上下划线是一个好习惯。

另一个重要点:如果您只定义了 long double 参数,则必须仅在字面量中使用浮点数:例如 10.0_kg。不允许使用:10_kg。但是,您可以定义一个具有相同名称的另一个后缀

constexpr Mass operator"" _kg(unsigned long long x)  { return Mass(static_cast<double>(x)); }  

如果两个定义都存在,我们可以在程序中使用 10.0_kg 和 10_kg。为了能够使用 constexpr 字面量,我们必须重新定义 Quantity 模板,引入 constexpr 令牌

template<int M, int L, int T>
class Quantity
{
public:
    constexpr Quantity(double val) : value(val){}
 
    constexpr Quantity& operator+=(const Quantity& rhs)
    {
        value+=rhs.value;
        return *this;
    }
 
    constexpr Quantity& operator-=(const Quantity& rhs)
    {
        value-=rhs.value;
        return *this;
    }
 
    constexpr double Convert(const Quantity& rhs)
    {
        return value/rhs.value;                    
    }
 

    constexpr double getValue() const
    {
        return value;
    }        
 
private:    
    double value;    
};

同样适用于运算符和单位定义:它们都应使用 constexpr 令牌进行定义。现在我们可以定义以下转换宏,它将使我们能够编写更短的转换表达式

#define ConvertTo(_x, _y) (_x).Convert(1.0_##_y) 

现在,而不是

(20 * mile).Convert(kilometre) 

我们可以写

ConvertTo(20.0_mi, km) 

您可以将您的后缀(不带下划线)作为转换宏的第二个参数。

表示 π 乘以一个因子

您可以定义一个方便的后缀来表示 π 的值

constexpr long double operator "" _pi(long double x) 
{ return x * 3.1415926535897932384626433832795;}
constexpr long double operator "" _pi(unsigned long long int x) 
{ return x * 3.1415926535897932384626433832795;} 

在这种情况下,您可以这样写字面量

2_pi

-2.5_pi

表示二进制数

基本方法

您可能希望在程序中使用二进制数。在这种情况下,您提供的所有字符都将被视为字符串。通用语法将是

[constexpr]  return_type  operator""  literal_suffix (const char* parameter)
{ statements ; return expression; } 

为了将字符串转换为字面量,我们可以编写以下递归函数(常量表达式中只允许递归函数,参见 [4])

constexpr unsigned long long ToBinary(unsigned long long x, const char* s)
{
    return (!*s ? x : ToBinary(x + x + (*s =='1'? 1 : 0), s+1));
}

现在我们可以定义后缀 _b 如下

constexpr unsigned long long int operator "" _b(const char* s) 
{ return ToBinary(0,s);}

以下是一些用法示例

std::cout << "1101(2) = " << 1101_b  << std::endl;
std::cout << "1111 1111 1111 1111 1111(2) = " 
    << std::hex << 11111111111111111111_b  << "(16)" << std::endl;
std::cout << "10 1010 1010 1010 1010(2) = " 
    << std::hex << 101010101010101010_b  << "(16)" << std::endl;  

此代码片段将打印

1101(2) = 13
1111 1111 1111 1111 1111(2) = fffff(16)
10 1010 1010 1010 1010(2) = 2aaaa(16) 

为二进制数添加比例因子

允许二进制数具有比例因子会很方便,这样我们就可以轻松地将二进制数字定位在特定位置。比例因子可以是十进制的。例如,我们可以写

1e32_b,表示 2^32;
1011e16_b,表示 11*2^16 = 720896。

我们可以定义以下后缀

unsigned long long operator"" _b(const char* str) 
{             
    return ToScaledBinary(0,0,str);
}  

任务是定义 ToScaledBinary 递归函数。首先,让我们定义 Scale 函数,它将把一串十进制数字转换为一个数字

constexpr unsigned long long Scale(unsigned long long x, const char* s)
{
    return (!*s ? x : Scale(10*x + ((unsigned long long)*s)-((unsigned long long)'0'), s+1));
} 

此函数将用于转换比例因子。现在我们可以定义 ToScaledBinary 函数,它是我们之前定义的 ToBinary 函数的一个小改进

constexpr unsigned long long ToScaledBinary
    (unsigned long long x, unsigned long long scale, const char* s)
{
    return (!*s 
                ? x 
                : ( *s == 'e' || *s == 'E'  
                        ? (x << Scale(0,s+1)) // put the digits in the right position
                        : ToScaledBinary(x + x + (*s =='1'? 1 : 0), scale,  s+1)));
} 

以下是一些用法示例

std::cout << std::hex << "0x" << 1111111111_b << std::endl;    
std::cout << std::hex << "0x" << 1111111111e16_b << std::endl;
std::cout << std::hex << "0x" << 1e32_b << std::endl;    
std::cout << std::dec << 1e32_b << std::endl;    
std::cout << std::dec << 1011e16_b << std::endl;  

程序的输出将是:

0x3ff
0x3ff0000
0x100000000
4294967296
720896

另一种方法是定义运算符为一个可变参数模板

template<char...chars>
unsigned long long operator"" _b() 
{         
    constexpr static char str[] = {chars..., '\0'};
    return ToScaledBinary(0,0,str); 
} 

可变参数模板允许我们使用多个参数。我们定义一个静态字符数组,并将所有这些值赋给它。我个人认为在这种情况下使用可变参数模板没有任何优势。使用字符串参数定义运算符更容易,而且无论如何最好使用不带模板的常量表达式。

字符串操作

可以使用常量字面量处理任何字符的字符串,在这种情况下,字符应被双引号包围,例如(后缀 _UP 和 _S 是用户定义的)

"apple"_UP
"zxy"_S

标准还允许使用 UTF_8、Unicode(16 位和 32 位)以及宽字符字符串(此处 _w1、_w2、_w3 和 _w4 是用户定义的后缀)

u8"one"_w1
u"one"_w2
U"one"_w3
L"one"_w4 

字面量的通用格式如下

[constexpr]  return_type  operator""  literal_suffix(const char_type * str, size_t  length)
 
{ statements; return expression; };

length 参数是强制性的。char_type 可以是以下之一:char、char16_t、char32_t、wchar_t。您可以使用此类用户自定义字面量将字符串转换为不同的表示形式或将字符串字面量放入容器中。

我们来看一个简单的例子,其中所有字符的字符串字面量都被转换为大写。因此,以下字面量:“apple”_UP、“Apple”_UP、“APPLE”_UP 将被转换为 std::string,其内容为“APPLE”。std::string 不能在常量表达式中使用,因此我们不使用 contstexpr 令牌来定义此运算符
std::string operator "" _UP(const char* s, std::size_t n) 
{
    std::string str(s,n);
    for (char &c: str)
    {       
       c = toupper(c);
    }
    return str;
} 

以下是一些示例

std::cout << "apple"_UP << std::endl;
std::cout << ("APPLE" == "apple"_UP) << std::endl; 

此代码将打印

APPLE
1  

在用户自定义字面量中使用字符

您可以在用户自定义字面量中使用字符,例如

u'π'_const

u'e'_const

在这种情况下,定义后缀的通用语法将是

[constexpr] return_type  operator""  literal_suffix(char_type parameter)
{ statements; return expression; };  

char_type 可以是以下之一:char、char16_t、char32_t、wchar_t。您可以使用此语法来包含不允许在标识符中使用的字符,例如 'π''µ'

以下是字符字面量的示例后缀定义

constexpr double operator "" _const(char16_t c) 
{     
    return (c == u'π' ? 3.141592653589793 
                      : (c == u'e' ? 2.718281828459045 : 0));
} 

我们可以这样使用它

std::cout << std::setprecision(16) << u'π'_const << std::endl;
std::cout << u'e'_const << std::endl;    

用户自定义字面量语法的详细说明:允许和不允许的内容

以下是一些基本语法以及用户自定义字面量中允许的内容(可选项目用方括号括起来)

(1)

[constexpr]  return_type  operator""  literal_suffix(unsigned long long parameter){…};  

字面量可以是十进制整数、十六进制数、八进制数;

(2)

[constexpr]  return_type  operator""  literal_suffix(long double parameter){…}; 

这里可以使用浮点数;

(3)

[constexpr]  return_type  operator""  literal_suffix(const char* parameter){…}; 
字面量可以是十进制整数、十六进制数、八进制数或浮点数;

(4)

[constexpr]  template<char … chrs> return_type  operator"" literal_suffix () {…} ; 

字面量可以是十进制整数、十六进制数、八进制数或浮点数;

(5)

[constexpr]  return_type  operator""  literal_suffix(const char_type* str, size_t  length ){…};  

字面量可以是 char_type 的字符串。

(6)

[constexpr] return_type  operator""  literal_suffix(char_type parameter) {…}; 

字面量可以是 char 类型。

这些规则表明以下字面量(后缀:_txt_32)不能使用

programming_txt

AHZ_32

但是以下(后缀:_txt_32_7)是允许的

“programming”_txt

“AHZ”_ 32

23456_7

参考文献

[1] http://learningcppisfun.blogspot.co.uk/2007/01/units-and-dimensions-with-c-templates.html

[2] David Abrahams and Aleksey Gurtovoy.C++ Template Metaprogramming: Concepts, Tools, and Techniques from Boost and Beyond (C++ in Depth)。

[3] https://boost.ac.cn/doc/libs/1_51_0/doc/html/boost_units.html

[4] https://codeproject.org.cn/Articles/417719/Constants-and-Constant-Expressions-in-Cplusplus11

© . All rights reserved.