cpp17关键新增特性理解

发布时间 2023-07-20 16:23:36作者: 非法关键字

折叠表达式

折叠表达式(Fold Expression)是C++17标准中引入的一个特性,它是一种用于处理可变参数模板展开的简洁语法。折叠表达式允许在编译时对参数包中的参数进行折叠操作,从而产生一个单一的值。这种特性在处理模板元编程和可变参数模板时非常有用,可以显著简化代码并提高代码的可读性。

折叠表达式的一般形式如下:

(expression op ... op)

其中,expression 是一个表达式,op 是折叠操作符,它可以是二元运算符(例如+*&& 等)或者逗号运算符 ,

折叠表达式可以应用在多种情况下,例如求和、求积、逻辑与/或操作等。下面是一些使用折叠表达式的示例:

  1. 求和:
template<typename... Args>
int sum(Args... args) {
    return (args + ...);
}

int result = sum(1, 2, 3, 4, 5); // 结果为15(1 + 2 + 3 + 4 + 5)
  1. 求积:
template<typename... Args>
int product(Args... args) {
    return (args * ...);
}

int result = product(1, 2, 3, 4, 5); // 结果为120(1 * 2 * 3 * 4 * 5)
  1. 逻辑与:
template<typename... Args>
bool all_true(Args... args) {
    return (args && ...);
}

bool result = all_true(true, true, true, false); // 结果为false(true && true && true && false)
  1. 逻辑或:
template<typename... Args>
bool any_true(Args... args) {
    return (args || ...);
}

bool result = any_true(false, false, true, false); // 结果为true(false || false || true || false)

在折叠表达式中,expression 表达式可以使用括号来明确优先级,以确保正确的计算顺序。折叠表达式的展开顺序是从左到右。需要注意的是,在使用逗号运算符的折叠表达式中,右侧的表达式不会产生效果,因为它们不会参与最终的结果。

结构化绑定

结构化绑定(Structured Binding)是C++17标准引入的一个特性,它允许将复杂的数据结构(如tuple、pair、数组、类对象等)中的成员绑定到多个变量中,以一种结构化和直观的方式访问数据成员。通过结构化绑定,可以避免使用临时变量或者使用索引来访问数据结构中的元素,从而使得代码更加简洁和易读。

结构化绑定使用auto关键字配合中括号 [] 来声明绑定变量,形式如下:

auto [var1, var2, ...] = expression;

其中,var1, var2, ... 是变量名,expression 是要解构的数据结构。解构过程会将expression中的成员按顺序绑定到相应的变量上。

以下是结构化绑定的一些示例:

  1. 使用结构化绑定访问tuple元素:
#include <tuple>
#include <iostream>

int main() {
    std::tuple<int, double, std::string> data = {42, 3.14, "Hello"};

    auto [num, value, message] = data;

    std::cout << num << std::endl;      // 输出:42
    std::cout << value << std::endl;    // 输出:3.14
    std::cout << message << std::endl;  // 输出:"Hello"

    return 0;
}
  1. 使用结构化绑定访问数组元素:
#include <iostream>

int main() {
    int arr[] = {10, 20, 30};

    auto [a, b, c] = arr;

    std::cout << a << std::endl; // 输出:10
    std::cout << b << std::endl; // 输出:20
    std::cout << c << std::endl; // 输出:30

    return 0;
}
  1. 使用结构化绑定遍历map:
#include <map>
#include <iostream>

int main() {
    std::map<std::string, int> myMap = {{"apple", 1}, {"banana", 2}, {"orange", 3}};

    for (const auto& [key, value] : myMap) {
        std::cout << "Key: " << key << ", Value: " << value << std::endl;
    }

    return 0;
}

结构化绑定提供了一种简洁而直观的方式来访问复杂数据结构的成员,它使得代码更加简洁易读,特别是在处理含有多个成员的数据结构时,尤其有用。

inline static

