您的位置:首页 > 编程语言 > C语言/C++

新C++标准:C++0x教程(二):面向所有开发者的特性(上)

2012-11-13 13:37 405 查看
译者:yurunsun@gmail.com
新浪微博@孙雨润 新浪博客
CSDN博客日期:2012年11月12日

原作:Scott Meyers

这些是Scott Meyers培训教程《新C++标准:C++0x教程》的官方笔记,培训课程的描述请见 http://www.aristeia.com/C++0x.html,版权信息请见 http://aristeia.com/Licensing/licensing.html.

漏洞和建议请发送邮件到 smeyers@aristeia.com. 翻译错误请发送邮件给yurunsun@gmail.com (译者注).

1. ">>" 作为嵌套模板的结尾

当可能时,">>"作为嵌套模板的结尾:

std::vector<std::list<int>> vi1;        // fine in C++0x, error in C++98

在C++98中需要以空格分隔:

std::vector<std::list<int> > vi2;       // fine in C++0x and C++98

衍生的语义推导变化:

const int n = … ;                       // n, m 是编译时常量
cosnt int m = … ;
std::array<int, n>m?n:m > a1;           // error (as in C++98)
std::array<int, (n>m?n:m) > a2;         // fine (as in C++98)
std::list<std::array<int, n>>2 >> L1;   // error in ’98: 2 shifts; error in ’0x: 1st “>>” closes both templates
std::list<std::array<int, (n>>2) >> L2; // fine in C++0x, error in ’98 (2 shifts)

2. auto 类型声明

2.1
auto
修饰的变量具有它们初始化表达式的类型

auto x1 = 10;                       // x1: int
std::map<int, std::string> m;
auto i1 = m.begin();                // i1: std::map<int, std::string>::iterator

const/volatile和引用/指针修饰符都可以添加在
auto


const auto *x2 = &x1;               // x2: const int*
const auto& i2 = m;                 // i2: const std::map<int, std::string>&

对于没有显示声明为引用的变量:初始化类型中的顶级
const/volatile
会被忽略;初始化类型中的数组和函数名会退化成指针

const std::list<int> li;
auto v1 = li;                       // v1: std::list<int>
auto& v2 = li;                      // v2: const std::list<int>&
float data[BufSize];
auto v3 = data;                     // v3: float*
auto& v4 = data;                    // v4: float (&)[BufSize]

先前一段例子:

auto x1 = 10;                       // x1: int
std::map<int, std::string> m;
auto i1 = m.begin();                // i1: std::map<int, std::string>::iteratorconst auto *x2 = &x1;               // x2: const int* (const isn’t top-level)
const auto& i2 = m; // i2: const std::map<int, std::string>&
auto ci = m.cbegin(); // ci: std::map<int, std::string>::const_iterator

新的容器函数
cbegin/cend/crbegin/crend
表示
const_iterator


auto ci = m.cbegin();               // ci: std::map<int, std::string>::const_iterator

2.2
auto
具有和模板相似的推导能力

template<typename T> void f(T t);
…
f(expr);                            // 从expr推导T的类型
auto v = expr;                      // 实质上做了类似的事情

除了对于新特性“花括号初始列表”,模板无法推导而
auto
能推导。“与模板具有相似的推导能力”意味着右值引用会退化成左值引用

int x;
auto&& a1 = x;                      // x 是左值, 所以a1的类型是int&
auto&& a2 = std::move(x);           // std::move(x) 是右值, 所以a2的类型是int&&

这在后续将详细讨论。

2.3
auto
可以同时声明多个相同类型变量

void f(std::string& s)
{
auto temp = s, *pOrig = &s;     // temp: std::string, pOrig: std::string*
}

再强调一遍,必须推导出相同类型的变量:

auto i = 10, d = 5.0;               // error!

2.4 直接初始化和赋值拷贝意义相同

auto v1(expr);                      // 直接初始化
auto v2 = expr;                     // 赋值拷贝

构造函数是否声明为
explicit
并不影响直接初始化,因为推导类型时不涉及类型转换;但如果拷贝构造函数声明为
explicit
,会影响对
auto
变量的赋值拷贝:

struct Explicit {
Explicit(){}
explicit Explicit(const Explicit&){}
} ex;
auto ex2 = ex;                      // Error
auto ex3(ex);                       // OK

