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

译者: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>::iterator
const 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 objbegin(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循环,不适用于whiledo...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;                // 编译报错

以前NULL0的使用方法保持兼容

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_tchar互转:

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

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值