cpp11 语言

C++11 语言扩展 — 通用特性

auto

考虑

    auto x = 7;

这里 x 的类型将是 int,因为这是其初始化器的类型。通常,我们可以这样写

    auto x = expression;

x 的类型将是“表达式”计算所得值的类型。

使用 auto 从其初始化器推导变量类型,在类型难以精确得知或难以书写时显然最为有用。考虑

    template<class T> void printall(const vector<T>& v)
    {
        for (auto p = v.begin(); p!=v.end(); ++p)
            cout << *p << "\n";
    }

在 C++98 中,我们必须写成

    template<class T> void printall(const vector<T>& v)
    {
        for (typename vector<T>::const_iterator p = v.begin(); p!=v.end(); ++p)
            cout << *p << "\n";
    }

当变量的类型关键地依赖于模板参数时,不使用 auto 编写代码会非常困难。例如

    template<class T, class U> void multiply(const vector<T>& vt, const vector<U>& vu)
    {
        // ...
        auto tmp = vt[i]*vu[i];
        // ...
    }

tmp 的类型应该是 T 乘以 U 的结果,但确切的类型对于人类读者来说可能难以理解,尽管编译器一旦确定了它正在处理的特定 TU,它当然会知道。

auto 特性有幸成为最早被提出和实现的特性:Stroustrup 在 1984 年初在他的 Cfront 实现中就使其工作,但由于 C 兼容性问题被迫将其移除。当 C++98 和 C99 接受移除“隐式 int”时,这些兼容性问题消失了;也就是说,现在两种语言都要求每个变量和函数都必须使用显式类型定义。auto 的旧含义(即“这是一个局部变量”)现在是非法的。几位委员会成员遍历了数百万行代码,只发现了少数用法——而且大多数都在测试套件中或似乎是 bug。

auto 主要是一个简化代码中符号的工具,它不影响标准库规范。

另请参见

decltype

decltype(E) 是名称或表达式 E 的类型(“声明类型”),可在声明中使用。例如

    void f(const vector<int>& a, vector<float>& b)
    {
        typedef decltype(a[0]*b[0]) Tmp;
        for (int i=0; i<b.size(); ++i) {
            Tmp* p = new Tmp(a[i]*b[i]);
            // ...
        }
        // ...
    }

这个概念在泛型编程中以“typeof”的标签流行了很长时间,但实际使用的“typeof”实现不完整且不兼容,因此标准版本命名为 decltype

注意:当你只需要为即将初始化的变量获取类型时,最好只使用 auto。如果你需要一个不是变量的东西的类型,例如 返回类型,那么你才真正需要 decltype

另请参见

  • C++ 草案 7.1.6.2 简单类型说明符
  • [Str02] Bjarne Stroustrup。《“typeof”提案草案》。C++ 反射器消息 c++std-ext-5364,2002 年 10 月。(原始建议)。
  • [N1478=03-0061] Jaakko Jarvi, Bjarne Stroustrup, Douglas Gregor, and Jeremy Siek: Decltype 和 auto(原始提案)。
  • [N2343=07-0203] Jaakko Jarvi, Bjarne Stroustrup, and Gabriel Dos Reis: Decltype(修订版 7):提议的措辞

范围-for 语句

范围 for 语句允许你遍历一个“范围”,这个范围可以是任何你可以像 STL 序列一样通过 begin()end() 定义的、可迭代的东西。所有标准容器都可以用作范围,std::string、初始化列表、数组以及任何你为其定义了 begin()end() 的东西(例如 istream)都可以。例如

void f(vector<double>& v)
{
    for (auto x : v) cout << x << '\n';
    for (auto& x : v) ++x;  // using a reference to allow us to change the value
}

你可以将其理解为“对于 v 中的所有 x”,从 v.begin() 开始迭代到 v.end()。另一个例子

    for (const auto x : { 1,2,3,5,8,13,21,34 }) cout << x << '\n';

begin()(和 end())可以是作为 x.begin() 调用的成员,也可以是作为 begin(x) 调用的独立函数。成员版本优先。

另请参见

初始化列表

考虑

    vector<double> v = { 1, 2, 3.456, 99.99 };
    list<pair<string,string>> languages = {
        {"Nygaard","Simula"}, {"Richards","BCPL"}, {"Ritchie","C"}
    }; 
    map<vector<string>,vector<int>> years = {
        { {"Maurice","Vincent", "Wilkes"},{1913, 1945, 1951, 1967, 2000} },
        { {"Martin", "Ritchards"}, {1982, 2003, 2007} }, 
        { {"David", "John", "Wheeler"}, {1927, 1947, 1951, 2004} }
    }; 

