c17 新特性 字面量,变量,函数,隐藏转换等

导论

        c17新特性引入了许多新的语法,这些语法特性更加清晰,不像传统语法,语义飘忽不定,比如‘a’你根本不知道是宽字符还是UTF-8 字符。以及测试i++ ++i,最后结果到底是多少。这种问题很大情况是根据编译器的优化进行猜测,不同环境,得出不同结果。而c17对这些问题给出完美的解决方案!

字符字面量

理论

官方给出的语法:

  • 普通字符字面量: 'c-char'

  • UTF-8 字符字面量: u8'c-char'

  • UTF-16 字符字面量: u'c-char'

  • UTF-32 字符字面量: U'c-char'

  • 宽字符字面量: L'c-char'

  • 普通多字符字面量: 'c-char-sequence'

  • 宽字符多字符字面量: L'c-char-sequence'

不太懂??基础薄弱!!

  1. 普通字符字面量:

    • 形式: 'c-char'
    • 表示: 一个单个的普通字符字面量,通常为 8 位 ASCII 字符。
    • 类型: char
  2. UTF-8 字符字面量:

    • 形式: u8'c-char'
    • 表示: 一个单个的 UTF-8 编码的字符字面量。
    • 类型: char8_t (C++20 引入)
  3. UTF-16 字符字面量:

    • 形式: u'c-char'
    • 表示: 一个单个的 UTF-16 编码的字符字面量。
    • 类型: char16_t
  4. UTF-32 字符字面量:

    • 形式: U'c-char'
    • 表示: 一个单个的 UTF-32 编码的字符字面量。
    • 类型: char32_t
  5. 宽字符字面量:

    • 形式: L'c-char'
    • 表示: 一个单个的宽字符字面量。宽字符的大小由实现定义,通常为 16 位或 32 位。
    • 类型: wchar_t
  6. 普通多字符字面量:

    • 形式: 'c-char-sequence'
    • 表示: 一个字符序列,由多个普通字符组成。
    • 类型: 数组类型 char[N],其中 N 为字符序列的长度。
  7. 宽字符多字符字面量:

    • 形式: L'c-char-sequence'
    • 表示: 一个字符序列,由多个宽字符组成。
    • 类型: 数组类型 wchar_t[N],其中 N 为字符序列的长度。

这些不同类型的字符字面量主要有以下区别:

  1. 字符编码:

    • 普通字符为 8 位 ASCII 编码。
    • UTF-8、UTF-16 和 UTF-32 字符分别采用 UTF-8、UTF-16 和 UTF-32 编码。
    • 宽字符的编码由实现定义,通常为 16 位或 32 位。
  2. 表示范围:

    • UTF-8、UTF-16 和 UTF-32 字符可以表示更广泛的字符集,包括非 ASCII 字符。
    • 宽字符的表示范围由实现定义。
  3. 内存占用:

    • 普通字符和 UTF-8 字符占用 1 个字节。
    • UTF-16 字符占用 2 个字节。
    • UTF-32 字符和宽字符占用 4 个字节。

        选择合适的字符类型取决于您的具体需求。对于仅需要处理 ASCII 字符的场景,使用普通字符就足够了。如果需要支持更广泛的字符集,可以考虑使用 UTF-8、UTF-16 或 UTF-32 字符。如果需要与遗留代码兼容,或者需要处理宽字符的场景,则可以使用宽字符。 现在我们可以定义出适合的字符串类型,写出最佳的程序。

实践

普通字符字面量:
char c1 = 'a';
char c2 = '\n';
char c3 = '\x2a'; // 等同于 '*'

UTF-8 字符字面量:
char8_t c1 = u8'a';
// char8_t c2 = u8'¢'; // 错误,¢无法用单个UTF-8码元表示
// char8_t c3 = u8'猫'; // 错误,猫无法用单个UTF-8码元表示
// char8_t c4 = u8'🍌'; // 错误,🍌无法用单个UTF-8码元表示

UTF-16 字符字面量:
char16_t c1 = u'a';
char16_t c2 = u'¢';
char16_t c3 = u'猫';
// char16_t c4 = u'🍌'; // 错误,🍌无法用单个UTF-16码元表示

UTF-32 字符字面量:
char32_t c1 = U'a';
char32_t c2 = U'¢';
char32_t c3 = U'猫';
char32_t c4 = U'🍌';

宽字符字面量:
wchar_t wc1 = L'a';
wchar_t wc2 = L'¢';
wchar_t wc3 = L'猫';
wchar_t wc4 = L'🍌'; // 在Windows上,这可能是非法的

