Visual C++ 2013 预览版中的 C++ 11 功能






4.82/5 (29投票s)
这是对 VC++ 2013 预览版中支持的 ISO C++ 11 编译器功能的快速概述。
引言
当 Visual C++ 2013 Preview 在六月的最后一周发布时,C++ 开发者惊喜地发现编译器已经增加了对多个 ISO C++ 11 特性的支持。本文汇集了这些新特性,并包含代码片段示例,展示了这些新特性的实际应用。如果您想编译本文中的示例,需要下载并安装 Visual Studio 2013 Preview。我没有在任何其他编译器上测试过这些示例,所以我不确定它与 GCC 或 Clang 的兼容性如何。
原始字符串字面量
VC++ 2013 现在支持原始字符串字面量。请注意,它不支持 Unicode 字符串字面量。原始字符串字面量允许您避免转义特殊字符,这在处理 HTML、XML 和正则表达式时非常方便。这是一个示例用法。
auto s1 = R"(This is a "raw" string)";
现在 s1
是一个 const char*
,包含值 - This is a "raw" string。这类似于 C# 中的 @
字符串字面量支持,尽管 C# 也不支持嵌入双引号。那么,如果您想在字符串字面量中嵌入 R"(...)" 怎么办?在这种情况下,您可以使用以下语法。
auto s2 = R"QQ(Example: R"(This is my raw string)")QQ";
现在 s2
包含 - Example: R"(This is my raw string)"。在这个示例中,我使用了 QQ
作为分隔符。这个分隔符可以是任何长度不超过 16 个字符的字符串。原始字符串字面量也可以包含换行符。
auto s3 = R"(<tr>
<td>data</td>
</tr>)";
最终,当他们也增加了对 Unicode 字符串字面量的支持时,您将能够将它们结合起来,拥有原始 Unicode 字符串字面量。
可变参数模板
可变参数模板是能接受可变数量模板参数的模板。在我看来,它更像是库作者而不是库使用者的一个特性,所以我不太确定它在终端 C++ 开发者中会有多流行。这里有一个非常简单的示例,展示了可变参数模板的实际应用。
// 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 也能完美地发挥作用。可变参数模板的实现包括一个 sizeof...
运算符,它返回参数包中的模板参数数量。
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++ 开发者熟悉的现有运算符。
可变参数模板的典型方法是将其专门化为一个参数,而其余参数是可选的(这会递归地工作)。这是一个相当朴素的示例,可能是一个不应该使用可变参数模板的示例,但它帮助我更好地理解了可变参数模板。
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";
}
请注意,没有人会像这样编写代码。这个示例仅用于学术目的。正如我在下一节中展示的,有正确的方法可以做到这一点。
元组实现
我查看了 std tuple 头文件(由 VC++ 团队的 Stephan T. Lavavej 维护 - 原始代码由 P.J. Plauger 编写),毫不夸张地说,仅仅浏览代码就让我头晕目眩了一段时间。为了更好地理解其实现方式,我将其简化并提取出实例化 tuple
和访问其值(并能够读写)所需的最小代码。这帮助我理解了在设计模板类时,可变参数模板如何通常与递归展开结合使用,从而显著减少代码行数。
// tuple
template<class... _Types> class tuple;
// empty tuple
template<> class tuple<> {};
// recursive tuple definition
template<class _This,
class... _Rest>
class tuple<_This, _Rest...>
: private tuple<_Rest...>
{
public:
_This _Myfirst;
};
递归特化使用继承,这样我们最终会为 tuple
指定的每种参数类型都拥有成员。为了访问 tuple
值,tuple_element
类被用作一种访问器类。
// 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...> >
{
};
同样,使用了递归继承,并且还对第 0 个情况进行了特化。请注意两个 typedef
,其中一个是对值类型的引用,另一个表示具有与 tuple_element
相同参数的 tuple
。因此,给定一个 _Index
值,我们可以检索该递归级别的 tuple
类型和 tuple
值的类型。这在 get
方法中使用。
// 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);
}
请注意返回类型,它使用了上面定义的 type
typedef
。类似地,tuple
被转换为上面定义的 _TType typedef
,然后我们访问 _Myfirst
成员(它代表值)。现在您可以编写如下代码。
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++ 中得到它真是太棒了。这个编译器特性允许类型的构造函数(委托构造函数)在其初始化列表中包含该类型的另一个构造函数。所以,以前您必须这样编写代码。
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)
{
//...
}
};
有了委托构造函数,您现在可以这样编写。
class Error
{
public:
Error() : Error(0, "Success")
{
}
Error(const char* message) : Error(-1, message)
{
}
Error(int errorCode, const char* message)
{
// ...
}
};
额外阅读 - 这是 Herb Sutter 和 Jim Hyslop 在 10 年前(2003 年 5 月)撰写的一篇关于委托构造函数的幽默文章,当时标准机构才刚刚开始认真考虑它作为一个提议。
函数模板的默认模板参数
这是 VC++ 2013 现在支持的又一个 C++ 11 特性。直到现在,以下代码在 VC++ 中是无法编译的。
template <typename T = int> void Foo(T t = 0) { }
// error C4519: default template arguments are only
// allowed on a class template
Visual C++ 2013 能够很好地编译此代码,并且模板类型也被正确推断。
Foo(12L); // Foo<long>
Foo(12.1); // Foo<double>
Foo('A'); // Foo<char>
Foo(); // Foo<int>
此特性的有用性在以下示例中更为明显。
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>>
并非所有参数都需要有默认值。
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
当您有带有默认参数的重载函数模板时,如果类型无法推断,可能会遇到编译器错误。
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年8月一个相当尴尬的日子,当时我意识到尽管我自认为是合格的C++程序员,但直到那时我才知道 explicit
关键字。我当时写了一篇博客文章。
简而言之,为了总结 explicit
的用法,请看下面的例子。
class Test1
{
public:
explicit Test1(int) { }
};
void Foo()
{
Test1 t1(20);
Test1 t2 = 20; // will not compile
}
虽然这可以通过转换构造函数完成,但对于转换运算符来说却无法做到,因为标准不支持它。这样做的坏处是,您无法设计一个类,使其在转换构造函数和转换运算符之间保持一致性。请看下面的例子。
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
了。
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
转换运算符一个不那么明显的行为。
class Test3
{
public:
operator bool() { return true; }
};
void Foo()
{
Test3 t3;
if (t3)
{
}
bool b = t3;
}
这编译得很好。现在尝试将 explicit
添加到运算符中。
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
添加到你的 bool
转换运算符并不能保护你的类型免受意外的 bool
转换。
初始化列表和统一初始化
我们一直能够将初始化列表与数组一起使用,现在你可以将它用于任何具有接受 std::initializer_list<T>
类型参数的方法(包括构造函数)的类型。标准库集合都已更新以支持初始化列表。
void foo()
{
vector<int> vecint = { 3, 5, 19, 2 };
map<int, double> mapintdoub =
{
{ 4, 2.3},
{ 12, 4.1 },
{ 6, 0.7 }
};
}
用你自己的函数来做这件事是微不足道的。
void bar1(const initializer_list<int>& nums)
{
for (auto i : nums)
{
// use i
}
}
bar1({ 1, 4, 6 });
您也可以对用户定义的类型执行此操作。
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 中添加的一个相关特性。它会自动使用匹配的构造函数。
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" } };
如果存在初始化列表构造函数,它将优先于另一个匹配的构造函数。
class bar6
{
public:
bar6(int, int) // (1)
{
// ...
}
bar6(initializer_list<int>) // (2)
{
// ...
}
};
bar6 b6 { 10, 10 }; // --> calls (2) above
好了,就这些了。一如既往,非常欢迎反馈和批评。谢谢。
参考文献
历史
- 2013年7月20日 - 文章发布