Visual Studio 2013预览版中的 C++11 新特性

介绍

Visual C++ 2013 Preview 在6月发布了,C++开发者又找到一个编译器可以更好的支持ISO C++ 11 的特性了。本文介绍了这些新的特性并附有代码实例。

你想动手尝试编译文中的这些代码话,需要去下载并安装Visual Studio 2013 Preview,我尚未在其他编译器上测试这些代码,所以我并不知道与Gcc 或Clang的兼容性(可恶的C++)。

 

原始字符串字面值

VC++ 2013现在支持原始字符串字面值了。注意:它并不支持统一码字符串字面值。一个原始字符串字面值允许你避免转义那些在HTML,XML和正则表达式里运用得得心应手的特殊字符。下面是一个示例用法:

1
auto s1 = R "(This is a " raw " string)" ;

现在,s1是一个指向常量字符串值为“This is a “raw” string”的指针。尽管不支持嵌套双引号,这与C#支持的@string文字是类似的。那么要在一个字符串字面值中嵌入R”(…)”会怎样。这种情况下,你可以使用以下语法:

1
auto s2 = R "QQ(Example: R" (This is my raw string) ")QQ" ;

现在,s2包含 Example: R”(This is my raw string)”。 在这个例子中,我把QQ作为界定符。这个界定符可以是任何长度不超过16的字符串。原始字符串字面值也可以包含换行:

1
2
3
auto s3 = R"(<tr>
<td>data</td>
</tr>)";

最后,不论他们什么时候添加统一码字符串字面值的支持,你都可以将它们连接起来并构成原始统一码字符串字面值。

 

可变参数模板

可变参数模板是一个允许多个参数的模板。在我看来,这是个提供给库作者而不是给库使用者的特性,所以我也不是很确定它在C++程序员中会有多流行。以下我们用一个非常简单的例子来展示如何在实际开发中使用可变参数模板。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Variadic template declaration
template<typename... Args> class Test;
 
// Specialization 1
template<typename T> class Test<T>
{
public :
   T Data;
};
 
// Specialization 2
template<typename T1, typename T2> class Test<T1, T2>
{
public :
   T1 Left;
   T2 Right;
};
 
void Foo()
{
   Test< int > data;
   data.Data = 24 ;
 
   Test< int , int > twovalues;
   twovalues.Left = 12 ;
   twovalues.Right = 15 ;
}

当使用可变参数模板时,智能感应(intellisense)能很好地配合我们的开发。可变参数模板的实现包括一个叫asizeof的函数,这个函数能返回这个模板的参数个数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template<typename... Args> class Test
{
public :
   size_t GetTCount()
   {
     return sizeof...(Args);
   }
};
 
// . . .
 
Test< int > data;
size_t args = data.GetTCount(); //1
 
Test< int , int , char*> data2;
args = data2.GetTCount(); //3
 
Test< int , float> data3;
args = data3.GetTCount(); //2

这其实就是一个数个数的例子,但我猜他们之所以使用一个现存的函数名是因为这样子做会让C++程序员们更容易上手。

 

对于可变参数模板,一个常用的做法就是专攻其中一个参数,然后把其余的参数都变为可选。这个做法可以以递归的形式实现。以下是一个比较傻的例子,但它能让你明白什么时候不应该用可变参数模板,继而更好地了解这个语言特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
template<typename... Args> class Test;
 
// Specialization for 0 arguments
template<> class Test<>
{
};
 
// Specialization for at least 1 argument
 
template<typename T1, typename... TRest> class Test<T1, TRest...>
   : public Test<TRest...>
{
public :
   T1 Data;
 
   // This will return the base type
   Test<TRest...>& Rest()
   {
     return * this ;
   }
};
 
void Foo()
{
   Test< int > data;
   data.Data = 24 ;
 
   Test< int , int > twovalues;
   twovalues.Data = 10 ;
   // Rest() returns Test<int>
   twovalues.Rest().Data = 11 ;
 
   Test< int , int , char*> threevalues;
   threevalues.Data = 1 ;
   // Rest() returns Test<int, int>
   threevalues.Rest().Data = 2 ;
   // Rest().Rest() returns Test<char*>
   threevalues.Rest().Rest().Data = "test data" ;
}

大家请注意了,千万别把代码写成这样。这个例子仅仅是用来教学的,正确的做法我会在下一个章节中告诉大家。

 

Tuple的实现

我们来看一下std tuple的头文件 (由VC++团队的Stephan T. Lavavej负责维护 – 最初的代码由P.J. Plauger编写),浏览这些代码,让我的大脑几乎要宕掉了。为了更好的理解代码,我将代码进行简化,摘出其中可以访问tuple的值的最少的代码(能够支持读和写)。这有助于理解在设计模板类时,通常可变参数模板是如何通过递归展开来大幅减少代码的行数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// tuple
template< class ... _Types> class tuple;
 