普通多字符字面量:
int mc1 = 'ab'; // 实现定义的值
int mc2 = 'abc'; // 实现定义的值

宽字符多字符字面量:
wchar_t wmc1 = L'ab'; // 实现定义的值

有关变量的新语法

变量的评估顺序

表达式的求值顺序:

  • C++ 标准没有规定表达式中各个子表达式的求值顺序,除了一些特定的情况。

  • 通常编译器可以自由选择求值顺序,只要最终结果与按照左到右的顺序求值一致。

        x = (++x, ++y); 传统cpp并没有标准的定义到底谁先执行,都是由编译器优化,这就会造成环境不同,效果不同,争的你死我活。现在c17是这样解决的。

C++ 标准库中的 std::evaluatestd::as_const 这两个工具函数:

  1. std::evaluate (C++23):

    • 功能: 强制求值一个表达式,确保其副作用按照预期顺序发生。

    • 声明: template<class T> constexpr T&& evaluate(T&& t) noexcept;

    • 使用:

      int x = 0, y = 0;
      x = (++x, ++y); // 存在顺序未定义,可能 x 或 y 先增加
      x = std::evaluate((++x, ++y)); // 确保 x 和 y 按从左到右的顺序增加
    • 作用: 帮助开发者明确表达式的求值顺序,避免由于未定义行为导致的bug。

  2. std::as_const (C++17):

    • 功能: 获取一个表达式的常量引用,避免意外修改。

    • 声明: template<class T> constexpr const T& as_const(T& t) noexcept;

    • 使用:

      std::string s = "hello";
      std::string_view sv = s; // 可能意外修改 s
      std::string_view sv = std::as_const(s); // 确保 sv 只能读取 s 的内容
    • 作用: 帮助开发者强制使用只读引用,防止无意中修改原对象。

        总之, std::evaluatestd::as_const 是 C++ 标准库提供的两个有用的工具函数,前者解决表达式求值顺序的问题,后者解决引用可能被意外修改的问题。开发者可以在需要时使用它们来编写更加健壮的代码。

ifswitch初始化

这种多说无益,直接看示例就明白了。

auto lambda = [](int x) {
    if (int y = x * x; y > 100) {
        return y;
    } else {
        return 0;
    }
};

int result = lambda(11); // result 为 121


auto lambda = [](char c) {
    switch (int x = c; x) {
        case 'a':
            return 1;
        case 'b':
            return 2;
        default:
            return 0;
    }
};

int result = lambda('b'); // result 为 2

 结构化绑定声明 简化代码。

可以看我往期文章,在此简单举个例子

#include <iostream>
#include <tuple>

int main() {
    std::tuple<int, char, std::string> t{42, 'a', "hello"};
    //变量的数量必须与复合类型的元素数量一致。
    auto [x, y, z] = t;
    std::cout << x << ", " << y << ", " << z << '\n'; // 输出: 42, a, hello
    return 0;
}

左值引用和右值引用

可以看我往期文章,就不再赘述,这是重中之重。

constexpr consteval 编译时求值

constexpr 和 consteval 是 C++11 和 C++20 引入的两个关键字,它们都用于在编译时执行计算,但是它们之间有一些区别:

  1. constexpr:

    • constexpr 表示该变量或函数在编译时就可以计算出它的值。
    • constexpr 函数可以在编译时执行,也可以在运行时执行。
    • 如果 constexpr 函数在编译时无法计算出结果,编译器会尝试在运行时执行该函数。
    • constexpr 可以用于变量、函数和类的成员函数。
  2. consteval:

    • consteval 是 C++20 引入的,它比 constexpr 更加严格。
    • consteval 函数必须在编译时就能计算出结果,不能在运行时执行。
    • 如果 consteval 函数在编译时无法计算出结果,编译器会报错。
    • consteval 只能用于函数,不能用于变量或类的成员函数。

举例

constexpr 变量
constexpr int x = 42;
constexpr std::array<int, 3> arr = {1, 2, 3};


constexpr 构造函数
struct Point {
    constexpr Point(int x, int y) : x(x), y(y) {}
    int x, y;
};
constexpr Point p(1, 2); // 在编译时创建 p 对象


consteval关键字
consteval int square(int x) {
    return x * x;
}
constexpr int y = square(3); // 在编译时计算出 y = 9

