cpp14关键新增特性理解

发布时间 2023-07-20 14:31:34作者: 非法关键字

new/delete elision

"new/delete elision" 是 C++ 中的一个优化技术,用于减少由于动态内存分配和释放而产生的性能开销。它发生在编译器优化的过程中,可以将某些动态内存分配和释放的操作消除,从而提高程序的执行效率。

具体来说,"new/delete elision" 是指在一些情况下,编译器会自动优化代码,将使用 "new" 运算符分配内存的对象直接放在栈上,而不是在堆上进行动态内存分配,从而避免了动态内存分配和释放的开销。

这种优化通常在以下情况下发生:

  1. 对于小型对象:如果对象相对较小,那么将其放在栈上可能比在堆上分配更高效。编译器可以根据对象的大小和复杂性来判断是否进行 "new/delete elision"。
  2. 短期生存周期:如果对象的生存周期很短,只在某个函数或代码块中使用,那么将其放在栈上可以避免频繁的堆内存分配和释放。

需要注意的是,"new/delete elision" 是由编译器进行的自动优化,开发者不需要显式地指定。优化是否发生取决于编译器的具体实现和优化级别。

举个简单的例子:

#include <iostream>

class MyClass {
public:
    MyClass() {
        std::cout << "MyClass constructor" << std::endl;
    }

    ~MyClass() {
        std::cout << "MyClass destructor" << std::endl;
    }
};

int main() {
    MyClass obj; // 使用 "new" 关键字动态分配内存
    return 0;
}

在这个例子中,对象 obj 使用 "new" 运算符动态分配内存,因此会在堆上分配内存空间并调用构造函数。然而,由于 obj 是一个局部对象且没有指定为指针,编译器可能会进行 "new/delete elision" 优化,将 obj 直接放在栈上分配内存,从而避免了堆内存分配和释放的开销。

统一初始化

统一初始化是 C++11 中引入的一个特性,它允许我们使用统一的语法来初始化各种类型的对象,包括内置类型、自定义类类型、数组和 STL 容器等。

在 C++11 之前,对象的初始化方式较为繁琐,不同类型的对象使用不同的初始化语法,例如:

  1. 内置类型:
int x = 42;
double y = 3.14;
  1. 自定义类类型:
class MyClass {
public:
    MyClass(int a, double b) : x(a), y(b) {}
private:
    int x;
    double y;
};

MyClass obj(10, 2.5);
  1. 数组:
int arr[3] = {1, 2, 3};

C++11 引入了统一初始化初始化语法,使用花括号 {} 来进行初始化,无论是内置类型、自定义类类型、数组还是容器,都可以使用这种语法来初始化:

  1. 内置类型:
int x{42};
double y{3.14};
  1. 自定义类类型:
class MyClass {
public:
    MyClass(int a, double b) : x(a), y(b) {}
private:
    int x;
    double y;
};

MyClass obj{10, 2.5};
  1. 数组:
int arr[]{1, 2, 3};
  1. 容器:
std::vector<int> vec{1, 2, 3};
std::map<std::string, int> myMap{{"one", 1}, {"two", 2}};

使用统一初始化语法有几个好处:

  • 可以用一种统一的语法来初始化不同类型的对象,使代码更加一致和易读。
  • 可以避免一些窄化转换(narrowing conversions)问题,统一初始化在编译时进行类型检查,如果初始化值不适合目标类型,则会产生编译错误。
  • 可以更方便地使用初始化列表进行初始化,尤其对于自定义类类型和容器类型,初始化列表可以简化代码。

需要注意的是,统一初始化的语法使用花括号 {},而不是传统的圆括号 (),因此请确保在初始化时使用正确的括号。同时,在某些情况下,统一初始化可能导致歧义或产生不符合预期的结果,因此在使用时应当谨慎,并遵循最佳实践。

SFINAE

SFINAE 是 C++ 中的一个编程术语,它代表"Substitution Failure Is Not An Error",即"替换失败并非错误"。SFINAE 是一种编译时的模板元编程技术,用于在模板实例化时根据类型匹配进行选择和排除候选函数或模板特化。

当进行函数模板实例化时,编译器会根据实参的类型进行类型推导,并尝试替换函数模板的模板参数。如果实例化过程中发生错误(例如无法匹配函数调用或无法推导出合适的类型),传统的 C++ 编译器通常会抛出错误。然而,SFINAE 技术通过在模板实例化中允许部分替换失败,避免将这样的错误视为编译错误,而是选择继续查找其他可行的候选函数或特化。

SFINAE 的主要应用场景是在模板元编程中,特别是在实现泛型代码时,根据类型特性对函数模板进行选择或排除。

一种常见的 SFINAE 使用场景是通过使用模板函数的返回类型和参数列表中的 decltype 表达式来实现条件选择。如果 decltype 表达式能够成功推导出类型,则函数模板可行,否则编译器将忽略该模板,而不会报错。

以下是一个简单的示例,展示了如何使用 SFINAE 来实现条件选择:

#include <iostream>
#include <type_traits>

// SFINAE:如果 T 类型有成员函数 print(),则调用 print();否则什么都不做
template <typename T>
typename std::enable_if_t<std::is_same_v<decltype(std::declval<T>().print()), void>>
doPrint(const T& obj) {
    obj.print();
}

// SFINAE:如果 T 类型没有成员函数 print(),则调用该函数,输出默认消息
template <typename T>
typename std::enable_if_t<!std::is_same_v<decltype(std::declval<T>().print()), void>>
doPrint(const T& obj) {
    std::cout << "Default print message" << std::endl;
}

// 测试类
class HasPrint {
public:
    void print() const {
        std::cout << "HasPrint::print()" << std::endl;
    }
};

class NoPrint {
    // 没有 print() 成员函数
};

int main() {
    HasPrint hp;
    NoPrint np;

    doPrint(hp); // 输出:HasPrint::print()
    doPrint(np); // 输出:Default print message

    return 0;
}

在这个例子中,我们定义了两个模板函数 doPrint,分别针对具有和没有 print() 成员函数的类型。通过使用 SFINAE 技术,编译器会根据实参的类型选择合适的函数模板进行实例化,从而实现条件选择。

泛型/元编程

泛型编程和元编程是 C++ 中的两个重要编程技术,它们都是为了实现更灵活、通用和高效的代码而设计的。

  1. 泛型编程: 泛型编程是一种编程范式,它强调编写通用代码,使得代码可以适用于不同类型的数据而无需针对每种类型编写特定的代码。在 C++ 中,泛型编程的主要工具是模板。通过使用函数模板和类模板,可以将代码与特定的数据类型解耦,从而实现通用性。

例如,考虑以下函数模板用于交换两个值:

template <typename T>
void swap(T& a, T& b) {
    T temp = a;
    a = b;
    b = temp;
}

这个函数模板可以交换不同类型的数据,如 int、double、char、自定义类等,而无需为每种类型编写一个交换函数。

  1. 元编程: 元编程是一种编程技术,它允许在编译期间进行计算和操作,以生成更加复杂的代码或在编译期间优化代码。在 C++ 中,元编程的主要工具是模板元编程(Template Metaprogramming,TMP)。TMP 允许在编译期间进行模板实例化和递归展开,从而在编译时生成代码。

TMP 的一个典型应用是实现递归算法,通过模板的递归展开,在编译期间生成代码。 TMP 的一些特性,例如模板特化、constexpr、变量模板和编译期常量计算等,使得元编程在 C++ 中非常强大。

例如,以下代码使用 TMP 计算斐波那契数列:

template <int N>
struct Fibonacci {
    static constexpr int value = Fibonacci<N - 1>::value + Fibonacci<N - 2>::value;
};

template <>
struct Fibonacci<0> {
    static constexpr int value = 0;
};

template <>
struct Fibonacci<1> {
    static constexpr int value = 1;
};

int main() {
    constexpr int result = Fibonacci<5>::value; // 编译时计算斐波那契数列的第 5 项
    return 0;
}

在这个例子中,Fibonacci 结构体使用模板的特化和递归展开在编译期间计算斐波那契数列的值。这使得代码更加高效,因为在运行时不需要进行递归计算。

常见的元编程技术包括:

  • constexpr:用于编译期计算
  • 模板:用来进行静态多态
  • 递归:编译期递归计算
  • SFINAE:子语句失败不报错
  • 模板特化:自定义模板实现

但是元编程并不必须包含全部这些技术,其核心只是发生在编译期。

例如,可以只使用constexpr,不用递归和模板:

constexpr int factorial(int n) {
  return n <= 1 ? 1 : (n * factorial(n-1)); 
}

int main() {
  int x = factorial(5); // 120
}

这依然是编译期计算,但没有用到模板、特化等技术。

又例如可以只用模板,不用递归和constexpr:

template<int N>
struct Factorial {
  static const int value = N * Factorial<N-1>::value;
};

template<> 
struct Factorial<0> {
  static const int value = 1;
};

int main() {
  int x = Factorial<5>::value; // 120
}

元编程的手段很丰富,可以灵活组合。

泛型编程和元编程都是在编译时运行的,它们都是 C++ 中的编译期特性,即在编译阶段进行处理而不是运行时。这两种技术在编译时对代码进行处理,以实现代码的泛化、优化和生成更复杂的代码结构。

  1. 泛型编程:
    • 泛型编程是指在编写代码时不指定具体的类型,而使用参数化的类型,使得代码可以适用于不同类型的数据。在 C++ 中,泛型编程主要通过模板来实现。
    • 泛型编程在编译时进行类型推导和实例化,编译器会根据模板参数的类型生成对应的代码,从而实现代码的通用性和复用性。
  2. 元编程:
    • 元编程是一种在编译期间进行计算和代码操作的技术,它允许通过模板的特性在编译时生成代码。在 C++ 中,元编程主要通过模板元编程(Template Metaprogramming,TMP)来实现。
    • 元编程在编译时进行模板实例化和递归展开,通过在模板实例化过程中进行计算和条件判断来生成代码。元编程的目的通常是实现优化、生成复杂的代码结构、在编译时进行条件选择等。

因为泛型编程和元编程都在编译阶段进行处理,所以它们不会产生运行时的额外开销。这使得泛型编程和元编程成为 C++ 中非常强大的编程技术,可以在编译期间实现高度灵活和高效的代码。