3. 基于范围的
for
循环

3.1 遍历容器的新方法

std::vector<int> v;
…
for (int i : v) std::cout << i;     // 将v中每一个元素赋值给i

对比C++0x

for ( iterVarDeclaration : expression ) statementToExecute

相当于

{
auto&& range = expression;
for (auto b = begin(range), e = end(range); b != e; ++b ) {
iterVarDeclaration = *b;
statementToExecute
}
}

3.2 迭代的变量可以是
reference/auto/const/volatile

for (int& i : v) std::cout << ++i;  // 将v中每一个元素递增
for (auto i : v) std::cout << i;    // 同上
for (auto& i : v) std::cout << ++i; // 同上
for (volatile int i : v) someOtherFunc(i); // 甚至 "volatile auto i"

3.3 适用对象

如果对于
T obj
begin(obj)
end(obj)
合法,那么基于范围的
for
循环也适用。包括所有的C++0x容器、数组和
valarray
、初始化lists、正则表达式、任何能提供合适的迭代器的用户定义类型。

std::unordered_multiset<std::shared_ptr<Widget>> msspw;
for (const auto& p : msspw) {
std::cout << p << '\n'; // print pointer value
}
short vals[ArraySize];
for (auto& v : vals) { v = -v; }

注意变量
auto& p
使用了引用的方式,避免对
shared_ptr
造成不必要的引用计数的操作。

【Note】新的基于范围的
for
循环,不适用于
while
do...while
循环,也就是后两者没有这种新语义

4.
nullptr

4.1
nullptr
是没有二义性的指针

新的关键词,专指空指针。
nullptr
的类型是
std::nullptr
,其他指针类型可以使用
static_cast
或者C风格类型转换转成
nullptr
,结果永远是空指针。

void f(int *ptr);               // 重载 ptr and int
void f(int val);
f(nullptr);                     // calls f(int*)
f(0);                           // calls f(int)
f(NULL);                        // 有可能 calls f(int),有可能报二义性错误

4.2 仅能够类型转换成其他指针类型和
bool

const char *p = nullptr;        // p is null
if (p) …                        // 编译通过,被转成false值
int i = nullptr;                // 编译报错

以前
NULL
0
的使用方法保持兼容

int *p1 = nullptr;              // p1 is null
int *p2 = 0;                    // p2 is null
int *p3 = NULL;                 // p3 is null
if (p1 == p2 && p1 == p3) …     // 编译通过,expression值为true

4.3
nullptr
可以用于forwarding模板

这一点与
0, NULL
不同:

template<typename F, typename P>    // 调用func,传入param参数
void Call(F func, P param) {
func(param);
}
void f(int* p);                     // some function to call
f(0);                               // fine
f(nullptr);                         // also fine
logAndCall(f, 0);                   // error! P 被推导成int, f(int) 非法
logAndCall(f, NULL);                // error!
logAndCall(f, nullptr);             // fine, P 被推导成 std::nullptr_t, f(std::nullptr_t) is okay

5. Unicode支持

5.1 两种新的字符类型

char16_t                            // 16-bit character (if available) 和 uint_least16_t 类似
char32_t                            // 32-bit character (if available) 和 uint_least32_t 类似

在字符前使用前缀,标识字符类型

u'x'                                // 'x' as a char16_t using UCS-2
U'x'                                // 'x' as a char32_t using UCS-4/UTF-32

C++98的语法仍然可用

'x'                                 // 'x' as a char
L'x'                                // 'x' as a wchar_t

5.2 相应的字符串表示法

u"UCS-2 string literal"             // ⇒ char16_ts in UTF-16
U"UCS-4 string literal"             // ⇒ char32_ts in UCS-4/UTF-32
"Ordinary/narrow string literal"    // "ordinary/narrow" ⇒ chars
L"Wide string literal"              // "wide" ⇒ wchar_ts
u8"UTF-8 string literal"            // ⇒ chars in UTF-8

stl
中相应的
string


std::string s1;                     // std::basic_string<char>
std::wstring s2;                    // std::basic_string<wchar_t>
std::u16string s3;                  // std::basic_string<char16_t>
std::u32string s4;                  // std::basic_string<char32_t>

5.3 字符编码转换

