C++11语言扩展:常规特性



C++11语言扩展:常规特性

本文由   伯乐在线  -   christian  翻译自   isocpp。欢迎加入 技术翻译小组。转载请参见文章末尾处的要求。

本节内容:auto、decltype、基于范围的for语句、初始化列表、统一初始化语法和语义、右值引用和移动语义、Lambdas、noexcept防止抛出异常、constexpr、nullptr——一个指针空值常量、复制并再抛出异常、内联命名空间、用户自定义数据标识。

auto

推导

1
auto x = 7;

在这里因为它的初始化类型我们将得到x的int类型。一般来说,我们可以写

1
auto x = expression;

x的类型我们将会根据初始化表达式“ expression”的类型来自动推导。

当一个变量的类型很难准确的知道或者写出的时候,用atuo通过初始化表达式的类型进行推导显然是非常有用的。

参考:

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

在C++98里我们必须这样写

1
2
3
4
5
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真的很难定义,例如:

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

 

从T*U的表达式,我们人类的思维是很难理清tmp的类型的,但是编译器肯定知道T和U经过了什么特殊处理。

auto的特性在最早提出和应用时是有区别的:1984年初,Stroustrup在他的Cfront实现里用到了它,但是是由于C的兼容问题被迫拿来用的,这些兼容的问题已经在C++98和C++99接受了隐性int时候消失了。也就是说,现在这两种语言要求每一个变量和函数都要有一个明确的类型定义。auto旧的含义(即这是一个局部变量)现在是违法的。标准委员会的成员们在数百万行代码中仅仅只找到几百个用到auto关键字的地方,并且大多数出现在测试代码中,有的甚至就是一个bug。

auto在代码中主要是作为简化工具,并不会影响标准库规范。

参考:

  • the C++ draft section 7.1.6.2, 7.1.6.4, 8.3.5 (for return types)
  • [N1984=06-0054] Jaakko Jarvi, Bjarne Stroustrup, and Gabriel Dos Reis: Deducing the type of variable from its initializer expression (revision 4).
  • Herb Sutter: GotW #92: Auto Variables, Part 1
  • Herb Sutter: GotW #93: Auto Variables, Part 2
  • Herb Sutter: GotW #94: AAA Style (Almost Always Auto)

decltype

decltype(E)的类型(“声明类型”)可用名称或表达E来声明。例如:

1
2
3
4
5
6
7
8
9
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”很长一段时间,但实际使用的“领域”的实现是不完整和不兼容,所以标准版命名了decltype。

注意:喜欢使用auto时,你只是需要一个变量的类型初始化。你真的需要decltype如果你需要一个类型的东西不是一个变量,例如返回类型。

参考:

  • the C++ draft 7.1.6.2 Simple type specifiers
  • [Str02] Bjarne Stroustrup. Draft proposal for “typeof”. C++ reflector message c++std-ext-5364, October 2002. (original suggestion).
  • [N1478=03-0061] Jaakko Jarvi, Bjarne Stroustrup, Douglas Gregor, and Jeremy Siek: Decltype and auto (original proposal).
  • [N2343=07-0203] Jaakko Jarvi, Bjarne Stroustrup, and Gabriel Dos Reis: Decltype (revision 7): proposed wording.

基于范围的for循环

声明的范围像是STL-sequence定义的begin()和end(),允许你在这个范围内循环迭代。所有标准容器可以作为一个范围来使用,比如可以是std::string,初始化器列表,一个数组,和任何你可以定义begin()和end()的,比如istream。例如:

1
2
3
4
5
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中所有的元素都从begin()开始迭代循环到了end()。另一个例子:

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

begin()(和end())可以被做为x.begin()的成员或一个独立的函数被称为开始(x)。成员版本优先。

参考:

  • the C++ draft section 6.5.4 (note: changed not to use concepts)
  • [N2243==07-0103] Thorsten Ottosen: Wording for range-based for-loop (revision 2).
  • [N3257=11-0027 ] Jonathan Wakely and Bjarne Stroustrup: Range-based for statements and ADL (Option 5 was chosen).

初始化列表

推导

1
2
3
4
5
6
7
8
9
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} }
    };

