65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (29投票s)

2013年7月20日

CPOL

6分钟阅读

viewsIcon

97493

这是对 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日 - 文章发布
© . All rights reserved.