std::codecvt
在C++98中能够使
wchar_t
char
互转:

std::codecvt<wchar_t, char, std::mbstate_t>

在C++0x中新增了如下功能:

UTF-16 ⇄ UTF-8 (std::codecvt<char16_t, char, std::mbstate_t>)
UTF-32 ⇄ UTF-8 (std::codecvt<char32_t, char, std::mbstate_t>)
UTF-8 ⇄ UCS-2, UTF-8 ⇄ UCS-4 (std::codecvt_utf8)
UTF-16 ⇄ UCS-2, UTF-16 ⇄ UCS-4 (std::codecvt_utf16)
// Behaves like std::codecvt<char16_t, char, std::mbstate_t>.
UTF-8 ⇄ UTF-16 (std::codecvt_utf8_utf16)

5.4 支持Raw String

对特殊字符\"/等不需要再手动escape:

std::string noNewlines(R"(\n\n)");
std::string cmd(R"(ls /home/docs | grep ".pdf")");
std::string withNewlines(R"(Line 1 of the string...
Line 2...
Line 3)");

R可以与任意字符编码:

LR"(Raw Wide string literal \t (without a tab))"
u8R"(Raw UTF-8 string literal \n (without a newline))"
uR"(Raw UTF-16 string literal \\ (with two backslashes))"
UR"(Raw UTF-32 string literal \u2620 (without a code point))"

需要注意的是
R
必须放在表示字符编码的字母后边。

5.5 自定义Raw String的定界符

// "operator()"|"operator->"
std::regex re1(R"!("operator\(\)"|"operator->")!");
// "(identifier)"
std::regex re2(R"xyzzy("\([A-Za-z_]\w*\)")xyzzy");

默认的字符串
R"(XXXX)";
里的左右括号之间的部分;如果在
"(
之间插入一个不超过16个字符不含空格的字符串,则会以这段字符串为界,如re2的
xyzzy


6. 统一的初始化语法

注意初始化不等于赋值,例如const对象不能被赋值但是可以被初始化

6.1 C++98中有多种初始化方式

const int y(5);                     // “direct initialization” syntax
const int x = 5;                    // “copy initialization” syntax
int arr[] = { 5, 10, 15 };          // brace initialization
struct Point1 { int x, y; };
const Point1 p1 = { 10, 20 };       // brace initializtion
class Point2 {
public:
Point2(int x, int y);
};
const Point2 p2(10, 20);            // function call syntax

容器的初始化需要另一个容器:

int vals[] = { 10, 20, 30 };
const std::vector<int> cv(vals, vals+3); // init from another container

成员变量和堆上数组无法初始化:

class Widget {
public:
Widget(): data(???) {}
private:
const int data[5];                      // not initializable
};
const float * pData = new const float[4];   // not initializable

6.2 C++0x使用
{}
作为统一初始化方式

{}
初始化可以用在所有地方:

const int val1 {5};
const int val2 {5};
int a[] { 1, 2, val1, val1+val2 };
struct Point1 { … };                        // as before
const Point1 p1 {10, 20};
class Point2 { … };                         // as before
const Point2 p2 {10, 20};                   // calls Point2 ctor
const std::vector<int> cv { a[0], 20, val2 };
class Widget {
public:
Widget(): data {1, 2, a[3], 4, 5} {}
private:
const int data[5];
};
const float * pData = new const float[4] { 1.5, val1-val2, 3.5, 4.5 };

当通过花括号
{}
初始化成员变量时,花括号可以包含在小括号
()
中:

Widget(): data({1, 2, a[3], 4, 5}) {}

一些以前不敢想象的方式:

Point2 makePoint() { return { 0, 0 }; }     // return expression; calls Point2 ctor
void f(const std::vector<int>& v);          // func. declaration
f({ val1, val2, 10, 20, 30 });              // function argument

6.3 统一初始化语法中的不同语义

聚合类型,例如array和struct:从头到尾初始化元素
非聚合类型:调用构造函数。

严格说来聚合类型的定义比上边描述的稍微复杂一些,标准的定义是:“聚合是一个数组或者这样的类:没有用户提供的构造函数,没有非静态成员的默认初始化函数,没有private或者protected的数据成员,没有基类,没有虚函数。”

6.4 聚合类型的初始化