初始化列表不再仅用于数组。接受 {}-list 的机制是一个函数(通常是构造函数),接受 std::initializer_list 类型的参数。例如

    void f(initializer_list<int>);
    f({1,2});
    f({23,345,4567,56789});
    f({});  // the empty list
    f{1,2}; // error: function call ( ) missing

    years.insert({{"Bjarne","Stroustrup"},{1950, 1975, 1985}});

初始化列表的长度可以是任意的,但必须是同质的(所有元素必须是模板参数类型 T,或可转换为 T)。

容器可以像这样实现一个初始化列表构造函数

    template<class E> class vector {
    public:
        vector (std::initializer_list<E> s) // initializer-list constructor
        {
                reserve(s.size());  // get the right amount of space
                uninitialized_copy(s.begin(), s.end(), elem);   // initialize elements (in elem[0:s.size()))
            sz = s.size();  // set vector size
        }

        // ... as before ...
    };

直接初始化和复制初始化之间的区别在 {}-初始化中仍然保持,但由于 {}-初始化,其相关性降低。例如,std::vector 有一个从 int 显式构造函数和一个 initializer_list 构造函数

    vector<double> v1(7);   // ok: v1 has 7 elements
    v1 = 9;                 // error: no conversion from int to vector
    vector<double> v2 = 9;  // error: no conversion from int to vector

    void f(const vector<double>&);
    f(9);                           // error: no conversion from int to vector

    vector<double> v1{7};           // ok: v1 has 1 element (with its value 7.0)
    v1 = {9};                       // ok v1 now has 1 element (with its value 9.0)
    vector<double> v2 = {9};        // ok: v2 has 1 element (with its value 9.0)
    f({9});                         // ok: f is called with the list { 9 }

    vector<vector<double>> vs = {
        vector<double>(10),         // ok: explicit construction (10 elements)
        vector<double>{10},         // ok explicit construction (1 element with the value 10.0)
        10                          // error: vector's constructor is explicit
    };  

函数可以将 initializer_list 作为不可变序列访问。例如

    void f(initializer_list<int> args)
    {
        for (auto p=args.begin(); p!=args.end(); ++p) cout << *p << "\n";
    }

接受单个 std::initializer_list 类型参数的构造函数被称为初始化列表构造函数。

标准库容器、stringregex 具有初始化列表构造函数、赋值等。初始化列表可用作范围,例如在 范围 for 语句中。

初始化列表是 统一和通用初始化方案的一部分。它们还可以防止收窄转换。通常,除非你想与 C++98 编译器共享代码或(很少地)需要使用 () 调用非 initializer_list 重载构造函数,否则你通常应该优先使用 {} 进行初始化,而不是 ()

另请参见

统一初始化语法和语义

C++98 提供了几种初始化对象的方式,具体取决于其类型和初始化上下文。如果使用不当,错误可能会令人惊讶,错误消息也模糊不清。考虑

    string a[] = { "foo", " bar" };          // ok: initialize array variable
    vector<string> v = { "foo", " bar" };    // error: initializer list for non-aggregate vector
    void f(string a[]);
    f( { "foo", " bar" } );                  // syntax error: block as argument

    int a = 2;              // "assignment style"
    int aa[] = { 2, 3 };    // assignment style with list
    complex z(1,2);         // "functional style" initialization
    x = Ptr(y);             // "functional style" for conversion/cast/construction

    int a(1);   // variable definition
    int b();    // function declaration
    int b(foo); // variable definition or function declaration

记住初始化规则并选择最佳方式可能很困难。

C++11 的解决方案是允许 {}-初始化列表用于所有初始化

    X x1 = X{1,2}; 
    X x2 = {1,2};   // the = is optional
    X x3{1,2}; 
    X* p = new X{1,2}; 

    struct D : X {
        D(int x, int y) :X{x,y} { /* ... */ };
    };

    struct S {
        int a[3];
        S(int x, int y, int z) :a{x,y,z} { /* ... */ }; // solution to old problem
    };

重要的是,X{a} 在每个上下文中构造相同的值,因此 {}-初始化在所有合法的地方都给出相同的结果。例如

    X x{a}; 
    X* p = new X{a};
    z = X{a};         // use as cast
    f({a});           // function argument (of type X)
    return {a};       // function return value (function returning X)