总的来说:

  • constexpr 是一种"可能在编译时计算"的函数或变量,而 consteval 是一种"必须在编译时计算"的函数。
  • constexpr 提供了更大的灵活性,但 consteval 提供了更严格的编译时计算保证。
  • 开发者应该根据具体需求选择使用 constexpr 还是 consteval如果一个函数在编译时就能计算出结果,使用 consteval 更合适;如果需要在运行时也能正常执行,使用 constexpr 更合适。

任意类型变量 any variant

variant 编译期

std::variant 是 C++17 引入的一个非常有用的类型,它可以表示一个在多个类型之间进行选择的值。它提供了一种安全和高效的方式来处理可能出现的多种类型。

以下是 std::variant 的一些主要特点:

  1. 可以包含多种类型: std::variant 可以存储不同类型的值,这些类型由开发者在定义 std::variant 时指定。

  2. 类型安全: 与使用 void* 或者联合(union)相比, std::variant 提供了更好的类型安全性。它可以在编译时就检查访问是否合法,从而避免运行时错误。

  3. 访问安全: std::variant 提供了多种安全的访问方式,如 std::getstd::visit 等,可以确保在访问时不会出现未定义行为。

  4. 无需手动内存管理: std::variant 会自动管理其包含的值的生命周期,开发者不需要手动分配或释放内存。

any  运行期

std::any 是 C++17 引入的一个很有用的类型,它可以用来存储和传递任意类型的值。它提供了一个安全和高效的方式来处理动态类型的数据。

以下是 std::any 的一些主要特点:

  1. 可以存储任意类型的值: std::any 可以存储任何类型的值,包括基本数据类型、自定义类型、数组、指针等。

  2. 类型安全: 与使用 void* 相比, std::any 提供了更好的类型安全性。它会在运行时检查访问是否合法,从而避免未定义的行为。

  3. 无需手动内存管理: std::any 会自动管理其包含的值的生命周期,开发者不需要手动分配或释放内存。

  4. 支持赋值和拷贝: std::any 支持赋值和拷贝操作,这使得它可以很方便地在代码中传递和存储值。

示例

#include <iostream>
#include <any>
#include <variant>
#include <string>

int main() {
    // 使用 std::any
    std::any anyValue = 42;
    std::cout << "std::any value: " << std::any_cast<int>(anyValue) << std::endl;

    anyValue = std::string("hello");
    std::cout << "std::any value: " << std::any_cast<std::string>(anyValue) << std::endl;




    // 使用 std::variant
    std::variant<int, std::string> variantValue = 42;
    std::cout << "std::variant value: " << std::get<int>(variantValue) << std::endl;

    variantValue = std::string("hello");
    std::cout << "std::variant value: " << std::get<std::string>(variantValue) << std::endl;

    return 0;
}

anyvariant区别

std::anystd::variant 都是 C++17 引入的非常有用的类型,但它们在功能和使用场景上有一些区别:

  1. 存储类型:

    • std::any 可以存储任意类型的值,包括自定义类型、数组、指针等

    • std::variant 只能存储在定义时指定的有限个类型中的一种

  2. 类型安全:

    • std::any 提供了运行时类型检查,通过 std::any_cast 访问时会检查类型是否匹配,不匹配则抛出 std::bad_any_cast 异常。

    • std::variant 则是在编译时就确定了可能存储的类型,通过 std::get 等函数访问时也可以在编译时检查类型是否匹配。

  3. 访问方式:

    • std::any 通过 std::any_cast 进行访问,需要显式指定类型。

    • std::variant 可以使用 std::getstd::visit 等多种方式进行访问。

  4. 使用场景:

    • std::any 适用于需要处理动态类型数据的场景,如插件系统、配置文件解析等

    • std::variant 则更适用于在有限的几种类型之间进行选择的场景,如函数重载、状态机等。

隐藏转换

这个模块可谓是本文的重点,坑点是最多的。

临时对象具体化 抛砖

        在 C++ 中,临时对象的具体化是一个非常重要的概念。临时对象是在表达式求值过程中创建的短暂对象,它们通常会在表达式结束后被销毁。

std::string getStr() {
    return "hello";
}
std::string s = getStr(); // 临时对象被具体化并赋值给s
在这个例子中, getStr() 函数返回一个临时的 std::string 对象,该对象会在 main()
函数中被具体化并赋值给 s。



int x = 1, y = 2;
int z = x + y; // 临时对象被具体化并赋值给z
在这个例子中,x + y 表达式创建了一个临时的 int 对象,该对象会被具体化并赋值给 z。

std::function<int(int)> get_lambda() {
    return [](int x) { return x * x; };
}
auto square_lambda = get_lambda();
int result = square_lambda(5); // 结果是 25