初始化列表不再只针对于数组了。定义一个接受{}初始化列表的函数(通常是初始化函数)接受一个std::initializer_list < T >的参数类型,例如:

1
2
3
4
5
6
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)。

一个容器可能实现一个初始化列表构造函数如下:

1
2
3
4
5
6
7
8
9
10
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构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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。例如:

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

仅具有一个std::initializer_list的单参数构造函数被称为初始化列表构造函数。

标准库容器,string类型及正则表达式均具有初始化列表构造函数,以及(初始化列表)赋值函数等。一个初始化列表可被用作Range,例如,表达式Range。

初始化列表是一致泛化初始化解决方案的一部分。他们还防止类型收窄。一般来说,你应该通常更喜欢使用{ }来代替()初始化,除非你想用c++98编译器来分享代码或(很少)需要使用()调用没initializer_list重载构造函数。

参考:

  • the C++ draft 8.5.4 List-initialization [dcl.init.list]
  • [N1890=05-0150 ] Bjarne Stroustrup and Gabriel Dos Reis: Initialization and initializers (an overview of initialization-related problems with suggested solutions).
  • [N1919=05-0179] Bjarne Stroustrup and Gabriel Dos Reis: Initializer lists.
  • [N2215=07-0075] Bjarne Stroustrup and Gabriel Dos Reis: Initializer lists (Rev. 3).
  • [N2640=08-0150] Jason Merrill and Daveed Vandevoorde: Initializer Lists – Alternative Mechanism and Rationale (v. 2) (final proposal).

统一初始化语法和语义

c++ 98提供了几种方法初始化一个对象根据其类型和初始环境。滥用时,会产生可以令人惊讶的错误和模糊的错误消息。推导:

1
2
3
4
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

1
2
3
4
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

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

要记得初始化的规则并选择最好的方法去初始化是比较难的。

C++11的解决方法是允许所有的初始化使用初始化列表

1
2
3
4
5
6
7
8
9
10
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}在在执行代码中都创建了一个相同的值,所以在使用“{}”进行初始化合法的情况下都产生了相同的结果。例如:

1
2
3
4
5
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)

参考:

  • the C++ draft section ???
  • [N2215==07-0075 ] Bjarne Stroustrup and Gabriel Dos Reis: Initializer lists (Rev. 3) .
  • [N2640==08-0150] Jason Merrill and Daveed Vandevoorde: Initializer Lists — Alternative Mechanism and Rationale (v. 2) (final proposal).

右值引用和移动语义

左值(用在复制操作符左边)和右值(用在复制操作符右边)的区别可以追溯到Christopher Strachey (C++遥远的祖先语言CPL和外延语义之父)的时代。在C++中,非const引用可以绑定到左值,const引用既可以绑定到左值也可以绑定要右值。但是右值却不可以被非const绑定。这是为了防止人们改变那些被赋予新值之前就被销毁的临时变量。例如:

1
2
3
4
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.后者听起来很傻,但实际上确实存在这样一个bug在Fortran编译器中:为值为0的内存位置分配。

到目前为止还好,但考虑以下代码:

1
2
3
4
5
6
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是一个复制元素要付出昂贵代价的类型,比如string和vector,swap将会变成一个十分昂贵的操作(对于标准库来说,我们有专门化的string和vector来处理)。注意一下这些奇怪的现象:我们并不想任何变量拷贝。我们仅仅是想移动变量a,b和tmp的值。

在C++11中,我们可以定义“移动构造函数”和“移动赋值操作符”来移动,而不是复制他们的参数:

1
2
3
4
5
6
7
8
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

& &表明“右值引用”。一个右值引用可以绑定到一个右值(而不是一个左值):

1
2
3
4
5
6
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把s2中字符变为自己的同时删除自己原有的字符串(也可以放在s2中,但是它也面临着被销毁)

我们如何知道是否可以简单的从源对象进行移动?我们可以告诉编译器:

1
2
3
4
5
6
7
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()称做eval()也许会更好,但是现在move()已经用了好多年了。在c++11中,move()模板(参考简介)和右值引用都可以使用。

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