// 空tuple
template<> class tuple<> {};
 
// 递归的tuple定义
template< class _This,
   class ... _Rest>
   class tuple<_This, _Rest...>
   : private tuple<_Rest...>
{
public :
   _This _Myfirst;
};

这里的递归特化使用了继承,因此tuple的每个类型成员都确定的时候递归会终止。读取tuple值的时候,tuple_element类起到读取辅助类的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// tuple_element
template<size_t _Index, class _Tuple> struct tuple_element;
 
// select first element
template< class _This, class ... _Rest>
struct tuple_element< 0 , tuple<_This, _Rest...>>
{
   typedef _This& type;
   typedef tuple<_This, _Rest...> _Ttype;
};
 
// recursive tuple_element definition
template <size_t _Index, class _This, class ... _Rest>
struct tuple_element<_Index, tuple<_This, _Rest...>>
   : public tuple_element<_Index - 1 , tuple<_Rest...> >
{
};

这里又一次使用了递归继承,同时边界条件也特化了。注意这里的两个typedef,其中一个定义为对应值类型的引用,另一个定义为和tuple_element类型参数相同的tuple类型。因此,给定一个_Index值,在那个递归层次上我们就能得到对应tuple的类型和tuple值的类型。下面的get方法就使用了这个特性。

1
2
3
4
5
6
7
8
// get reference to _Index element of tuple
template<size_t _Index, class ... _Types> inline
   typename tuple_element<_Index, tuple<_Types...>>::type
   get (tuple<_Types...>& _Tuple)
{
   typedef typename tuple_element<_Index, tuple<_Types...>>::_Ttype _Ttype;
   return (((_Ttype&) _Tuple)._Myfirst);
}

注意返回类型,它使用上面定义的类型 typedef。同样,元组(tupleis)转换为上述定义过的类型 _TType ,然后我们访问 _Myfirst 成员 (它表示的值)。现在你可以如下所示,编写代码,

1
2
3
4
5
6
tuple< int , char> t1;
get < 0 >(t1) = 959 ;
get < 1 >(t1) = 'A' ;
 
auto v1 = get < 0 >(t1);
auto v2 = get < 1 >(t1);

现在 , 这 不用 说 , 但 我会 说 只是 可以 肯定 的是 —— 这 里只 是 为了 演示 。 不 要在 实际 代码 中 使用 这些 方法, 而是调用 std::tuple, 它可以完成比 这 一切多的功能 ( 这就是为什么他有800行长).

 

代理构造函数

代理构造函数已经在C#中用了好长时间,所以将其引入到C++中也很不错。编译器允许一个类型的构造函数(代理构造函数)在其初始化列表中包含另一个构造函数。以前编写代码形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Error
{
public :
   Error()
   {
     Init( 0 , "Success" );
   }
 
   Error( const char* message)
   {
     Init(- 1 , message);
   }
 
   Error( int errorCode, const char* message)
   {
     Init(errorCode, message);
   }
 
private :
   void Init( int errorCode, const char* message)
   {
     //...
   }
};

采用代理构造函数是,可以写成如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Error
{
public :
   Error() : Error( 0 , "Success" )
   {
   }
 
   Error( const char* message) : Error(- 1 , message)
   {
   }
 
   Error( int errorCode, const char* message)
   {
     // ...
   }
};

相关阅读-  Herb Sutter和Jim Hyslop在十年前(2003年5月)写的一篇有趣的关于代理构造函数的文章。

函数模板中的默认模板参数

这是VC++ 2013现在支持的另一项C++ 11特性。目前为止,下面的代码仍然无法通过VC++编译。

1
2
3
4
template <typename T = int > void Foo(T t = 0 ) { }
 
// error C4519: default template arguments are only
// allowed on a class template

Visual C++ 2013 能够顺利编译这些代码,模板参数推断也能正确进行。

1
2
3
4
Foo(12L); // Foo<long>
Foo( 12.1 ); // Foo<double>
Foo( 'A' ); // Foo<char>
Foo(); // Foo<int>

这项特性的实用性在下面的例子里尤为明显。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template <typename T> class Manager
{
public :
   void Process(T t) { }
};
 
template <typename T> class AltManager
{
public :
   void Process(T t) { }
};
 
template <typename T, typename M = Manager<T>> void Manage(T t)
{
   M m;
   m.Process(t);
}
 
Manage( 25 ); // Manage<int, Manager<int>>
Manage< int , AltManager< int >>( 25 ); // Manage<int, AltManager<int>>

并不是所有的参数都需要默认参数。

1
2
3
4
5
6
template <typename B, typename T = int > void Bar(B b = 0 , T t = 0 ) { }
 