在这个例子中:
1. `get_lambda()` 函数返回一个 lambda 表达式。
2. 这个 lambda 表达式产生了一个临时的 lambda 对象。
3. 这个临时的 lambda 对象被用于初始化 `square_lambda` 变量。
这个临时的 lambda 对象就是通过"临时对象具体化"的过程产生的。编译器会确保这个临时对象的生命周期足够长,以满足初始化 `square_lambda` 的需求。

保证拷贝省略 引玉

        保证拷贝省略(Guaranteed Copy Elision, GCE)并不是 C++17 引入的新特性,它实际上是在 C++11 中引入的。

  C++11中引入了以下几种情况下的拷贝省略:

  1. 返回值优化(RVO):

    • 当函数返回一个局部对象时,编译器可以直接在调用方的位置构造该对象,而无需进行拷贝。
  2. 移动语义优化:

    • 当返回一个临时对象时,编译器可以使用移动构造函数而不是拷贝构造函数
  3. 构造函数参数优化:

    • 当构造函数的参数是一个临时对象时,编译器可以直接在目标位置构造该对象,而无需进行拷贝。

C++17进一步扩展了拷贝省略的范围,增加了以下几种情况:

  1. 无参数构造函数拷贝省略:

    • 当函数返回一个局部对象,且该对象没有参数的构造函数时,编译器可以直接在调用方的位置构造该对象,而无需进行拷贝。
  2. 聚合类型拷贝省略:

    • 当函数返回一个聚合类型(如数组或结构体)的局部对象时,编译器可以直接在调用方的位置构造该对象,而无需进行拷贝。

总的来说,C++11和C++17中的拷贝省略优化可以显著提高程序的性能,减少不必要的拷贝操作。编译器会自动进行这些优化,开发者无需手动干预。

性能优化巅峰   

先举个例子

#include <iostream>
 
class Noisy {
public:
    Noisy() { 
        std::cout << "Noisy object constructed at " << this << '\n'; 
    }

    Noisy(const Noisy& other) {
        std::cout << "Noisy object copy-constructed at " << this << '\n';
    }

    Noisy(Noisy&& other) noexcept {
        std::cout << "Noisy object move-constructed at " << this << '\n';
    }

    Noisy& operator=(const Noisy& other) {
        if (this != &other) {
            std::cout << "Noisy object copy-assigned at " << this << '\n';
        }
        return *this;
    }

    Noisy& operator=(Noisy&& other) noexcept {
        if (this != &other) {
            std::cout << "Noisy object move-assigned at " << this << '\n';
        }
        return *this;
    }

    ~Noisy() {
        std::cout << "Noisy object destructed at " << this << '\n';
    }
};
 
Noisy f(){
    // c11传统调用 创建一个临时 Noisy 对象,然后将其拷贝构造或移动构造到 v 中    
    // c17 编译器可以直接在 v 上构造 Noisy 对象,省略掉不必要的拷贝/移动操作。 称为(since C++17) "保证拷贝省略"
    Noisy v = Noisy();   //注意这里没有任何函数调用!!! 它是怎么做到给返回值给变量v赋值的??
/*
解答:
1.编译器识别到 Noisy v = Noisy(); 是一个直接初始化语句。 
2.它会在 v 的位置直接构造一个 Noisy 对象(Noisy v),而不是先创建一个临时对象,然后再拷贝或移动到 v 中。
3.这个构造过程会调用 Noisy 类的默认构造函数,输出 "Noisy object constructed at 0x[地址]"。
4.最终,v 就直接成为一个 Noisy 类型的对象,不需要经过任何拷贝或移动操作。
*/ 
                
    return v; 
}
 
void g(Noisy arg)
{
    
    std::cout << "&arg = " << &arg << '\n';
}

int main()
{
    // c11  会执行一次拷贝或移动操作,将 v 的内容复制或移动到返回值中。
    //C++17引入命名返回值优化( NRVO)允许编译器直接在返回值位置构造对象(不调用构造函数),避免不必要的拷贝或移动。
    Noisy v = f();  //同理 f()返回值v 如何实现给v初始化的?
   
/*
如果 f() 返回一个临时 Noisy 对象或者返回一个命名的 Noisy 对象(例如一个局部变量),编译器会尝试应用 RVO 和 NRVO 等优化,
尽量避免不必要的拷贝和移动操作。
编译器可以直接在 v 的位置构造这个返回的 Noisy 对象,避免任何拷贝或移动。
*/              
    
    std::cout << "&v = " << &v << '\n';

    g(f());// (since C++17)  "拷贝省略"
}