6.4.1
union

统一初始化语法可以用在
union
上,但是只有
union
的第一个成员会被初始化

union u { int a; char* b; };
u a = { 1 };                            // okay
u d = { 0, "asdf" };                    // error
u e = { "asdf" };                       // error (can’t initialize an int with a char array)

6.4.2 元素个数不匹配时

如果初始化元素数量超过容器大小,则编译报错,如果小于容器大小,剩下的对象进行"值初始化":内置类型初始为0、自定义类型调用构造函数、没有构造函数则对自定义类型的成员进行递归的值初始化

struct Point1 { int x, y; };            // as before
const Point1 p1 = { 10 };               // same as { 10, 0 }
const Point1 p2 = { 1, 2, 3 };          // error! too many initializers
long f();
std::array<long, 3> arr = { 1, 2, f(), 4, 5 }; // error! too many initializers

6.4.3 C++98中
.x
的初始化方式被取消

struct Point {
int x, y, z;
};
Point p { .x = 5, .z = 8 }; // error!

6.5 非聚合类型的初始化

6.5.1 间接调用构造函数

class Point2 {                          // as before
public:
Point2(int x, int y);
};
short a, b;
const Point2 p1 {a, b};                 // same as p1(a, b)
const Point2 p2 {10};                   // error! too few ctor args
const Point2 p3 {5, 10, 20};            // error! too many ctor args

对容器也一样有效,注意和聚合类型初始化的区别

std::vector<int> v { 1, a, 2, b, 3 };   // calls a vector ctor
std::unordered_set<float> s { 0, 1.5, 3 }; // calls an unordered_set ctor

6.5.2 使用
= {}
进行赋值

大部分是OK的:

const int val1 = {5};
const int val2 = {5};
int a[] = { 1, 2, val1, val1+val2 };
struct Point1 { … };
const Point1 p1 = {10, 20};
class Point2 { … };
const Point2 p2 = {10, 20};
const std::vector<int> cv = { a[0], 20, val2 };

下面是非法的情况:

class Widget {
public:
Widget(): data = {1, 2, a[3], 4, 5} {}  // error!
private:
const int data[5];
};
const float * pData = new const float[4] = { 1.5, val1-val2, 3.5, 4.5 }; // error!
Point2 makePoint() { return = { 0, 0 }; }   // error!
void f(const std::vector<int>& v);          // as before
f( = { val1, val2, 10, 20, 30 });           // error!

注意这种语法无法调用以
explicit
声明的构造函数:

class Widget {
public:
explicit Widget(int);
};
Widget w1(10);                              // okay, direct init: explicit ctor callable
Widget w2{10};                              // 同上
Widget w3 = 10;                             // error! copy init: explicit ctor not callable
Widget w4 = {10};                           // 同上

因此推荐养成不适用
= {}
而只是用
{}
的习惯

6.6
{}
初始化方式禁止隐式有损转换

所谓有损转换是指:目标类型无法表示源类型的所有值,或者编译器不能保证源值会在目标类型能表达的范围之内。C++98允许赋值时隐式有损转换,在C++0x总会编译报错:

struct Point { int x, y; };
Point p1 { 1, 2.5 };    // fine in C++98: implicit double ⇒ int conversion; error in C++0x
Point p2 { 1, static_cast<int>(2.5) }; // fine in both C++98 and C++0x

这会导致:直接使用构造函数,与使用
{}
初始化、由编译器间接调用构造函数,两种方法有些细微差别:

class Widget {
public:
Widget(unsigned u);
};
int i;
Widget w1(i);           // okay, implicit int ⇒ unsigned
Widget w2 {i};          // error! int ⇒ unsigned narrows
unsigned u;
Widget w3(u);           // fine
Widget w4 {u};          // also fine, same as w3’s init.

6.7 初始化list

6.7.1 使用方法

初始化list与聚合类型初始化相似

int x, y;
int a[] { x, y, 7, 22, -13, 44 };           // 数组
std::vector<int> v { 99, -8, x-y, x*x };    // std. library type
Widget w { a[0]+a[1], x, 25, 16 };          // 自定义类型

但不仅有初始化的功能:

std::vector<int> v {}; // initialization
v.insert(v.end(), { 99, 88, -1, 15 });      // 一次插入多个元素
v = { 0, 1, x, y };                         // repace的动作

