【翻译】C++14的新特性简介

时间:2022-07-22
本文章向大家介绍【翻译】C++14的新特性简介,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

之前写完了《C++Primer》的笔记,但是《C++Primer》已经是快十年的老书了,其包含的C++特性仅仅到C11为止,因此又去看了些C++14的特性,发现Anthony Calandra在https://github.com/AnthonyCalandra/modern-cpp-features/blob/master/CPP14.md 中有对C++14重要的新特性的简介,看完就翻译整理后发上来了。原文中有些地方写得不是很好理解所以对其做了少量修改。

新特性一览

语言新特性

  • 二进制字面值
  • 泛型的Lambda表达式
  • 初始化Lambda的捕获列表
  • 推断返回类型
  • decltype(auto)
  • 放宽对常量表达式函数的约束
  • 变量模板
  • [[deprecated]]属性

标准库新特性

  • 可用于标准库类型的自定义字面量
  • 编译期的整型序列
  • std::make_unique

二进制字面值(Binary literals)

二进制字面值提供了一个表示二进制数字的方便的方法,有了这个之后我们可以直接使用二进制数字了,而且允许我们使用单引号 ' 来作为数字分位符让数字方便阅读

0b110 // == 6
0b1111'1111 // == 255

泛型的Lambda表达式(Generic lambda expressions)

C14允许我们在Lambda的参数列表中使用auto了,使多态的Lambda成为可能

auto identity = [](auto x) { return x; };
int three = identity(3); // == 3
std::string foo = identity("foo"); // == "foo"

初始化Lambda的捕获列表(Lambda capture initializers)

C14允许我们用任意的表达式对Lambda的捕获列表内容进行初始化了。如今提供给捕获列表的名称不再需要与任何所在的局部范围的变量相关联了,而且可以向Lambda表达式输入引入新的名称了。初始化表达式会在Lambda被创建时(而不是调用时)进行计算

int factory(int i) { return i * 10; }
auto f = [x = factory(2)] { return x; }; // returns 20

auto generator = [x = 0] () mutable {
  // this would not compile without 'mutable' as we are modifying x on each call
  return x++;
};
auto a = generator(); // == 0
auto b = generator(); // == 1
auto c = generator(); // == 2

于是现在我们可以move()或者forward()那些之前只能通过拷贝或引用捕获的值进入Lambda,因此我们可以捕获那些只允许move的得到右值引用的类型的值进入Lambda了(例如unique_ptr)。注意在下面的例子中等号左边的task2捕获列表中的p是属于Lambda体私有的变量而不是原始p的引用

auto p = std::make_unique<int>(1);

auto task1 = [=] { *p = 5; }; // ERROR: std::unique_ptr cannot be copied
// vs.
auto task2 = [p = std::move(p)] { *p = 5; }; // OK: p is move-constructed into the closure object
// the original p is empty after task2 is created

运用这个引用捕获,我们可以拥有不同于被引用变量的名称

auto x = 1;
auto f = [&r = x, x = x * 10] {
  ++r;
  return r + x;
};
f(); // sets x to 2 and returns 12

推断返回类型(Return type deduction)

编译器会帮你尝试推断出使用auto在C14中作为的返回类型。伴随着Lambda你现在可使用auto减少对其返回类型的描述(不再需要使用尾置返回),这还会使得返回一个推断类型或一个右值引用成为可能

// Deduce return type as `int`.
auto f(int i) {
 return i;
}
template <typename T>
auto& f(T& t) {
  return t;
}

// Returns a reference to a deduced type.
auto g = [](auto& x) -> auto& { return f(x); };
int y = 123;
int& z = g(y); // reference to `y`

decltype(auto)

decltype(auto)类型说明符也可以像auto一样推断类型。但是,它在推断返回类型的时候能保持它们的引用属性和const属性,这是auto所做不到的