C++11 的统一初始化并非完美统一,但已非常接近。C++11 的 {} 初始化语法和语义提供了一种更简单、更一致的初始化方式,它也更强大(例如,vector v = { 1, 2, 3, 4 })并且更安全(例如,{} 不允许窄化转换)。建议优先使用 {} 进行初始化。

另请参见

右值引用和移动语义

左值(可在赋值的左侧使用)和右值(可在赋值的右侧使用)之间的区别可以追溯到 Christopher Strachey(C++ 远祖 CPL 和指示语义之父)。在 C++98 中,对非 const 的引用可以绑定到左值,对 const 的引用可以绑定到左值或右值,但没有可以绑定到非 const 右值的东西。这是为了保护人们免于更改在它们的新值被使用之前就被销毁的临时变量的值。例如

    void incr(int& a) { ++a; }
    int i = 0;
    incr(i);    // i becomes 1
    incr(0);    // error: 0 in not an lvalue

如果允许 incr(0),那么要么一些从未被任何人看到的临时变量会递增,要么——更糟糕的是——0 的值会变成 1。后者听起来很傻,但早期的 Fortran 编译器中确实存在一个这样的 bug,它们会为值 0 留出一个内存位置。

到目前为止一切顺利,但是考虑一下

    template<class T> swap(T& a, T& b)      // "old style swap"
    {
        T tmp(a);   // now we have two copies of a
        a = b;      // now we have two copies of b
        b = tmp;    // now we have two copies of tmp (aka a)
    } 

如果 T 是一种复制元素可能很昂贵的类型,例如 stringvector,那么 swap 就会成为一个昂贵的操作(对于标准库,我们有 stringvectorswap() 特化来处理这个问题)。注意一些奇怪的事情:我们根本不想要任何复制。我们只是想把 abtmp 的值稍微移动一下。

在 C++11 中,我们可以定义“移动构造函数”和“移动赋值”,以移动而非复制其参数

    template<class T> class vector {
        // ...
        vector(const vector&);          // copy constructor
        vector(vector&&);           // move constructor
        vector& operator=(const vector&);   // copy assignment
        vector& operator=(vector&&);        // move assignment
    };  // note: move constructor and move assignment takes non-const &&
        // they can, and usually do, write to their argument

&& 表示“右值引用”。右值引用可以绑定到右值(但不能绑定到左值)

    X a;
    X f();
    X& r1 = a;      // bind r1 to a (an lvalue)
    X& r2 = f();        // error: f() is an rvalue; can't bind

    X&& rr1 = f();  // fine: bind rr1 to temporary
    X&& rr2 = a;    // error: bind a is an lvalue

移动赋值的理念是,它不是制作副本,而是简单地从其源获取表示,并将其替换为廉价的默认值。例如,对于 string,使用移动赋值的 s1=s2 不会复制 s2 的字符;相反,它只会让 s1 将这些字符视为自己的,并以某种方式删除 s1 的旧字符(也许通过将它们留在 s2 中,而 s2 大概即将被销毁)。

我们如何知道是否可以直接从源移动?我们告诉编译器:

    template<class T> 
    void swap(T& a, T& b)   // "perfect swap" (almost)
    {
        T tmp = move(a);    // could invalidate a
        a = move(b);        // could invalidate b
        b = move(tmp);      // could invalidate tmp
    }

move(x) 只是一个转换,表示“你可以将 x 视为右值”。也许如果 move() 被称为 rval() 会更好,但现在 move() 已经使用了多年。move() 模板函数可以用 C++11 编写(参见“简要介绍”),并且使用右值引用。

右值引用也可以用于提供完美转发。

在 C++11 标准库中,所有容器都提供了移动构造函数和移动赋值,并且插入新元素的操作(例如 insert()push_back())都有接受右值引用的版本。最终结果是,标准容器和算法会在不进行用户干预的情况下悄悄地提高性能,因为它们减少了复制。

另请参见

Lambdas

lambda 表达式是一种指定函数对象的机制。lambda 的主要用途是指定要由某个函数执行的简单操作。例如

    vector<int> v = {50, -10, 20, -30};

    std::sort(v.begin(), v.end());  // the default sort
    // now v should be { -30, -10, 20, 50 }

    // sort by absolute value:
    std::sort(v.begin(), v.end(), [](int a, int b) { return abs(a)<abs(b); });
    // now v should be { -10, 20, -30, 50 }