inline static 是C++中对函数和变量同时使用的关键字组合。它结合了两个关键字的特性,分别是:

  1. inline: 这个关键字用于告诉编译器在编译时尝试将函数或代码段直接插入到调用它的地方,而不是生成一个函数调用。这样做有助于减少函数调用的开销,特别是对于较小的函数,同时也可以提高程序的执行速度。然而,inline 只是一个建议,编译器不一定会将函数内联,具体是否内联由编译器决定。
  2. static: 这个关键字用于指示一个变量或函数在编译单元内具有静态生命周期,也就是说,它在程序执行期间只会初始化一次,并且在整个程序的生命周期内存在。对于全局变量和函数,使用 static 使得它们的作用域限制在当前的编译单元内,即使在其他文件中也不能直接访问。

inlinestatic 结合使用时,它们各自的特性相互叠加,产生了以下效果:

  1. inline static 函数:这种情况下,函数会被建议在调用点内部进行内联,并且函数在当前编译单元内具有静态生命周期。也就是说,对于每个使用 inline static 函数的编译单元,都将有一个对应的静态函数实例,并且这些实例在整个程序的生命周期内只初始化一次。
  2. inline static 变量:这种情况下,变量会在当前编译单元内具有静态生命周期,并且对于其他编译单元是不可见的,即使使用了相同的名称。每个包含该变量的编译单元都会有一个对应的静态变量实例,它们互相独立且不共享状态。

在实际使用中,inline static 组合通常用于在头文件中定义函数和变量,以便在多个编译单元中使用,同时避免引起符号重定义错误。这样可以确保每个编译单元都拥有自己的静态实例,避免了在链接时出现重复定义的问题。

optional

在 C++17 中,std::optional 是一个非常有用的特性,它是一个模板类,用于表示可能包含或者不包含值的情况。std::optional 提供了一种安全且语义清晰的方式来处理可能为空的值,避免了使用空指针或特殊值来表示缺失值的问题。

std::optional 的用法类似于指针,但是它可以自动处理空值的情况,无需手动进行空值检查。它可以用于代替可能返回空值的函数、容器中可能存在空值的情况,或者简化数据处理中的空值检查。

以下是 std::optional 的一些用法示例:

  1. 代替可能返回空值的函数:
#include <optional>
#include <iostream>

std::optional<int> divide(int a, int b) {
    if (b != 0) {
        return a / b;
    } else {
        return std::nullopt; // 表示空值
    }
}

int main() {
    auto result = divide(10, 2);
    if (result) {
        std::cout << "Result: " << *result << std::endl; // 输出:Result: 5
    } else {
        std::cout << "Division by zero!" << std::endl;
    }

    auto invalid_result = divide(10, 0);
    if (!invalid_result) {
        std::cout << "Division by zero!" << std::endl; // 输出:Division by zero!
    }

    return 0;
}
  1. 作为容器中可能存在空值的元素:
#include <optional>
#include <vector>
#include <iostream>

int main() {
    std::vector<std::optional<int>> data = {1, 2, std::nullopt, 4};

    for (const auto& element : data) {
        if (element) {
            std::cout << *element << std::endl; // 输出:1, 2, 4
        } else {
            std::cout << "Empty element!" << std::endl; // 输出:Empty element!
        }
    }

    return 0;
}

std::optional 还提供了一些成员函数,如 value() 可以获取值,但要注意在调用 value() 之前最好先检查 std::optional 是否包含值,否则可能会导致未定义行为。还有其他一些成员函数,如 has_value() 用于检查是否包含值,reset() 用于清空 std::optional 中的值等。

通过使用 std::optional,可以更加安全地处理可能为空的值,并且使代码更加简洁和易读。

any

在 C++17 中,std::any 是一个非常有用的特性,它是一个模板类,用于在运行时保存任意类型的值,并且可以在需要时动态获取保存的值。std::any 提供了一种通用的方式来保存和处理未知类型的数据,类似于一个类型安全的、类型推断的 void*

