g++: "error: unable to find string literal operator"
g++: "warning: invalid suffix on literal"

我:之前写的代码还能好好地编译,使用了带有C++11编译选项后编译器居然说代码有问题,肯定是编译器错了!!!

#include <cstdio>
#define _SLO_ "string literal operator."
int main()
{
    printf("Hello "_SLO_);
    return 0;
}

上述代码无法通过编译,然而,如果在_SLO_前面加一个空格,又可以正确地编译了,这是为什么呢?

问题解答

原来,C++11标准中增加了一种称为 “literal operator” 的操作符,用户可以采用这种方法来自定义新的操作,以便于提供易于读懂的快捷语法。例如,我们需要实现一种度量距离的类:

class Dist {
    long double meter;
public:
    Dist(long double m)
        : meter(m) {}
};

在国际单位中,使用米(meter)来度量距离,但有时候使用其它的度量更能使人理解,例如:两地的距离958km,屏幕的对角线距离9.7inch, 碳原子半径91pm等等。 如果在定义距离的时候每次都加上一个数量级,会给阅读代码带来困难且不易于维护,采用literal operator,可以定义以下函数解决这个问题。

Dist operator "" _km(long double km)
{
    return Dist(km * 1000);
}
// 定义了函数之后,就可以通过以下方式来构造一个距离对象:
Dist dist = 1.23_km;

Literal operator

常见的Literal operator 参数列表如下:

operator "" identifier (const char *);
operator "" identifier (unsigned long long int);
operator "" identifier (long double);
operator "" identifier (char);
operator "" identifier (const char*, std::size_t);
// 后两种还有对应的 wchar_t, char16_t, char32_t 版本,此处略去

其中, identifier 是一个以下划线 “_” 开始的字符串,不以下划线开始的Literal operator 被保留给标准库使用。用户只能自定义以上类型的Literal operator, 比如用户不能定义unsigned int、double等类型的Literal operator, 甚至不能定义 long long int 类型的Literal operator。

Dist dist = -1_km; // error: no match for 'operator-'
Dist dist = -(1_km); // error: no match for 'operator-'

第一种方法在编译器看来和第二种相同,负号被解析为一个一元操作符,因此首先调用operator””_km,然后再执行operator-,由于Dist类没有重载负号,因此编译器会报错。

Literal operator template

如果字面值操作符是一个模板,它必须具有空的参数列表,且只能有一个char… 类型的模板参数,即

template<char...> result_type operator "" identifier();

例如我们需要实现一个三进制转换函数,则下面的字面值操作符模板可以完成这项工作:

#include<iostream>
using namespace std;
 
template<unsigned long long R>
constexpr unsigned long long to_ternary()
{
    return R;
}
template<unsigned long long R, char ch, char... C>
constexpr unsigned long long to_ternary()
{
    static_assert(ch == '0' || ch == '1' || ch == '2', "invalid ternary.");
    static_assert(3*R+(ch-'0') > R, "invalid ternary.");
    return to_ternary<3*R+(ch-'0'), C...>();
}
template<char... C>
unsigned long long operator "" _ternary()
{
    return to_ternary<0ULL, C...>();
}
int main()
{
    cout << 1002_ternary << endl; // 29
    return 0;
}

有的读者可能会问:这么简单的转换只需要一个循环就足够了,为什么要写成好几个函数模板,让问题变得这么复杂?实际上,这种方法只能应用于字面值常量,对于变量字符串则无能为力,而对字面值常量采用这种方法有以下几种优点:可以在编译期判断是否有非法字符;可以在编译期判断是否溢出;在编译器优化的情况下,以上函数模板没有运行时开销;采用字面值操作符(在某些情况下)更容易读懂。 C++的语法用于向编写者提供各种不同的工具,而是否使用、如何使用、何时使用完全取决于代码的编写者,解决某个问题往往会有很多种方法。

名称查找

当编译器遇到一个用户定义的字面值以后,将会采用以下策略查找函数定义:

1. 若字面值为一个整数类型
如果存在一个参数类型为unsigned long long 的字面值操作符函数,则调用这个函数;
否则,函数重载中必须有且仅有一个字符串类型参数的函数或模板函数,并调用这个函数。
2. 若字面值为一个浮点数类型
如果存在一个参数类型为long double 的字面值操作符函数,则调用这个函数;
否则,函数重载中必须有且仅有一个字符串类型参数的函数或模板函数,并调用这个函数。
3. 若字面值为字符串,则调用 operator “” identifier (str, len),其中str为字符串常量,len为其长度
4. 若字面值为字符型,则调用 operator “” identifier (ch),其中ch为字符。

long double operator "" _x(long double d){return d;};
std::u16string operator "" _x(const char16_t* str, size_t len){return u16string(str);};
unsigned long long operator "" _x(const char* str){ return stoull(str); }
int main()
{
    auto a = 3.14_x; // operator "" _x(3.14L)
    auto b = u"string"_x; // operator "" _x(u"string", 6)
    auto c = 12345_x; // operator "" _x("12345")
    auto d = "12345"_x; // error, not operator "" _x("12345")
}

标准库中的应用(C++14)

#include<iostream>
#include<string>
#include<chrono>
#include<complex>
using namespace std;
using namespace std::literals;
 
int main()
{
    auto three_hour = 3h; // chrono::duration: 3 hour
    auto half_second = 0.5s; // chrono::duration: 0.5 second
    auto ten_minutes = 10min; // chrono::duration: 10 minutes
    // ... 
 
    complex<double> cp = 1.2 + 3.4i; // complex<double>: 1.2 + 3.4i
    complex<long double> cpl = 1.2L + 3.4il; // complex<long double>: 1.2 + 3.4il
    complex<float> cpf = 1.2f + 3.4if; // complex<float>: 1.2f + 3.4if
 
    string s1 = "string\0\0literal"; // s1 == "string"
    string s2 = "string\0\0literal"s; // s2 == "string\0\0literal"
    cout << s1 << '\n' << s2 << endl;
    return 0;
}

注意到if是C++关键字,C++标准允许将关键字作为字面值操作符的标识符,但””和标识符之间不能包含空格:

long double operator ""int (long double); // ok
long double operator "" int (long double); // error

总结

Literal operator 在一定程度上使得代码中的常量具有更加明确的意义,如果好好使用字面值操作符会让代码更容易读懂,但如果滥用造成词不达意,也会给阅读带来麻烦。 总之,C++标准提供了这项功能,能不能用好,就看你怎么写了。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注