const int x = 0;
auto x1 = x; // int
decltype(auto) x2 = x; // const int
int y = 0;
int& y1 = y;
auto y2 = y1; // int
decltype(auto) y3 = y1; // int&
int&& z = 0;
auto z1 = std::move(z); // int
decltype(auto) z2 = std::move(z); // int&&
// Note: Especially useful for generic code!

// Return type is `int`.
auto f(const int& i) {
 return i;
}

// Return type is `const int&`.
decltype(auto) g(const int& i) {
 return i;
}

int x = 123;
static_assert(std::is_same<const int&, decltype(f(x))>::value == 0);
static_assert(std::is_same<int, decltype(f(x))>::value == 1);
static_assert(std::is_same<const int&, decltype(g(x))>::value == 1);

放宽对常量表达式函数的约束(Relaxing constraints on constexpr functions)

在C11中,常量表达式函数的函数体只能包含非常局限的语法,包括但不仅仅是:typedef, using, 和只能有一个return等。在C14中,允许包含的语法大幅扩张让我们可以使用更普通的语法例如if语句,多个return,循环语句等等…

constexpr int factorial(int n) {
  if (n <= 1) {
    return 1;
  } else {
    return n * factorial(n - 1);
  }
}
factorial(5); // == 120

变量模板(Variable templates)

C14允许我们编写变量模板如下

template<class T>
constexpr T pi = T(3.1415926535897932385);
template<class T>
constexpr T e  = T(2.7182818284590452353);

[[deprecated]]属性 ( [[deprecated]] attribute )

C14引入了[[deprecated]](不推荐)属性,标识了一个元素(函数,类等等…)是不被鼓励且可能引发编译器警告的。如果这个属性包含了一个警告原因,那么这会在编译器警告中显示出来

[[deprecated]]
void old_method();
[[deprecated("Use new_method instead")]]
void legacy_method();

可用于标准库类型的自定义字面量(User-defined literals for standard library types)

新的可用于标准库类型的自定义字面量包括了新的内置字面量chrono和basic_string。它们可以作为常量表达式constexpr,也就是它们可以在编译期被使用。这些字面量的用法还包括了编译期的整型解析,二进制字面量和虚数字面量

using namespace std::chrono_literals;
auto day = 24h;
day.count(); // == 24
std::chrono::duration_cast<std::chrono::minutes>(day).count(); // == 1440

编译期的整型序列(Compile-time integer sequences)

模板类std::integer_sequence代表了一个串编译期的整型。这里有两个帮助模板类:

  • std::make_integer_sequence<T, N> ——创建一个T类型的值从0到N-1的整型序列
  • std::index_sequence_for<T...> ——将模板参数的值打包到一个整型序列中

将一个数组转为tuple:

template<typename Array, std::size_t... I>
decltype(auto) a2t_impl(const Array& a, std::integer_sequence<std::size_t, I...>) {
  return std::make_tuple(a[I]...);
}

template<typename T, std::size_t N, typename Indices = std::make_index_sequence<N>>
decltype(auto) a2t(const std::array<T, N>& a) {
  return a2t_impl(a, Indices());
}

std::make_unique

类似std::make_shared,C14引入了std::make_unique.由于以下几点原因std::make_unique是创建std::unique_ptr实例的推荐方式:

  • 能避免使用到new运算符
  • 当要求指针保持基础类型时避免代码的重复
  • 最重要的,它是异常安全(exception-safety)的。假如我们用以下方法调用foo函数:
foo(std::unique_ptr<T>{new T{}}, function_that_throws(), std::unique_ptr<T>{new T{}});

编译器可以以自由的顺序进行,如果编译器先调用了new T{},然后是function_that_throws(),再然后…由于在一开始对T的构造中我们在堆上分配了一块内存,然后我们抛出了异常,因此我们在这里会导致一块内存泄漏

而通过std::make_unique,我们可以异常安全地进行这个过程:

foo(std::make_unique<T>(), function_that_throws(), std::make_unique<T>());