输出


constructed at 0x7fff1d765096    解释:代码行 Noisy v = Noisy(); 的调用
&v = 0x7fff1d765096
constructed at 0x7fff1d765097    解释:代码行 Noisy v = Noisy(); 的调用
&arg = 0x7fff1d765097

destructed at 0x7fff1d765097
destructed at 0x7fff1d765096

 C++11 和 C++17 在这些优化上的差异:

  1. 临时变量的初始化:

    • C++11 及更早的版本中,Noisy v = Noisy(); 会先创建一个临时 Noisy 对象,然后将其拷贝或移动到 v 中。这会涉及一次构造和一次拷贝/移动操作。
    • C++17 引入了"保证拷贝省略"(Guaranteed Copy Elision, GCE)特性,编译器可以直接在 v 上构造 Noisy 对象,省略掉不必要的拷贝/移动操作。
  2. 函数返回值的优化:

    • C++11 及更早的版本中,return v; 会执行一次拷贝或移动操作,将 v 的内容复制或移动到返回值中。
    • C++17 引入的"命名返回值优化"(Named Return Value Optimization, NRVO)允许编译器直接在返回值位置构造对象,避免不必要的拷贝或移动。
  3. 函数参数的传递:

    • C++11 及更早的版本中,g(f()); 会先创建一个临时 Noisy 对象,然后将其拷贝或移动到 arg 中。
    • C++17 的"拷贝省略"(Copy Elision)特性允许编译器直接在 arg 上构造 Noisy 对象,避免不必要的拷贝或移动。

 上列的例子,应该能让读者更清楚的对c17”保证拷贝省略“进一步的理解,最后我们在整理一下思路,

临时变量的初始化: Noisy v = Noisy()  -> Nosiy v(); 
函数返回值的优化: Noisy v = {Noisy v; return v;} -> Nosiy v(); 
函数参数的传递:


主函数 nrvo优化:
Noisy v = f(); -> 
Noisy v = {Noisy v = Noisy();  return v; }->   //临时变量初始化的优化
Noisy v = {Noisy v();  return v; } -> 
Noisy v = {return v} ->                      //函数返回值的优化
Noisy v = Noisy() ->  
Noisy v;


g(f())
g(Noisy arg = {Noisy v = Noisy();  return v;})
g(Noisy arg =  {Noisy v();  return v;})
g(Noisy  arg )

隐式转换

C++ 隐式转换的内容总结如下:

  1. 隐式转换的顺序:

    • 标准转换序列 - 包括值转换和一些数值转换

    • 用户定义转换 - 通过单参数构造函数或转换函数进行

    • 可能的额外标准转换序列

  2. 值转换:

    • 左值到右值

    • 数组到指针

    • 函数到指针

    • 临时对象化 - 将右值转换为左值

  3. 整数提升:

    • 将小整数类型提升为 int 或 unsigned int

  4. 浮点提升:

    • float 转换为 double

  5. 数值转换:

    • 整数间转换

    • 浮点间转换

    • 浮点和整数间转换

    • 指针间转换

    • 指针到成员间转换

    • 布尔转换

  6. 限定符转换:

    • 在相似类型间添加或删除 const/volatile 限定符

  7. 上下文转换:

    • 在特定上下文中进行的隐式转换,如条件表达式、逻辑运算符等

举例

标准转换序列:
int x = 3.14; // float 到 int 的隐式转换

用户定义转换:
class Rational {
public:
    Rational(int numerator, int denominator = 1) {
        // 用户定义的单参数构造函数
    }
};
Rational r = 5; // int 到 Rational 的隐式转换 5->Rational  因为没加explicit

值转换:
int* p = new int[10]; 
int x = p[0]; // 数组到指针的隐式转换,然后左值到右值的隐式转换

整数提升:
char c = 'a';int i = c; // char 提升到 int

浮点提升:
float f = 3.14f;
double d = f; // float 提升到 double

限定符转换:
const int* p = new int;
int* q = const_cast<int*>(p); // 从 const int* 到 int* 的转换

上下文转换:
if (Rational r = 5) { // Rational 到 bool 的隐式转换
    // ...
}

总结

        总的来说,这篇文章全面介绍了C++17中的一些重要语法和概念变化,对于了解和掌握C++17的新特性很有帮助。文章内容丰富,逻辑清晰,可以作为C++17学习的良好参考资料。

  • 20
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值