任何函数都可以使用初始化list作为参数。

6.7.2 实现原理

{}
转换成
std::initializer_list
对象
函数可以以
std::initializer_list
作为参数
std::initializer_list
存储初始元素的值,并提供几个函数:
size()/begin()/end()


注意
initializer_list
与其他容器不同,没有
rbegin/rend/cbegin/cend/crbegin/crend
几个迭代游标。

标准库中
initializer_list
对象永远按值传递。

6.7.3 示例

Case1:

#include <initializer_list>                 // necessary header
std::u16string getName(int ID);             // lookup name with given ID
class Widget {
public:
Widget(std::initializer_list<int> nameIDs){
names.reserve(nameIDs.size());
for (auto id: nameIDs) names.push_back(getName(id));
}
private:
std::vector<std::u16string> names;
};
...
// copies values into an array wrapped by an initializer_list  passed to the Widget ctor.
Widget w { a[0]+a[1], x, 25, 16 };

这个例子表达的意思是:
Widget
对象在构造过程中,将初始化列表中的
ID
转成
UTF-16
的字符串存储。注意在
getName
函数中可以使用move语义提高效率。

Case2:
std::initializer_list
可以与其他参数一起使用:

class Widget {
public:
Widget(const std::string& name, double epsilon, std::initializer_list<int> il);
…
};
std::string name("Buffy");
// same as Widget w(name, 0.5, std::initializer_list({5,10,15}));
Widget w { name, 0.5, {5, 10, 15} };


Case3:可以用作模板

class Widget {
public:
template<typename T> Widget(std::initializer_list<T> il);
...
};
...
Widget w1 { -55, 25, 16 };                      // fine, T = int

注意推导过程中出现元素类型不一致会报错:

Widget w2 { -55, 2.5, 16 };                         // error, T can’t be deduced


6.7.4
initializer_list
与统一初始化方式
{}
的冲突

如果一个类实现了
initializer_list
为参数的构造函数,编译器为
{}
优先匹配这个构造函数。例如

class Widget {
public:
Widget(double value, double uncertainty);       // #1
Widget(std::initializer_list<double> values);   // #2
…
};
double d1, d2;
…
Widget w1 { d1, d2 };                               // calls #2
Widget w2(d1, d2);                                  // calls #1

进一步理解:

class Widget {
public:
Widget(double value, double uncertainty);       // #1
Widget(std::initializer_list<std::string> values); // #2
…
};
double d1, d2;
…
Widget w1 { d1, d2 };                               // 调用失败,编译报错。因为没有double ⇒ string的转换
Widget w2(d1, d2);                                  // still calls #1

也就是说如果实现了
initializer_list
参数的版本,那么
{}
永远考虑这个版本,即便出错。

6.7.5 多个
initializer_list
之间的冲突

如果一个对象实现了多个版本的以
initializer_list
为参数的构造函数,编译器会转换成本最小的版本。

class Widget {
public:
Widget(std::initializer_list<int>);             // #1
Widget(std::initializer_list<double>);          // #2
Widget(std::initializer_list<std::string>);     // #3
Widget(int, int, int); // due to above ctors, this ctor not
}; // considered for “{... }” args
// int ⇒ double same rank as double ⇒ int, so ambiguous
Widget w1 { 1, 2.0, 3 };
// float ⇒ double better than float ⇒ int, so calls #2
Widget w2 { 1.0f, 2.0, 3.0 };
std::string s;
Widget w3 { s, "Init", "Lists" };                   // calls #3

如果即使最优的选择也包含有损转换,则编译报错:

class Widget {
public:
Widget(std::initializer_list<int>);
Widget(int, int, int);                          // due to above ctor, not
};                                                  // considered for “{... }” args
Widget w { 1, 2.0, 3 };                             // error! double ⇒ int narrows

6.8 统一初始化方式的总结

{}
初始化可以用在任何地方,语义上区分聚合与非聚合
禁止有损转换
initializer_list
对象允许初始化list传给函数,并不限制构造函数,例如
std::vector::insert


如果这篇文章对您有帮助,请到CSDN博客留言;
转载请注明:来自雨润的技术博客 http://blog.csdn.net/sunyurun
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: