6 核心语言功能性的提升
这些特征允许语言去做以前没法做到的,或者做起来异常麻烦的,或者需要不可移植的库的事情。
6.1 变参模板
标准C++模板(类和函数)只能使用序列集合作为参数。而C++0x允许模板定义使用任意数量,任意类型的参数。
template<typename... Values> class tuple;
这个模板类tuple使用任意数目的类型名字作为它的参数:
class tuple<std::vector<int>, std::map<std::string, std::vector<int> > > someInstanceName;
参数的个数可以是零,所以类tuple<> someInstanceName也可以工作。
如果不希望变参模板持有零个参数,下面的定义也可以工作:
template<typename First, typename... Rest> class tuple;
变参模板也适用于函数,这就提供了类型安全的技巧,它类似于标准C的可变参函数技巧。
template<typename... Params> void printf(const std::string &strFormat, Params... parameters);
注意...运算符的使用,在函数签名里它在类型Params的右边,而不是像模板定义那样在左边。当...运算符在类型左边的时候,就像模板定义里面那样,它表示"打包"(pack)运算。它指出这种类型可以是零个或者多个。当这个运算符放在类型的右边的时候,它表示"拆包"(unpack)运算。这使得the replication of the operations done on that type, one for each of the types that were packed with the pack operator。在上面的例子里面,每一种被打包在Params里面的类型,都会给函数printf提供一个参数。
使用变参模板常常是递归的。可变参数本身无法用来实现函数或者类。所以说,定义类似C++0x变参printf的技巧如下所示:
void printf(const char *s)
{
while (*s)
{
if (*s == '%' && *(++s) != '%')
throw std::runtime_error("invalid format string: missing arguments");
std::cout << *s++;
}
}
template<typename T, typename... Args>
void printf(const char* s, T value, Args... args)
{
while (*s)
{
if (*s == '%' && *(++s) != '%')
{
std::cout << value;
printf(*s ? ++s : s, args...); // call even when *s == 0 to detect extra arguments
return;
}
std::cout << *s++;
}
throw std::logic_error("extra arguments provided to printf");
}
这就是一个递归调用。请注意printf变参模板的版本调用它自己,当args为空的时候就会调用那个简单的版本。
没有简单的办法可以轮询变参模板的各个值。然而,使用拆包操作,模板参数可以在任何地方虚拟地分离出来。
例如,一个类可以这样定义:
template <typename... BaseClasses> class ClassName : public BaseClasses...
{
public:
ClassName (BaseClasses&&... baseClasses) : BaseClasses(baseClasses)... {}
}
拆包操作可以复制ClassName的基类们的类型,使得这个类可以派生自每个传入的类型。而且,构建函数必须持有对每个基类的引用,用来初始化ClassName的基类们。
至于函数模板,可变参数可以被前向传递。当与右值引用合作的时候,它传递的非常好:
template<typename TypeToConstruct> struct SharedPtrAllocator
{
template<typename ...Args> tr1::shared_ptr<TypeToConstruct> ConstructWithSharedPtr(Args&&... params)
{
return tr1::shared_ptr<TypeToConstruct>(new TypeToConstruct(std::forward<Args>(params)...));
}
}
这个例子将参数列表拆包到TypeToConstruct的构建函数里面。std::forward<Args>(params)是一种完美地将参数作为合适的类型传递给构建函数的语法,甚至涉及到了右值。拆包操作将前向传递的语法递交给每个参数。在tr1::shared_ptr里面,为了达到关于内存泄露方面的一定程度的安全性,这种特别的工厂函数自动地将已分配的内存封装起来。
此外,打包的模板参数里面的参数数目,可以被确定出来,如下:
template<typename ...Args> struct SomeStruct
{
static const int size = sizeof...(Args);
}
语法SomeStruct<Type1, Type2>::size将是2,而SomeStruct<>::size将是0。
6.2 新的字符串文法
标准C++提供两种字符串文法。第一种,包含在双引号里面,产生const char类型的以零结尾的数组。而第二种,被定义为L"",产生const wchar_t类型的以零结尾的数组,其中wchar_t是宽字符。两种文法类型都没有提供对Unicode编码字符串文法的支持。
为了在C++编译器里加强对Unicode的支持,类型char的定义被修改为最小能存放UTF-8的八个bit的编码,而最大能包含编译器基本执行字符集的所有成员。而之前它仅仅被定义为后者(前者,译者注)。
C++0x支持三种类型的Unicode编码: UTF-8, UTF-16, 和UTF-32。除了前面所说的关于char定义的改变,C++0x还增加了两种新的字符类型:char16_t和char32_t,分别用于存放UTF-16和UTF-32。
下面就展示了如何用这些编码去创建字符串:
u8"I'm a UTF-8 string."
u"This is a UTF-16 string."
U"This is a UTF-32 string."
第一个字符串类型是普通的const char[]。第二个字符串类型是const char16_t[]。第三种则是const char32_t[]。
当创建Unicode字符文法的时候,将Unicode码值(codepoints)直接插入到字符串里面通常是很有用的。为了实现这一点,C++0x允许下面的语法:
u8"This is a Unicode Character: /u2018."
u"This is a bigger Unicode Character: /u2018."
U"This is a Unicode Character: /u2018."
在'/u'之后的数字是一个十六进制的数,它不需要通常的前缀'0x'。标识符'/u'表示这是一个16比特的Unicode码值;为了输入一个32比特的码值,可以使用'/U'和一个32位的十六进制数字。只有有效的Unicode码值才可以输入。比如,在U+D800—U+DFFF之间的码值是被禁止的,因为它们被预留用于作为UTF-16编码替代对(surrogate pairs)。
有时候,避免使用手动转义字符串也是有用的,特别是用XML或者一些脚本语言的文法时。C++0x提供一种raw string语法。
R"[The String Data / Stuff " ]"
R"delimiter[The String Data / Stuff " ]delimiter"
在第一种情况下,所有居于[ ]括号之间的都是字符串的一部分。字符 " 和 / 都不需要被转义。在第二种情况下,字符串 "delimiter[标识了字符串的开始,所以它的终结由]delimiter"来标识。字符串delimiter是任意的字符串,这就允许用户在raw string文法里面使用 ] 字符。
raw string文法也可以与宽字符文法,或者Unicode文法结合使用:
u8R"XXX[I'm a "raw UTF-8" string.]XXX"
uR"*@[This is a "raw UTF-16" string.]*@"
UR"[This is a "raw UTF-32" string.]"
6.3 用户可定义的文法
标准C++提供了一些文法。这几个字符,"12.5"就是一种,它被编译器识别为double类型,值为12.5。然而,在增加了后缀"f"之后,也就是"12.5f",就创建了类型为float的值,值为12.5。文法的后缀修饰符是C++规范里面确定的,而C++代码则无法创造新的文法修饰符。
C++0x包括一种能力,允许用户定义新类型的文法修饰符,然后就可以基于由文法修饰的字符串来构建对象。
文法的翻译被重新定义为两个不同的阶段:raw和cooked。一个raw文法是一些特定类型的字符串序列,而cooked文法则是独立类型的字符串序列。C++文法1234,作为raw文法,是字符'1', '2', '3', '4'的序列,而作为cooked文法,它表示整型1234。C++文法0xA在raw形式下是'0', 'x', 'A',而在cooked形式下就是整型10。
文法都可以在raw和cooked形式下被扩展,除了字符串文法,它只能按照cooked形式来处理。这个例外是由于字符串本身拥有前缀,可以影响字符特定的含义以及字符的类型。
所有用户定义的文法都是后缀表示,定义前缀文法是不允许的。
用户定义的处理raw形式的文法如下所示:
OutputType operator"Suffix"(const char *literal_string);
OutputType someVariable = 1234Suffix;
第二个语句执行通过用户定义文法函数对应的代码。这个函数使用C风格的字符串"1234"作为输入,所以它是以零结尾的。
另外一种处理raw文法的技巧是通过变参模板:
template<char...> OutputType operator"Suffix"();
OutputType someVariable = 1234Suffix;
它将文法处理函数作为operator"Suffix"<'1', '2', '3', '4'>来实例化。这种情况下,字符串没有结尾的零字符。这样做的主要目的是使用C++0x的constexpr关键字和编译器,进而使得文法在编译时间全部都被转换,这里假设OutputType是constexpr可构建,可拷贝的类型,并且文法处理函数也是constexpr函数。
对于cooked文法,使用了cooked文法的类型,它没有可以替换的template的形式:
OutputType operator"Suffix"(int the_value);
OutputType someVariable = 1234Suffix;
对于字符串文法,和前面所说的新的字符串前缀相一致,使用下面的形式:
OutputType operator"Suffix"(const char * string_values, size_t num_chars);
OutputType operator"Suffix"(const wchar_t * string_values, size_t num_chars);
OutputType operator"Suffix"(const char16_t * string_values, size_t num_chars);
OutputType operator"Suffix"(const char32_t * string_values, size_t num_chars);
OutputType someVariable = "1234"Suffix; //Calls the const char * version
OutputType someVariable = u8"1234"Suffix; //Calls the const char * version
OutputType someVariable = L"1234"Suffix; //Calls the const wchar_t * version
OutputType someVariable = u"1234"Suffix; //Calls the const char16_t * version
OutputType someVariable = U"1234"Suffix; //Calls the const char32_t * version
字符文法是类似的定义。
6.4 多任务内存模型
参见:内存模型(计算)http://en.wikipedia.org/wiki/Memory_model_%28computing%29
C++标准委员会计划将多线程编程标准化。
有两部分内容会涉及到:定义内存模型,允许多个线程可以在一个程序里面共存,定义线程之间交互的支持。第二个部分是通过库模块来提供的,参见"线程组件"一节。
在多个线程可以访问相同内存位置的环境里,为了进行控制,内存模型是必要的。遵循规则的程序可以确保正确执行,而破坏规则的程序则可能由于编译器的优化和内存一致性的问题而导致无法预料的行为。
6.5 线程本地存储
在多线程的环境里,每个线程必定常常有自己独特的变量。目前它可以是函数的局部变量,但无法是全局和静态变量。
下一个标准里面,提出了新的线程本地存储生存期(除了已存在的静态,动态和自动类型)。线程本地存储由存储修饰符thread_local来指示。
任何一个可以有静态存储生存期(也就是生命周期跨度了整个程序的执行)的对象,都可以被设定为线程本地生存期。目的是类似于其他静态生存期的变量,一个线程本地的对象可以用构建函数来初始化,用析构函数来销毁。
6.6 C++对象缺省函数的使能
在标准C++里面,编译器会为那些自己没有构建函数,拷贝构建函数,拷贝赋值运算符operator=以及析构函数的对象提供相应的缺省函数。如上所述,使用者可以通过定义自己的版本来重写这些缺省的函数。C++也定义了几个为所有类使用的全局的操作符(比如运算符operator=以及operator new),同样使用者也可以重写它们。
问题在于对于这些缺省函数的创建缺乏控制。比如,要使一个类不可拷贝,就需要声明一个私有的拷贝构建和拷贝赋值运算,而不实现它们。试图使用这些函数将会导致编译或者链接错误。然而这并不是一个理想的解决方案。
而且,在使用缺省构建函数的时候,有时候显式地告诉编译器去产生它也是很有用的。如果对象已经定义了自己的构建函数,编译器将不会产生缺省构建函数。尽管在许多情况下这够用了,但同时拥有特定的构建函数和编辑器产生的缺省函数也是同样有用的。
C++0x允许显式地使用,或者不使用这些标准的对象函数。比如,下面的类型显式地声明它需要缺省的构建函数:
struct SomeType
{
SomeType() = default; //The default constructor is explicitly stated.
SomeType(OtherType value);
};
另一方面,这个特征可以显式的给禁止掉。比如,下面的类型就是不可拷贝的:
struct NonCopyable
{
NonCopyable & operator=(const NonCopyable&) = delete;
NonCopyable(const NonCopyable&) = delete;
NonCopyable() = default;
};
一个类型可以禁止它使用操作符new去分配内存:
struct NonNewable
{
void *operator new(std::size_t) = delete;
};
这个对象只能作为一个栈对象,或者其他类型的成员生成。如果不使用一些不可移植的技巧,它是无法直接地被分配在堆上(因为placement new是唯一的途径可以在用户分配的内存上调用构建函数,而通过上面的方式,这种途径已经被禁止了,所以对象无法被正常地构建出来)。
修饰符= delete可以用来禁止调用任何函数,也可以用来禁止调用特定参数的成员函数,例如:
struct NoDouble
{
void f(int i);
void f(double) = delete;
};
试图用double类型去调用f()时,将会遭到编译器的拒绝,而不会隐式地转换为int类型。这种方法可以被泛化,来禁止调用除了int类型的所有函数,如下:
struct OnlyInt
{
void f(int i);
template<class T> void f(T) = delete;
};
6.7 64位整型的定义
在32位的系统上面,一个long long整型类型至少64位,非常有用。C99标准为C语言引入了这种类型,也是被大多数C++编译器已经支持的扩展(实际上,一些编译器在引入C99之前就已经支持它了)。C++将这种类型加入到标准里面。
6.8 静态断言
C++标准提供了两种方法来测试断言,一种是宏assert,以及预处理指令#error。然而两种方式都不适用于模板:宏是在执行时测试断言,而预处理指令则是在预处理时测试断言,后者发生在模板实例化之前。在测试依赖于模板参数的属性时,两种方法都不适合。
新的工具引入了新的方法,可以在编译时测试断言,这就是新的关键字static_assert。这个声明使用下面的形式:
static_assert( constant-expression, error-message ) ;
下面是一些关于使用static_assert的例子:
static_assert( 3.14 < GREEKPI && GREEKPI < 3.15, "GREEKPI is inaccurate!" ) ;
template< class T >
struct Check
{
static_assert( sizeof(int) <= sizeof(T), "T is not big enough!" ) ;
} ;
当常量表达式是false的情况下,编译器产生错误消息。第一个例子展示了对预处理指令#error的替换方式,而在第二个例子里面每次模板类Check实例化的时候断言都被检查。
静态断言在模板之外也同样有用。比如,一个算法的特定实现可能依赖于某个long long整型而不是int整型的大小,这是标准无法确保的情况。尽管这种假设在大多数系统和编译器上面是有效的,但不总是。
6.9 对类成员(而不是对象成员)使用'sizeof'
在标准C++里面,sizeof可以用在类型和对象上,但是不能用于下面的情况:
struct SomeType { OtherType member; };
sizeof(SomeType::member); //Does not work.
上面的例子应该返回OtherType的大小。C++03不允许这个,会有编译错误。但是C++0x使其成为可能。