参数 [](int a, int b) { return abs(a) 是一个“lambda”(或“lambda 函数”或“lambda 表达式”),它指定了一个操作,给定两个整数参数 ab,返回比较它们绝对值的结果。

lambda 表达式可以访问其使用范围内的局部变量。例如

    void f(vector<Record>& v)
    {
        vector<int> indices(v.size());
        int count = 0;
        generate(indices.begin(),indices.end(),[&count](){ return count++; });

        // sort indices in the order determined by the name field of the records:
        std::sort(indices.begin(), indices.end(), [&](int a, int b) { return v[a].name<v[b].name; });
        // ...
    }

有些人觉得这“非常棒!”,另一些人则认为这是编写危险晦涩代码的方式。两者都对。

[&] 是一个“捕获列表”,指定使用的局部名称将按引用传递。如果只希望“捕获” v,可以这样说:[&v]。如果希望按值传递 v,可以这样说:[=v][v]。不捕获任何东西是 [],按引用捕获所有是 [&],按值捕获所有是 [=]

如果某个操作既不常见也不简单,请考虑使用具名函数对象或函数。例如,上面的示例可以写成

    void f(vector<Record>& v)
    {
        vector<int> indices(v.size());
        int count = 0;
        generate(indices.begin(),indices.end(),[&](){ return ++count; });

        struct Cmp_names {
            const vector<Record>& vr;
            Cmp_names(const vector<Record>& r) :vr(r) { }
            bool operator()(int a, int b) const { return vr[a].name<vr[b].name; }
        };

        // sort indices in the order determined by the name field of the records:
        std::sort(indices.begin(), indices.end(), Cmp_names(v));
        // ...
    }

对于像这个 Record 名称字段比较这样的小函数,函数对象表示法比较冗长,尽管生成的代码可能相同。在 C++98 中,此类函数对象必须是非局部的才能用作模板参数;在 C++ 中,这不再是必需的

要指定 lambda,您必须提供

  • 其捕获列表:它可以使用的变量列表(除了其参数),如果有的话(在记录比较示例中,[&] 表示“所有局部变量按引用传递”)。如果不需要捕获任何名称,lambda 以纯 [] 开头。
  • (可选)其参数及其类型(例如,(int a, int b)
  • 要执行的操作作为一个块(例如,{ return v[a].name)。
  • (可选)使用新的后缀返回类型语法指定返回类型;但通常我们只需从 return 语句中推断返回类型。如果未返回值,则推断为 void

另请参见

noexcept 以防止异常传播

如果一个函数不能抛出异常,或者程序没有编写来处理函数抛出的异常,那么该函数可以声明为 noexcept。例如

    extern "C" double sqrt(double) noexcept;    // will never throw

    vector<double> my_computation(const vector<double>& v) noexcept // I'm not prepared to handle memory exhaustion
    {
        vector<double> res(v.size());   // might throw
        for(int i; i<v.size(); ++i) res[i] = sqrt(v[i]);
        return res;
    }

如果声明为 noexcept 的函数抛出异常(以便异常尝试逃离 noexcept 函数),程序将通过调用 std::terminate() 终止。调用 terminate() 不能依赖于对象处于良好定义的 estados;也就是说,不能保证析构函数已被调用,没有保证的堆栈展开,也没有可能恢复程序,如同没有遇到问题一样。这是故意的,并使 noexcept 成为一个简单、粗暴且非常高效的机制——比旧的动态 throw() 异常规范机制效率高得多。

可以使函数有条件地 noexcept。例如,一个算法可以指定为 noexcept,当且仅当它对模板参数使用的操作是 noexcept

    template<class T>
    void do_f(vector<T>& v) noexcept(noexcept(f(v.at(0)))) // can throw if f(v.at(0)) can
    {
        for(int i; i<v.size(); ++i)
            v.at(i) = f(v.at(i));
    }

这里,我们首先将 noexcept 用作运算符:noexcept(f(v.at(0))) 为真,如果 f(v.at(0)) 不能抛出,也就是说,如果所使用的 f()at()noexcept 的。

noexcept() 运算符是一个常量表达式,不评估其操作数。

noexcept 声明的一般形式是 noexcept(expression),“纯 noexcept”只是 noexcept(true) 的简写。函数的所有声明都必须具有兼容的 noexcept 规范。

析构函数不应该抛出;如果类的所有成员都有 noexcept 析构函数(默认情况下,它们也会有),那么生成的析构函数是隐式 noexcept 的(与其主体中的代码无关)。

让移动操作抛出通常不是一个好主意,因此尽可能将它们声明为 noexcept。如果类的成员使用的所有复制或移动操作都具有 noexcept 析构函数,则生成的复制或移动操作隐式为 noexcept

noexcept 在标准库中被广泛且系统地使用,以提高性能并明确要求。

另请参见

constexpr

constexpr 机制

  • 提供更通用的常量表达式
  • 允许涉及用户定义类型的常量表达式
  • 提供了一种在编译时保证初始化的方法

考虑

    enum Flags { good=0, fail=1, bad=2, eof=4 };

    constexpr int operator|(Flags f1, Flags f2) { return Flags(int(f1)|int(f2)); }

    void f(Flags x)
    {
        switch (x) {
        case bad:         /* ... */ break;
        case eof:         /* ... */ break;
        case bad|eof:     /* ... */ break;
        default:          /* ... */ break;
        }
    }

这里 constexpr 表示函数必须是简单形式,以便在给定常量表达式参数时能够在编译时进行评估。

除了能够在编译时评估表达式之外,我们还希望能够要求在编译时评估表达式;变量定义前面的 constexpr 可以实现这一点(并隐含 const

    constexpr int x1 = bad|eof; // ok

    void f(Flags f3)
    {
        constexpr int x2 = bad|f3;  // error: can't evaluate at compile time
        int x3 = bad|f3;        // ok
    }

通常我们希望对全局或命名空间对象进行编译时评估保证,通常是为了将对象放置在只读存储中。

这也适用于其构造函数足够简单以至于可以为 constexpr 的对象以及涉及此类对象的表达式

    struct Point {
        int x,y;
        constexpr Point(int xx, int yy) : x(xx), y(yy) { }
    };

    constexpr Point origo(0,0);
    constexpr int z = origo.x;

    constexpr Point a[] = {Point(0,0), Point(1,1), Point(2,2) };
    constexpr int x = a[1].x;   // x becomes 1

请注意 constexpr 不是 const 的通用替代品(反之亦然)

  • const 的主要功能是表达一个对象不会通过接口被修改(尽管该对象很可能通过其他接口被修改)。碰巧声明一个对象为 const 为编译器提供了极好的优化机会。特别是,如果一个对象被声明为 const 并且没有取其地址,编译器通常能够在编译时评估其初始化器(尽管不能保证),并将其保存在其表中,而不是将其发出到生成的代码中。
  • constexpr 的主要功能是扩展编译时可计算的范围,使此类计算类型安全,并且也可在编译时上下文中使用(例如,初始化枚举器或整型模板参数)。声明为 constexpr 的对象其初始化器在编译时求值;它们基本上是保存在编译器表格中的值,仅在需要时才发出到生成的代码中。

另请参见

  • C++ 草案 3.6.2 非局部对象的初始化,3.9 类型 [12],5.19 常量表达式,7.1.5 constexpr 说明符
  • [N1521=03-0104] Gabriel Dos Reis: 通用常量表达式(原始提案)。
  • [N2235=07-0095] Gabriel Dos Reis, Bjarne Stroustrup, and Jens Maurer: 通用常量表达式 – 修订版 5

nullptr – 空指针字面量

nullptr 是一个表示空指针的字面量;它不是一个整数

    char* p = nullptr;
    int* q = nullptr;
    char* p2 = 0;           // 0 still works and p==p2

    void f(int);
    void f(char*);

    f(0);                   // call f(int)
    f(nullptr);             // call f(char*)

    void g(int);
    g(nullptr);             // error: nullptr is not an int
    int i = nullptr;        // error: nullptr is not an int

另请参见

复制和重新抛出异常

如何捕获异常然后在另一个线程上重新抛出它?使用标准 18.8.5 异常传播中描述的库魔法

  • exception_ptr current_exception(); 返回:一个 exception_ptr 对象,它引用当前处理的异常 (15.3) 或当前处理的异常的副本,或者一个空 exception_ptr 对象,如果当前没有异常正在处理。被引用的对象应保持有效,至少只要有一个 exception_ptr 对象引用它。…
  • void rethrow_exception(exception_ptr p);
  • template exception_ptr copy_exception(E e); 效果:如同
    try {
        throw e;
    } catch(...) {
        return current_exception();
    }

这对于将异常从一个线程传输到另一个线程特别有用。

内联命名空间

inline namespace 机制旨在通过提供一种支持版本控制形式的机制来支持库演进。考虑

    // file V99.h:
    inline namespace V99 {
        void f(int);    // does something better than the V98 version
        void f(double); // new feature
        // ...
    }

    // file V98.h:
    namespace V98 {
        void f(int);    // does something
        // ...
    }

    // file Mine.h:
    namespace Mine {
    #include "V99.h"
    #include "V98.h"
    }

我们这里有一个命名空间 Mine,包含最新版本(V99)和前一个版本(V98)。如果你想具体指定,可以

    #include "Mine.h"
    using namespace Mine;
    // ...
    V98::f(1);  // old version
    V99::f(1);  // new version
    f(1);       // default version

关键在于 inline 指定符使嵌套命名空间中的声明看起来就像它们已在封闭命名空间中声明一样。

这是一个非常“静态”和面向实现者的设施,因为 inline 指定符必须由命名空间的设计者放置——从而为所有用户做出选择。Mine 的用户不可能说“我希望默认是 V98 而不是 V99。”

参见

  • 标准 7.3.1 命名空间定义 [7]-[9]。

用户定义字面量

C++ 总是为各种内置类型提供字面量(2.14 字面量)

    123 // int
    1.2 // double
    1.2F    // float
    'a' // char
    1ULL    // unsigned long long
    0xD0    // hexadecimal unsigned
    "as"    // string

然而,在 C++98 中,没有用户定义类型的字面量。这可能会带来麻烦,也被视为违反了用户定义类型应与内置类型一样受到良好支持的原则。特别是,人们曾要求

    "Hi!"s          // std::string, not ``zero-terminated array of char''
    1.2i            // imaginary
    123.4567891234df    // decimal floating point (IBM)
    101010111000101b    // binary
    123s            // seconds
    123.56km        // not miles! (units)
    1234567890123456789012345678901234567890x   // extended-precision

C++11 通过字面量运算符的概念支持“用户定义字面量”,这些运算符将带有给定后缀的字面量映射到所需的类型。例如

    constexpr complex<double> operator "" i(long double d)  // imaginary literal
    {
        return {0,d};   // complex is a literal type
    }

    std::string operator""s (const char* p, size_t n)   // std::string literal
    {
        return string(p,n); // requires free store allocation
    }

注意 constexpr 的使用,以启用编译时评估。有了这些,我们可以编写

    template<class T> void f(const T&);
    f("Hello"); // pass pointer to char*
    f("Hello"s);    // pass (5-character) string object
    f("Hello\n"s);  // pass (6-character) string object

    auto z = 2+1i;  // complex(2,1)

基本的(实现)思想是,在解析可能是一个字面量之后,编译器总是检查后缀。用户定义字面量机制只是允许用户指定一个新的后缀,以及在字面量之前要对它做什么。不可能重新定义内置字面量后缀的含义或扩充字面量的语法。字面量运算符可以请求将其(前面的)字面量以“处理过的”(如果未定义新后缀,它将具有的值)或“未处理过的”(作为字符串)形式传递。

要获取“未经处理”的字符串,只需请求一个 const char* 参数

    Bignum operator"" x(const char* p)
    {
        return Bignum(p);
    }

    void f(Bignum);
    f(1234567890123456789012345678901234567890x);

这里,C 风格字符串 "1234567890123456789012345678901234567890" 被传递给 operator"" x()。请注意,我们没有显式地将这些数字放入字符串中。

有四种字面量可以添加后缀以创建用户定义的字面量。

  • 整数字面量:由接受单个 unsigned long longconst char* 参数的字面量运算符接受。
  • 浮点字面量:由接受单个 long doubleconst char* 参数的字面量运算符接受。
  • 字符串字面量:由接受一对 (const char*, size_t) 参数的字面量运算符接受。
  • 字符字面量:由接受单个 char 参数的字面量运算符接受。

请注意,您不能为只接受 const char* 参数(不带大小)的字符串字面量创建字面量运算符。例如

    string operator"" S(const char* p);     // warning: this will not work as expected

    "one two"S; // error: no applicable literal operator

理由是,如果我们想要“不同类型的字符串”,我们几乎总是无论如何都想知道字符的数量。

后缀通常会很短(例如 s 代表 stringi 代表虚数,m 代表米,x 代表扩展),因此不同的用途很容易发生冲突。使用命名空间来防止冲突

    namespace Numerics { 
        // ...
        class Bignum { /* ... */ }; 
        namespace literals { 
            operator"" X(char const*); 
        } 
    } 

    using namespace Numerics::literals; 

另请参见