在C++0x的标准库中,所有的容器都提供了移动构造函数和移动赋值操作符,那些插入新元素的操作,如insert()和push_back(), 也都有了可以接受右值引用的版本。最终结果是,在无用户干预时,标准容器和算法的性能都提升了,因为复制操作的减少。

参考:

  • N1385 N1690 N1770 N1855 N1952
  • [N2027==06-0097] Howard Hinnant, Bjarne Stroustrup, and Bronek Kozicki: A brief introduction to rvalue references
  • [N1377=02-0035] Howard E. Hinnant, Peter Dimov, and Dave Abrahams: A Proposal to Add Move Semantics Support to the C++ Language (original proposal).
  • [N2118=06-0188] Howard Hinnant: A Proposal to Add an Rvalue Reference to the C++ Language Proposed Wording (Revision 3) (final proposal).

lambdas

Lambda表达式是一种描述函数对象的机制,它的主要应用是描述某些具有简单行为的函数(译注:Lambda表达式也可以称为匿名函数,具有复杂行为的函数可以采用命名函数对象,当然,简单和复杂之间的划分依赖于编程人员的选择)。例如:

1
2
3
4
5
6
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) < abs(b); }是一个"lambda"(又称为"lambda函数"或者"lambda表达式"), 它描述了这样一个函数操作:接受两个整形参数a和b,然后返回对它们的绝对值进行"<"比较的结果。(译注:为了保持与代码的一致性,此处应当为"[] (int a, int b) { return abs(a) < abs(b); }",而且在这个lambda表达式内实际上未用到局部变量,所以 [&] 是无必要的)

一个Lambda表达式可以存取在它被调用的作用域内的局部变量。例如:

1
2
3
4
5
6
7
8
9
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; });
        // ...
    }

有人认为这“相当简洁”,也有人认为这是一种可能产生危险且晦涩的代码的方式。我的看法是,两者都正确。

[&] 是一个“捕捉列表(capture list)”,用于描述将要被lambda函数以引用传参方式使用的局部变量。如果我们仅想“捕捉"参数v,则可以写为: [&v]。而如果我们想以传值方式使用参数v,则可以写为:[=v]。如果什么都不捕捉,则为:[]。将所有的变量以引用传递方式使用时采用 [&], [=] 则相应地表示以传值方式使用所有变量(译注:“所有变量”即指lambda表达式在被调用处,所能见到的所有局部变量)。

如果某一函数的行为既不通用也不简单,那么我建议采用命名函数对象或者函数。例如,如上示例可重写为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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));
        // ...
    }

对于简单的函数功能,比如记录名称域的比较,采用函数对象就略显冗长,尽管它与lambda表达式生成的代码是一致的。在C++98中,这样的函数对象在被用作模板参数时必须是非本地的(译注:即你不能在函数对象中像此处的lambda表达式那样使用被调用处的局部变量),然而在C++中(译注:意指C++0x),这不再是必须的。

为了描述一个lambda,你必须提供:

  • 它的捕捉列表:它可以使用的变量列表(除了形参之外),如果存在的话("[&]" 在上面的记录比较例子中意味着“所有的局部变量都将按照引用的方式进行传递”)。如果不需要捕捉任何变量,则使用 []。
  • (可选的)它的所有参数及其类型(例如: (int a, int b) )。
  • 组织成一个块的函数行为(例如:{ return v[a].name < v[b].name; })。
  • (可选的)采用了新的后缀返回类型符号的返回类型。但典型情况下,我们仅从return语句中去推断返回类型,如果没有返回任何值,则推断为void。

参考:

  • Standard 5.1.2 Lambda expressions
  • [N1968=06-0038] Jeremiah Willcock, Jaakko Jarvi, Doug Gregor, Bjarne Stroustrup, and Andrew Lumsdaine: Lambda expressions and closures for C++ (original proposal with a different syntax)
  • [N2550=08-0060] Jaakko Jarvi, John Freeman, and Lawrence Crowl: Lambda Expressions and Closures: Wording for Monomorphic Lambdas (Revision 4) (final proposal).
  • [N2859=09-0049] Daveed Vandevoorde: New wording for C++0x Lambdas.

noexcept防止抛出异常