Bar( 10 ); // Bar<int, int>
Bar(10L); // Bar<long, int>
Bar(10L, 20L); // Bar<long, long>
Bar(); // will not compile

如果带默认参数的函数模板有重载的话,类型无法推断的时候编译器将会给出错误。

1
2
3
4
5
6
7
template <typename T = int > void Foo(T t = 0 ) { }
template <typename B, typename T = int > void Foo(B b = 0 , T t = 0 ) { }
 
Foo(12L); // will not compile
Foo( 12.1 ); // will not compile
Foo( 'A' ); // will not compile
Foo(); // Foo<int>

使用函数模板的默认模板参数时应当在这里注意。

 

显式转换运算符

我仍然记得2004年八月的一天,那个时候我意识到尽管我是一个还不错的C++程序员,我对explicit关键字一无所知,这令我十分局促不安。那之后我写了一篇博客文章

简单说明一下explicit的使用。考虑一下下面的例子。

1
2
3
4
5
6
7
8
9
10
11
class Test1
{
public :
   explicit Test1( int ) { }
};
 
void Foo()
{
   Test1 t1( 20 );
   Test1 t2 = 20 ; // will not compile
}

尽管转换构造函数可以达到这一目的,转换运算符因为缺乏标准支持而无法完成类似的任务。坏消息是你无法确保转换构造函数和转换运算符的行为是一致的。考虑一下下面的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Test1
{
public :
   explicit Test1( int ) { }
};
 
class Test2
{
   int x;
public :
   Test2( int i) : x(i) { }
   operator Test1() { return Test1(x); }
};
 
void Foo()
{
   Test2 t1 = 20 ;
   Test1 t2 = t1; // will compile
}

上面的代码能通过编译。现在有了C++ 11的新特性,explicit也可以用在转换运算符上了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Test2
{
   int x;
public :
   Test2( int i) : x(i) { }
   explicit operator Test1() { return Test1(x); }
};
 
void Foo()
{
   Test2 t1 = 20 ;
   Test1 t2 = (Test1)t1; // this compiles
   Test1 t3 = t1; // will not compile
}

下面的这个例子里隐式应用了bool类型的转换运算符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Test3
{
public :
   operator bool() { return true ; }
};
 
void Foo()
{
   Test3 t3;
   if (t3)
   {
   }
 
   bool b = t3;
}

这段代码能通过编译。现在试一下在运算符上加上explicit关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Test3
{
public :
   explicit operator bool() { return true ; }
};
 
void Foo()
{
   Test3 t3;
   if (t3) // this compiles!
   {
   }
 
   bool b = t3; // will not compile
}

正如预期,第二个转换无法通过编译,但是第一个通过了。这是因为if里的bool转换被视为显式转换。因此在这里你要小心谨慎,仅仅添加explicit关键字无法防止意外情况下的类型转换,类型可能仍然是不安全的。

 

初始化列表和统一初始化

一直以来我们都可以用初始化列表初始化数组,现在对于有类型为std::initializer_list<T>(包含构造函数)的类型我们也可以这么做。标准库中的容器现在都支持这一特性。

1
2
3
4
5
6
7
8
9
10
void foo()
{
   vector< int > vecint = { 3 , 5 , 19 , 2 };
   map< int , double> mapintdoub =
   {
     { 4 , 2.3 },
     { 12 , 4.1 },
     { 6 , 0.7 }
   };
}

自己实现这些功能很浪费时间

1
2
3
4
5
6
7
8
9
void bar1( const initializer_list< int >& nums)
{
   for (auto i : nums)
   {
     // use i
   }
}
 
bar1({ 1 , 4 , 6 });

用户自定义类型也可以使用这一特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class bar2
{
public :
   bar2(initializer_list< int > nums) { }
};
 
class bar3
{
public :
   bar3(initializer_list<bar2> items) { }
};
 
bar2 b2 = { 3 , 7 , 88 };
 
bar3 b3 = { { 1 , 2 }, { 14 }, { 11 , 8 } };

C++11也新增了一个相关特性:统一初始化( Uniform initialization)。这一特性将自动匹配合适的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class bar4
{
   int x;
   double y;
   string z;
 
public :
   bar4( int , double, string) { }
};
 
class bar5
{
public :
   bar5( int , bar4) { }
};
 
bar4 b4 { 12 , 14.3 , "apples" };
 
bar5 b5 { 10 , { 1 , 2.1 , "bananas" } };

使用初始化列表的构造函数将被优先使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class bar6
{
public :
   bar6( int , int ) // (1)
   {
     // ...
   }
 
   bar6(initializer_list< int >) // (2)
   {
     // ...
   }
};
 
bar6 b6 { 10 , 10 }; // --> calls (2) above

参考资料

参与翻译:无奈的钝刀, MtrS, 大志darcy, jimmyjmh, LinuxQueen, soaring

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值