使用 std::any 可以避免在处理未知类型数据时的类型转换和类型检查,它提供了更加安全和便捷的方式来保存和获取数据。

以下是 std::any 的一些用法示例:

  1. 保存不同类型的值:
#include <any>
#include <iostream>
#include <string>

int main() {
    std::any data;

    data = 42; // 保存一个整数
    std::cout << std::any_cast<int>(data) << std::endl; // 输出:42

    data = 3.14; // 保存一个浮点数
    std::cout << std::any_cast<double>(data) << std::endl; // 输出:3.14

    data = std::string("Hello"); // 保存一个字符串
    std::cout << std::any_cast<std::string>(data) << std::endl; // 输出:"Hello"

    return 0;
}
  1. 使用 std::any 保存未知类型的值:
#include <any>
#include <iostream>
#include <typeinfo>

void print_type(const std::any& data) {
    if (!data.has_value()) {
        std::cout << "Empty data!" << std::endl;
    } else {
        std::cout << "Data type: " << data.type().name() << std::endl;
    }
}

int main() {
    std::any data;

    print_type(data); // 输出:Empty data!

    data = 42;
    print_type(data); // 输出:Data type: i

    data = 3.14;
    print_type(data); // 输出:Data type: d

    data = "Hello";
    print_type(data); // 输出:Data type: PKc (表示一个指向字符的指针)

    return 0;
}

在使用 std::any 时,需要注意以下几点:

  • 使用 std::any 存储值时,需要确保保存的值不会在 std::any 的生命周期内被销毁,否则在尝试获取值时可能会导致未定义行为。
  • 在获取 std::any 中的值时,最好使用 std::any_cast 函数进行类型安全的转换。如果尝试获取的类型与实际类型不匹配,会抛出 std::bad_any_cast 异常。
  • std::any 不适合频繁地保存和获取数据,因为它需要在运行时进行类型检查,可能会有一定的性能开销。

通过使用 std::any,可以更加灵活地处理未知类型的数据,并且避免了类型转换和类型检查的繁琐操作。

fallthrough、nodiscard、maybe_unused

在 C++17 中,fallthroughnodiscardmaybe_unused 都是一种用于修饰代码的属性或属性标识符。

  1. fallthrough:它是一个注释标识符,用于在 switch 语句中明确表达意图,告诉编译器故意让控制流穿透到下一个 case 标签,而不进行隐式的 break。它可以帮助开发者在编写 switch 语句时显式地标识不使用 break 语句的情况,以避免出现意外行为。使用 fallthrough 标识符时,需要确保它的前面是一个 case 标签,否则会导致编译错误。
switch (x) {
    case 1:
        std::cout << "Case 1" << std::endl;
        [[fallthrough]]; // 告诉编译器穿透到下一个 case 标签
    case 2:
        std::cout << "Case 2" << std::endl;
        break;
    default:
        std::cout << "Default Case" << std::endl;
}
  1. nodiscard:它是一个属性标识符,用于告诉编译器需要注意函数或类型的返回值,避免出现未使用返回值而导致的警告。将 nodiscard 放在函数或类型的声明前,可以提示开发者注意检查函数的返回值,避免因为忽略返回值而引发潜在的错误。
[[nodiscard]] int func() {
    return 42;
}

int main() {
    func(); // 编译器可能会给出警告,提示函数的返回值未被使用
    return 0;
}
  1. maybe_unused:它也是一个属性标识符,用于告诉编译器允许变量或实体未被使用,从而避免因为未使用变量而产生的警告。有时,某些变量在调试阶段可能会被暂时保留,但在发布版本中可能没有实际用途,此时可以使用 maybe_unused 来避免无关的警告。
void func([[maybe_unused]] int x) {
    // x 可能在调试阶段被保留,但在发布版本中可能没有实际用途
}

通过使用这些属性标识符,可以更好地指导编译器进行优化和提供警告信息,从而帮助开发者编写更安全、高效和清晰的 C++ 代码。