如果一个函数不能抛出异常或者一个程序没有对函数抛出的异常进行处理,那么这个函数可以用关键字noexcept进行修饰,例如:

1
2
3
4
5
6
7
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();比如无法保证析构函数正常调用,不能保证栈的自动释放,也无法保证在遇到任何问题时重新启动。故意这样的使noexcept成为一种简单“粗暴”而有效的处理机制-它比旧的处理机制throw()动态抛出异常要有效的多。

它可以让一个函数根据条件来实现noexcept修饰。比如,一个算法可以根据他的模板参数来决定自己是否抛出异常。

1
2
3
4
5
6
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被用作操作符operator:如果if f(v.at(0))不能够抛出异常,noexcept(f(v.at(0)))则返回true,所以f()和at()是无法抛出异常noexcept。

noexcept()操作符是一个常量表达式,并且不计算表达式的值。

声明的通常形式是noexcept(expression),并且单独的一个“noexcept”关键字实际上就是的一个noexcept(true)的简化。一个函数的所有声明都必须与noexcept声明保持 兼容。

一个析构函数不应该抛出异常;通常,如果一个类的所有成员都拥有noexcept修饰的析构函数,那么这个类的析构函数就自动地隐式地noexcept声明,而与函数体内的代码没有关系。

通常,将某个抛出的异常进行移动操作是一个很坏的主意,所以,在任何可能的地方都用noexcept进行声明。如果某个类的所有成员都有使用noexcept声明的析构函数,那么这个类默认生成的复制或者移动操作(类的复制构造函数,移动构造函数等)都是隐式的noexcept声明。(?)

noexcept 被广泛地系统地应用在C++11的标准库中,以此来提供标准库的性能和满足标准库对于简洁性的需求。

参考:

  • Standard: 15.4 Exception specifications [except.spec].
  • Standard: 5.3.7 noexcept operator [expr.unary.noexcept].
  • [N3103==10-0093] D. Kohlbrenner, D. Svoboda, and A. Wesie: Security impact of noexcept. (Noexcept must terminate, as it does).
  • [N3167==10-0157] David Svoboda: Delete operators default to noexcept.
  • [N3204==10-0194] Jens Maurer: Deducing “noexcept” for destructors.
  • [N3050==10-0040] D. Abrahams, R. Sharoni, and D. Gregor: Allowing Move Constructors to Throw (Rev. 1).

constexpr

常量表达式机制:

  • 提供了更多的通用的值不发生变化的表达式
  • 允许用户自定义的类型成为常量表达式
  • 提供了一种保证在编译期完成初始化的方法

考虑下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
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关键字之后,可以实现这一功能,当然,它也同时会让这个变量成为常量。

1
2
3
4
5
6
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进行修饰)的对象可以做到这一点(?)

1
2
3
4
5
6
7
8
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
  •  const的主要功能是修饰一个对象而不是通过一个接口(即使对象很容易通过其他接口修改)。只不过声明一个对象常量为编译器提供了优化的机会。特别是,如果一个声明了一个对象常量而他的地址没有取到,编译器通常可以在编译时对他进行初始化(尽管这不是肯定的)保证这个对象在他的列表里而不是把它添加到生成代码里。
  • constexpr的主要功能可以在编译时计算表达式的值进行了范围扩展,这是一种计算安全而且可以用在编译时期(如初始化枚举或者整体模板参数)。constexpr声明对象可以在初始化编译的时候计算出结果来。他们基本上只保存在编译器的列表,如果需要的话会释放到生成的代码里。

参考:

  • the C++ draft 3.6.2 Initialization of non-local objects, 3.9 Types [12], 5.19 Constant expressions, 7.1.5 The constexprspecifier
  • [N1521=03-0104] Gabriel Dos Reis: Generalized Constant Expressions (original proposal).
  • [N2235=07-0095] Gabriel Dos Reis, Bjarne Stroustrup, and Jens Maurer: Generalized Constant Expressions – Revision 5.

nullptr 一个指针空值常量

nullptr是一个指针空值常量,不是一个整数。

1
2
3
4
5
6
7
8
9

你可能感兴趣的:(C++)