简介

  • C++编程技巧

C 位操作 获取比特位中指定位置的值

在 C 语言中,你可以使用位操作来获取整数中特定位的值。假设你有一个整数(intunsigned int),并且你想要获取它的第 n 个比特位(从0开始计数,最低位为第0位)的值,可以使用以下步骤:

获取指定比特位的值

假设我们有一个整数 num,想要获取它的第 n 位的值。我们可以通过以下步骤实现:

  1. 右移:将整数右移 n 位,使得我们感兴趣的位移动到最低位。
  2. 按位与:对右移后的结果与 1 进行按位与操作,提取最低位的值(即我们需要的第 n 位的值)。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main() {
    unsigned int num = 0b10101100; // 示例整数
    int n = 5; // 要获取的比特位位置

    // 获取第 n 位的值
    unsigned int bit_value = (num >> n) & 1;

    printf("The value of bit %d is: %u\n", n, bit_value);

    return 0;
}

代码解释

  • num >> n:将 num 右移 n 位。这样,第 n 位就移动到了最低位。
  • (num >> n) & 1:使用按位与操作符 & 来提取最低位的值。如果最低位是 1,结果是 1;如果是 0,结果是 0

示例输出

如果 num = 0b10101100,并且 n = 5,那么第 5 位是 1,因此输出将是:

1
The value of bit 5 is: 1

通过这种方式,你可以获取整数中任意比特位的值。

C++ 将两个uint8_t的数据拼接成一个uint16_t的数据

在C++中,如果你有两个 uint8_t 类型的数据,并且你想将它们拼接成一个 uint16_t 类型的数据,可以通过位移和按位或操作来实现。通常,这两个 uint8_t 数据分别作为拼接后的高位和低位。

示例代码

假设你有两个 uint8_t 类型的变量 high_bytelow_byte,你可以将它们拼接成一个 uint16_t 类型的变量 result,其中 high_byte 是高 8 位,low_byte 是低 8 位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <cstdint>

int main() {
    uint8_t high_byte = 0xAB;  // 高位字节,例如 0xAB
    uint8_t low_byte = 0xCD;   // 低位字节,例如 0xCD

    // 拼接成一个uint16_t
    uint16_t result = (static_cast<uint16_t>(high_byte) << 8) | low_byte;

    std::cout << std::hex << "Result: 0x" << result << std::endl;

    return 0;
}

代码解释

  • static_cast<uint16_t>(high_byte) << 8:首先,将 high_byte 转换为 uint16_t 类型,然后将它左移 8 位,使其成为 result 的高位。
  • | low_byte:使用按位或操作符 |low_byte 添加到 result 的低 8 位。

示例输出

如果 high_byte = 0xABlow_byte = 0xCD,那么拼接后的 result 是:

1
Result: 0xABCD

这个结果表示 high_byte 成为 result 的高 8 位,而 low_byte 成为 result 的低 8 位。

C++ 将整数输出二进制

在C++中,将整数以二进制格式输出有多种方法。可以使用手动位操作实现,也可以利用标准库中的函数来完成。下面是几种方法来实现这一功能。

方法1:使用位操作手动输出二进制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <bitset>

int main() {
    unsigned int num = 29; // 示例整数

    std::cout << "Binary representation of " << num << " is: ";

    for (int i = 31; i >= 0; i--) {
        std::cout << ((num >> i) & 1);
    }

    std::cout << std::endl;

    return 0;
}

代码解释

  • num >> i:将 num 右移 i 位,使得我们感兴趣的位移动到最低位。
  • ((num >> i) & 1):提取最低位的值,并将其输出。

在这个例子中,假设 num = 29,二进制输出为:

1
Binary representation of 29 is: 00000000000000000000000000011101

方法2:使用 bitset

C++ 标准库提供了 std::bitset,可以方便地将整数转换为二进制表示并输出。

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <bitset>

int main() {
    unsigned int num = 29; // 示例整数

    std::bitset<32> binary(num);

    std::cout << "Binary representation of " << num << " is: " << binary << std::endl;

    return 0;
}

代码解释

  • std::bitset<32>:创建一个大小为32位的 bitset,并用 num 的值来初始化它。
  • binarybitset 会自动将整数转换为二进制格式并存储。

输出结果同样会是:

1
Binary representation of 29 is: 00000000000000000000000000011101

方法3:模板实现支持不同位数

如果你想实现一个可以处理任意位数的模板函数,可以使用以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <bitset>

template <typename T>
void printBinary(T num) {
    std::bitset<sizeof(T) * 8> binary(num);
    std::cout << "Binary representation: " << binary << std::endl;
}

int main() {
    unsigned int num = 29; // 示例整数
    printBinary(num);

    return 0;
}

代码解释

  • sizeof(T) * 8:计算类型 T 的位数,并用 bitset 表示。
  • printBinary:模板函数可以处理不同类型的整数(uint8_t, uint16_t, int, 等)。

这会自动根据输入的整数类型调整输出的二进制位数。对于 num = 29,输出将是:

1
Binary representation: 00000000000000000000000000011101

这些方法都能有效地将整数转换为二进制并输出。你可以根据需求选择最适合你的方法。

const/constexpr

  • const一般的用法就是修饰变量、引用、指针,修饰之后它们就变成了常量,需要注意的是const并未区分出编译期常量和运行期常量,并且const只保证了运行时不直接被修改。
  • 一般的情况,const 也就简单这么用一下,const 放在左边,表示常量:
    • const int x = 100; // 常量
    • const int& rx = x; // 常量引用
    • const int* px = &x; // 常量指针
  • 给变量加上const之后就成了“常量”,只能读、不能修改,编译器会检查出所有对它的修改操作,发出警告,在编译阶段防止有意或者无意的修改。这样一来,const常量用起来就相对安全一点。在设计函数的时候,将参数用 const 修饰的话,可以保证效率和安全。
  • 除此之外,const 还能声明在成员函数上,const 被放在了函数的后面,表示这个函数是一个“常量”,函数的执行过程是 const 的,不会修改成员变量。
  • 此外,const还有下面这种与指针结合的比较绕的用法: ```cpp int a = 1; const int b = 2; const int* p = &a; int const* p1 = &a;

// *p = 2; // error C3892: “p”: 不能给常量赋值 p = &b; // *p1 = 3; // error C3892: “p1”: 不能给常量赋值 p1 = &b;

int* const p2 = &a; //p2 = &b; // error C2440: “=”: 无法从“const int ”转换为“int *const ” *p2 = 5; const int const p3 = &a;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+ const int 与 int const并无很大区别,都表示: 指向常量的指针,可以修改指针本身,但不能通过指针修改所指向的值。
+ 而对于int *const,则是表示:一个常量指针,可以修改所指向的值,但不能修改指针本身。
+ const int* const 表示一个不可修改的指针,既不能修改指针本身,也不能通过指针修改所指向的值。
+ 总之,const默认与其左边结合,当左边没有任何东西则与右边结合。


+ constexpr,表面上看,constexpr不仅是const,而且在编译期间就已知,这种说法并不全面,当它应用在函数上时,就跟它名字有点不一样了。使用constexpr关键字可以将对象或函数定义为在编译期间可求值的常量,这样可以在编译期间进行计算,避免了运行时的开销
+ constexpr对象 必须在编译时就能确定其值,并且通常用于基本数据类型。例如:
  + constexpr int MAX_SIZE = 100; // 定义一个编译时整型常量
  + constexpr double PI = 3.14159; // 定义一个编译时双精度浮点型常量
+ const和constexpr变量之间的主要区别在于变量的初始化,const可以推迟到运行时,constexpr变量必须在编译时初始化。const 并未区分出编译期常量和运行期常量,并且const只保证了运行时不直接被修改,而constexpr是限定在了编译期常量。简而言之,所有constexpr对象都是const对象,而并非所有的const对象都是constexpr对象。
+ 当变量具有字面型别(literal type)(这样的型别能够持有编译期可以决议的值)并已初始化时,可以使用constexpr来声明该变量。如果初始化由构造函数执行,则必须将构造函数声明为constexpr.
+ 当满足这两个条件时,可以声明引用constexpr:引用的对象由常量表达式初始化,并且在初始化期间调用的任何隐式转换也是常量表达式。
+ constexpr变量或函数的所有声明都必须具有constexpr说明符。
```cpp
constexpr float x = 42.0;
constexpr float y{108};
constexpr float z = exp(5, 3);
constexpr int i; // Error! Not initialized
int j = 0;
constexpr int k = j + 1; //Error! j not a constant expression
  • constexpr函数 是指能够在编译期间计算结果的函数。它们的参数和返回值类型必须是字面值类型,并且函数体必须由单个返回语句组成。例如:
    1
    2
    3
    
    constexpr int square(int x) {
     return x * x;
     }
    
  • constexpr int result = square(5); // 在编译期间计算结果,result 的值为 25
  • 使用 constexpr 可以提高程序的性能和效率,因为它允许在编译期间进行计算,避免了运行时的计算开销。同时,constexpr 还可以用于指定数组的大小、模板参数等场景,提供更灵活的编程方式。
  • 对constexpr函数的理解:
    • constexpr函数可以用在要求编译器常量的语境中。在这样的语境中,如果你传给constexpr函数的实参值是在编译期已知的,则结果也会在编译期间计算出来。如果任何一个实参值在编译期间未知,则代码将无法通过编译。
    • 在调用constexpr函数时,若传入的值有一个或多个在编译期间未知,则它的运作方式和普通函数无异,也就是它也是在运行期执行结果的计算。也就是说,如果一个函数执行的是同样的操作,仅仅应用语境一个是要求编译期常量,一个是用于所有其他值的话,那就不必写两个函数。constexpr函数就可以同时满足需求。 ```cpp constexpr float exp(float x, int n) { return n == 0 ? 1 : n % 2 == 0 ? exp(x * x, n / 2) : exp(x * x, (n - 1) / 2) * x; }

constexpr auto x = 5; constexpr auto n = 3; constexpr int result = exp(x, n); // ok, 前面加上constexpr,进行编译期间求值,单步调试根本进不去

int xx = 4; int nn = 3; //constexpr int result2 = exp(xx, nn); // error C2131: 表达式的计算结果不是常数 int result3 = exp(xx, nn); // ok, 这里作为普通函数来使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
+ 比起非constexpr对象或constexpr函数而言,constexpr对象或是constexpr函数可以用在一个作用域更广的语境中。只要有可能使用constexpr,就使用它吧。

## 内存泄漏

+ 尤其是在跨平台开发的时候更加要注意这类隐晦的异常问题,Effective C++中也提到了,要以独立语句将new对象存储于智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的内存泄漏。

## std::async真的异步吗

+ std::async是C++11开始支持多线程时加入的同步多线程构造函数,其弥补了std::thread没有返回值的问题,并加入了更多的特性,使得多线程更加灵活
+ 顾名思义,std::async是一个函数模板,它将函数或函数对象作为参数(称为回调)并异步运行它们,最终返回一个std::future,它存储std::async()执行的函数对象返回的值,为了从中获取值,程序员需要调用其成员 future::get.
+ 那std::async一定是异步执行吗?先来看段代码:
```cpp
int calculate_sum(const std::vector<int>& numbers) {
    std::cout << "Start Calculate..." << std::endl; // (4)
    int sum = 0;
    for (int num : numbers) {
        sum += num;
    }
    return sum;
}
int main() {
    std::vector<int> numbers = { 88, 101, 56, 203, 72, 135 };
    std::future<int> future_sum = std::async(calculate_sum, numbers);
    std::cout << "Other operations are in progress..." << std::endl; // (1)
    int counter = 1;
    while (counter <= 1000000000) {
        counter++;
    }
    std::cout << "Other operations are completed." << std::endl; // (2)
    // 等待异步任务完成并获取结果
    int sum = future_sum.get();
    std::cout << "The calculation result is:" << sum << std::endl; // (3)
    return 0;
}
  • 执行完(1) (2), 然后再(4)(3), 说明是真正调用std::future<>::get()才去执行的,如果没有调用get,那么就一直不会执行。

  • std::async是否异步受参数控制的,其第一个参数是启动策略,它控制 std::async 的异步行为。可以使用 3 种不同的启动策略创建 std::async ,即:
    • std::launch::async 它保证异步行为,即传递的函数将在单独的线程中执行
    • std::launch::deferred 非异步行为,即当其他线程将来调用get()来访问共享状态时,将调用函数
    • std::launch::async std::launch::deferred 它是默认行为。使用此启动策略,它可以异步运行或不异步运行,具体取决于系统上的负载,但我们无法控制它
  • 如果我们不指定启动策略,其行为类似于std::launch::async std::launch::deferred. 也就是不一定是异步的。
  • Effective Modern C++ 里面也提到了,如果异步执行是必须的,则指定std::launch::async策略。

sizeof & strlen

  • 相信大家都有过这样的经历,在项目中使用系统API或者与某些公共库编写逻辑时,需要C++与C 字符串混写甚至转换,在处理字符串结构体的时候就免不了使用sizeof和strlen,这俩看着都有计算size的能力,有时候很容易搞混淆或者出错。

  • sizeof 是个操作符,可用于任何类型或变量,包括数组、结构体、指针等, 返回的是一个类型或变量所占用的字节数; 在编译时求值,不会对表达式进行求值。
  • strlen 是个函数,只能用于以 null 字符结尾的字符串,返回的是一个以 null 字符(’\0’)结尾的字符串的长度(不包括 null 字符本身),且在运行时才会计算字符串的长度。

  • 需要注意的是,使用 sizeof 操作符计算数组长度时需要注意数组元素类型的大小。例如,对于一个 int 类型的数组,使用 sizeof 操作符计算其长度应该为 sizeof(array) / sizeof(int)。而对于一个字符数组,使用strlen函数计算其长度应该为 strlen(array)。
    1
    2
    
    char str[] = "hello";
    char *p = str;
    
  • 此时,用sizeof(str)得到的是6,因为hello是5个字符,系统储存的时候会在hello的末尾加上结束标识\0,一共为6个字符;
  • 而sizeof(p)得到的却是4,它求得的是指针变量p的长度,在32位机器上,一个地址都是32位,即4个字节。
    • 用sizeof(p)得到的是1,因为p定义为char,相当于一个字符,所以只占一个字节
    • 用strlen(str),得到的会是5,因为strlen求得的长度不包括最后的\0。
    • 用strlen(p),得到的是5,与strlen(str)等价。
  • 上面的是sizeof和strlen的区别,也是指针字符串和数组字符串的区别。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    const char* src = "hello world";
    char* dest = NULL;
    int len = strlen(src); // 这里很容易出错,写成sizeof(src)就是求指针的长度,即4
    dest = (char*)malloc(len + 1); // 这里很容易出错,写成len
    char* d = dest;
    const char* s = &src[len - 1]; // 这里很容易出错,写成len
    while (len-- != 0) {
       *d++ = *s--;
    }
    *d = '\0'; // 这句很容易漏写
    printf("%sIn", dest);
    free(dest);
    

std::shared_ptr线程安全

  • 对shared_ptr相信大家都很熟悉,但是一提到是否线程安全,可能很多人心里就没底了,借助本节,对shared_ptr线程安全方面的问题进行分析和解释。shared_ptr的线程安全问题主要有两种:
    1. 引用计数的加减操作是否线程安全;
    2. shared_ptr修改指向时是否线程安全
  • 引用计数
    • shared_ptr中有两个指针,一个指向所管理数据的地址,另一个指向执行控制块的地址。
    • 执行控制块包括对关联资源的引用计数以及弱引用计数等。在前面我们提到shared_ptr支持跨线程操作,引用计数变量是存储在堆上的,那么在多线程的情况下,指向同一数据的多个shared_ptr在进行计数的++或–时是否线程安全呢?
    • 引用计数在STL中的定义如下:
      1
      2
      
      _Atomic_word _M_use_count;   // #shared
      _Atomic_word _M_weak_count;  // #weak + (#shared != 0)
      
    • 当对shared_ptr进行拷贝时,引入计数增加,实现如下: ```cpp template <> inline bool _Sp_counted_base<_S_atomic>::_M_add_ref_lock_nothrow() noexcept { // Perform lock-free add-if-not-zero operation. _Atomic_word __count = _M_get_use_count(); do { if (__count == 0) return false; // Replace the current counter value with the old value + 1, as // long as it's not changed meanwhile. } while (!__atomic_compare_exchange_n(&_M_use_count, &__count, __count + 1, true, __ATOMIC_ACQ_REL, __ATOMIC_RELAXED)); return true; }

template <> inline void _Sp_counted_base<_S_single>::_M_add_ref_copy() { ++_M_use_count; }

1
2
3
4
5
6
7
8
9
10
11
  + 对引用计数的增加主要有以下2种方法:_M_add_ref_copy函数,对_M_use_count + 1,是原子操作。_M_add_ref_lock函数,是调用__atomic_compare_exchange_n``实现的``,主要逻辑仍然是_M_use_count + 1,而该函数是线程安全的,和_M_add_ref_copy的区别是对不同_Lock_policy有不同的实现,包含直接加、原子操作加、加锁。
  + 因此我们可以得出结论:在多线程环境下,管理同一个数据的shared_ptr在进行计数的增加或减少的时候是线程安全的,这是一波原子操作。

+ 修改指向
  + 修改指向分为操作同一个shared_ptr对象和操作不同的shared_ptr对象两种

+ 多线程代码操作的是同一个shared_ptr的对象
  + 比如std::thread的回调函数,是一个lambda表达式,其中引用捕获了一个shared_ptr对象
```cpp
shared_ptr<A> sp1 = make_shared<A>();
std::thread td([&sp1] () {....});
  • 又或者通过回调函数的参数传入的shared_ptr对象,参数类型是指针或引用:
    • 指针类型:void fn(shared_ptr<A>* sp) { ... }std::thread td(fn, &sp1);引用类型:void fn(shared_ptr<A>& sp) { ... }std::thread td(fn, std::ref(sp1));
  • 当你在多线程回调中修改shared_ptr指向的时候,这时候确实不是线程安全的。
    1
    2
    3
    4
    5
    6
    7
    
    void fn(shared_ptr<A>& sp) {
    if (..) {
        sp = other_sp;
    } else if (...) {
        sp = other_sp2;
    }
    }
    
  • shared _ptr内数据指针要修改指向,sp原先指向的引用计数的值要减去1,other_sp指向的引用计数值要加1。然而这几步操作加起来并不是一个原子操作,如果多个线程都在修改sp的指向的时候,那么有可能会出问题。比如在导致计数在操作-1的时候,其内部的指向已经被其他线程修改过了,引用计数的异常会导致某个管理的对象被提前析构,后续在使用到该数据的时候触发coredump。当然如果你没有修改指向的时候,是没有问题的。也就是:
    • 同一个shared_ptr对象被多个线程同时读是安全的
    • 同一个shared_ptr对象被多个线程同时读写是不安全的
  • 多线程代码操作的不是同一个shared_ptr的对象
    • 这里指的是管理的数据是同一份,而shared_ptr不是同一个对象,比如多线程回调的lambda是按值捕获的对象。
      1
      
      std::thread td([sp1] () {....});
      
    • 或者参数传递的shared_ptr是值传递,而非引用:
      1
      2
      3
      4
      
      void fn(shared_ptr<A> sp) {
      ...
      }
      std::thread td(fn, sp1);
      
    • 这时候每个线程内看到的sp,他们所管理的是同一份数据,用的是同一个引用计数。但是各自是不同的对象,当发生多线程中修改sp指向的操作的时候,是不会出现非预期的异常行为的。也就是说,如下操作是安全的:
      1
      2
      3
      4
      5
      6
      7
      
      void fn(shared_ptr<A> sp) {
      if (..) {
          sp = other_sp;
      } else if (...) {
          sp = other_sp2;
      }
      }
      
    • 尽管前面我们提到了如果是按值捕获(或传参)的shared_ptr对象,那么该对象是线程安全的,然而话虽如此,但却可能让人误入歧途。因为我们使用shared_ptr更多的是操作其中的数据,对齐管理的数据进行读写,尽管在按值捕获的时候shared_ptr是线程安全的,我们不需要对此施加额外的同步操作(比如加解锁),但是这并不意味着shared_ptr所管理的对象是线程安全的!请注意这是两回事。
    • 最后再来看下std官方手册是怎么讲的:
      1
      
      All member functions (including copy constructor and copy assignment) can be called by multiple threads on different instances of shared_ptr without additional synchronization even if these instances are copies and share ownership of the same object. If multiple threads of execution access the same instance of shared_ptr without synchronization and any of those accesses uses a non-const member function of shared_ptr then a data race will occur; the shared_ptr overloads of atomic functions can be used to prevent the data race.
      
    • 这段话的意思是,shared_ptr 的所有成员函数(包括复制构造函数和复制赋值运算符)都可以由多个线程在不同的 shared_ptr 实例上调用,即使这些实例是副本并且共享同一个对象的所有权。如果多个执行线程在没有同步的情况下访问同一个 shared_ptr 实例,并且这些访问中的任何一个使用了 shared_ptr 的非 const 成员函数,则会发生数据竞争;可以使用shared_ptr的原子函数重载来防止数据竞争。
    • 我们可以得到下面的结论:
      • 多线程环境中,对于持有相同裸指针的std::shared_ptr实例,所有成员函数的调用都是线程安全的。
        • 当然,对于不同的裸指针的 std::shared_ptr 实例,更是线程安全的
        • 这里的 “成员函数” 指的是 std::shared_ptr 的成员函数,比如 get ()、reset ()、operrator->()等
    • 多线程环境中,对于同一个std::shared_ptr实例,只有访问const的成员函数,才是线程安全的,对于非const成员函数,是非线程安全的,需要加锁访问。

对象拷贝

  • 在众多编程语言中C++的优势之一便是其高性能,可是开发者代码写得不好(比如:很多不必要的对象拷贝),直接会影响到代码性能,接下来就讲几个常见的会引起无意义拷贝的场景
  • for循环
    1
    2
    3
    4
    5
    6
    
    std::vector<std::string> vec;
    for(std::string s: vec) {
    }
    // or
    for(auto s: vec) {
    }
    
  • 这里每个string都会被拷贝一次,为避免无意义拷贝可以将其改成:
  • for(const auto& s: vec) 或者 for (const std::string& s: vec)

  • lambda捕获
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    // 获取对应消息类型的内容
    std::string GetRichTextMessageXxxContent(const std::shared_ptr<model::Message>& message,
     const std::map<model::MessageId, std::map<model::UserId, std::string>>& related_user_names,
     const model::UserId& login_userid,
     bool for_message_index) {
     // ...
     // 解析RichText内容
     return DecodeRichTextMessage(message, [=](uint32_t item_type, const std::string& data) {
    std::string output_text;
    // ...
    return output_text;
    });
    }
    
  • 上述代码用于解析获取文本消息内容,涉及到富文本消息的解析和一些逻辑的计算,高频调用,他在解析RichText内容的callback中直接简单粗暴的按值捕获了所有变量,将所有变量都拷贝了一份,这里造成不必要的性能损耗,尤其上面那个std::map。这里可以改成按引用来捕获,规避不必要的拷贝。
  • lambda函数在捕获时会将被捕获对象拷贝,如果捕获的对象很多或者很占内存,将会影响整体的性能,可以根据需求使用引用捕获或者按需捕获:
    • auto func = &a{};
    • auto func = a = std::move(a){}; (限C++14以后)
  • 隐式类型转换
  • 这里在遍历关联容器时,看着是const引用的,心想着不会发生拷贝,但是因为类型错了还是会发生拷贝,std::map 中的键值对是以 std::pair<const Key, T> 的形式存储的,其中key是常量。因此,在每次迭代时,会将当前键值对拷贝到临时变量中。在处理大型容器或频繁遍历时,这种拷贝操作可能会产生一些性能开销,所以在遍历时推荐使用const auto&,也可以使用结构化绑定:for(const auto& [key, value]: map){} (限C++17后)

  • 返回值优化
    • RVO是Return Value Optimization的缩写,即返回值优化,NRVO就是具名的返回值优化,为RVO的一个变种,此特性从C++11开始支持。为了更清晰的了解编译器的行为,这里实现了构造/析构及拷贝构造、赋值操作函数,如下: ```cpp class Widget { public: Widget() { std::cout « “Widget: Constructor” « std::endl; } Widget(const Widget& other) { name = other.name; std::cout « “Widget: Copy construct” « std::endl; } Widget& operator=(const Widget& other) { std::cout « “Widget: Assignment construct” « std::endl; name = other.name; return *this; } ~Widget() { std::cout « “Widget: Destructor” « std::endl; } public: std::string name; };

Widget GetMyWidget(int v) { Widget w; if (v % 2 == 0) { w.name = 1; return w; } else { return w; } } int main(){ const Widget& w = GetMyWidget(2); // (1) Widget w = GetMyWidget(2); // (2) GetMyWidget(2); // (3) return 0; }

1
2
3
4
5
6
7
8
9
+ 针对上面(1)(2)(3)的调用,我之前也是有点迷惑,以为要减少拷贝必须得用常引用来接,但是发现编译器进行返回值优化后(1)(2)(3)运行结果都是一样的,也就是日常开发中,针对函数中返回的临时对象,可以用对象的常引用或者新的一个对象来接,最后的影响其实可以忽略不计的。不过个人还是倾向于对象的常引用来接,一是出于没有优化时(编译器不支持或者不满足RVO条件)可以减少一次拷贝,二是如果返回的是对象的引用时可以避免拷贝。但是也要注意不要返回临时对象的引用。
```cpp
// pb协议接口实现
inline const ::PB::XXXConfig& XXConfigRsp::config() const {
   //...
}
void XXSettingView::SetSettingInfo(const PB::XXConfigRsp& rsp){
 const auto config = rsp.config(); // 内部返回的是对象的引用,这里没有引用来接导致不必要的拷贝
}
  • 当遇到上面这种返回对象的引用时,外部最好也是用对象的引用来接,减少不必要的拷贝。
  • 此外,如果Widget的拷贝赋值操作比较耗时,通常在使用函数返回这个类的一个对象时也是会有一定的讲究的。
    1
    2
    3
    4
    
    // style 1
    Widget func(Args param);
    // style 2
    bool func(Widget* ptr, Args param);
    
  • 上面的两种方式都能达到同样的目的,但直观上的使用体验的差别也是非常明显的:
    • style 1只需要一行代码,而style 2需要两行代码,可能大多数人直接无脑style 1
      1
      2
      3
      4
      5
      
      // style 1
      Widget obj = func(params);
      // style 2
      Widget obj;
      func(&obj, params);
      
  • 但是,能达到同样的目的,消耗的成本却未必是一样的,这取决于多个因素,比如编译器支持的特性、C++语言标准的规范强制性等等。
  • 看起来style 2虽然需要写两行代码,但函数内部的成本却是确定的,只会取决于你当前的编译器,外部即使采用不同的编译器进行函数调用,也并不会有多余的时间开销和稳定性问题。使用style 1时,较复杂的函数实现可能并不会如你期望的使用RVO优化,如果编译器进行RVO优化,使用style 1无疑是比较好的选择。利用好编译器RVO特性,也是能为程序带来一定的性能提升。

迭代器删除

  • 在处理缓存时,容器元素的增删查改是很常见的,通过迭代器去删除容器(vector/map/set/unordered_map/list)元素也是常有的,但这其中使用不当也会存在很多坑
    1
    2
    3
    4
    5
    
    std::vector<int> numbers = { 88, 101, 56, 203, 72, 135 };
    auto it = std::find_if(numbers.begin(), numbers.end(), [](int num) {
      return num > 100 && num % 2 != 0;
    });
    vec.erase(it);
    
  • 上面代码,查找std::vector中大于 100 并且为奇数的整数并将其删除。std::find_if 将从容器的开头开始查找,直到找到满足条件的元素或者遍历完整个容器,并返回迭代器it,然后去删除该元素。但是这里没有判断it为空的情况,直接就erase了,如果erase一个空的迭代器会引发crash。很多新手程序员会犯这样的错误,随时判空是个不错的习惯

  • 删除元素不得不讲下std::remove 和 std::remove_if,用于从容器中移除指定的元素, 函数会将符合条件的元素移动到容器的末尾,并返回指向新的末尾位置之后的迭代器,最后使用容器的erase来擦除从新的末尾位置开始的元素。 ```cpp std::vector<std::string> vecs = { “A”, “”, “B”, “”, “C”, “hhhhh”, “D” }; vecs.erase(std::remove(vecs.begin(), vecs.end(), “”), vecs.end());

// 移除所有偶数元素 vec.erase(std::remove_if(vec.begin(), vec.end(), { return x % 2 == 0; }), vec.end());

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
+ 这里的erase不用判空,其内部实现已经有判空处理。
```cpp
_CONSTEXPR20 iterator erase(const_iterator _First, const_iterator _Last) noexcept(
        is_nothrow_move_assignable_v<value_type>) /* strengthened */ {
    const pointer _Firstptr = _First._Ptr;
    const pointer _Lastptr  = _Last._Ptr;
    auto& _My_data          = _Mypair._Myval2;
    pointer& _Mylast        = _My_data._Mylast;
    // ....
    if (_Firstptr != _Lastptr) { // something to do, invalidate iterators
        _Orphan_range(_Firstptr, _Mylast);
        const pointer _Newlast = _Move_unchecked(_Lastptr, _Mylast, _Firstptr);
        _Destroy_range(_Newlast, _Mylast, _Getal());
        _Mylast = _Newlast;
    }
    return iterator(_Firstptr, _STD addressof(_My_data));
}
  • 此外,STL容器的删除也要小心迭代器失效,先来看个vector、list、map删除的例子:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
    // vector、list、map遍历并删除偶数元素
    std::vector<int> elements = { 1, 2, 3, 4, 5 };
    for (auto it = elements.begin(); it != elements.end();) {
     if (*it % 2 == 0) {
          elements.erase(it++);
      } else {
          it++;
      }
    }
    // Error
    std::list<int> cont{ 88, 101, 56, 203, 72, 135 };
    for (auto it = cont.begin(); it != cont.end(); ) {
      if (*it % 2 == 0) {
          cont.erase(it++);
      } else {
          it++;
      }
    }
    // Ok
     std::map<int, std::string> myMap = { {1, "one"}, {2, "two"}, {3, "three"}, {4, "four"}, {5, "five"} };
    // 遍历并删除键值对,删除键为偶数的元素
    for (auto it = myMap.begin(); it != myMap.end(); ) {
      if (it->first % 2 == 0) {
          myMap.erase(it++);
      } else {
          it++;
      }
    }
    // Ok
    
  • 上面几类容器同样的遍历删除元素,只有vector报错crash了,map和list都能正常运行。其实vector调用erase()方法后,当前位置到容器末尾元素的所有迭代器全部失效了,以至于不能再使用。
  • 迭代器的失效问题:对容器的操作影响了元素的存放位置,称为迭代器失效。迭代器失效的情况:
    • 当容器调用erase()方法后,当前位置到容器末尾元素的所有迭代器全部失效。
    • 当容器调用insert()方法后,当前位置到容器末尾元素的所有迭代器全部失效。
    • 如果容器扩容,在其他地方重新又开辟了一块内存,原来容器底层的内存上所保存的迭代器全都失效。
  • 迭代器失效有三种情况,由于底层的存储数据结构,分三种情况:
    • 序列式迭代器失效,序列式容器(std::vector和std::deque),其对应的数据结构分配在连续的内存中,对其中的迭代器进行insert和erase操作都会使得删除点和插入点之后的元素挪位置,进而导致插入点和删除掉之后的迭代器全部失效。可以利用erase迭代器接口返回的是下一个有效的迭代器。
    • 链表式迭代器失效,链表式容器(std::list)使用链表进行数据存储,插入或者删除只会对当前的节点造成影响,不会影响其他的迭代器。可以利用erase迭代器接口返回的是下一个有效的迭代器,或者将当前的迭代器指向下一个erase(iter++)。
    • 关联式迭代器失效,关联式容器,如map, set,multimap,multiset等,使用红黑树进行数据存储,删除当前的迭代器,仅会使当前的迭代器失效。erase迭代器的返回值为 void(C++11之前),可以采用erase(iter++)的方式进行删除。值得一提的是,在最新的C++11标准中,已经新增了一个map::erase函数执行后会返回下一个元素的iterator,因此可以使用erase的返回值获取下一个有效的迭代器。
  • 在实现上有两种模板,其一是通过 erase 获得下一个有效的 iterator,使用于序列式迭代器和链表式迭代器(C++11开始关联式迭代器也可以使用)
    1
    2
    3
    4
    5
    6
    7
    
    for (auto it = elements.begin(); it != elements.end(); ) {
      if (ShouldDelete(*it)) {
          it = elements.erase(it); // erase删除元素,返回下一个迭代器
      } else {
          it++;
      }
    }
    
  • 其二是,递增当前迭代器,适用于链表式迭代器和关联式迭代器。
    1
    2
    3
    4
    5
    6
    7
    
    for (auto it = elements.begin(); it != elements.end(); ) {
      if (ShouldDelete(*it)) {
          elements.erase(it++); 
      } else {
          it++;
      }
    }
    

c++ 获取当前时间戳 毫秒

在C++中获取当前时间戳(以毫秒为单位)通常需要使用操作系统提供的时间函数。以下是一个使用C++标准库的例子,可以获取当前时间戳(以毫秒为单位):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <chrono>

int main() {
    // 获取当前时间点
    auto now = std::chrono::system_clock::now();

    // 将当前时间点转换为毫秒
    auto ms = std::chrono::time_point_cast<std::chrono::milliseconds>(now);

    // 获取时间戳
    auto timestamp = ms.time_since_epoch().count();

    std::cout << "Current timestamp in milliseconds: " << timestamp << std::endl;

    return 0;
}

这段代码使用了 <chrono> 头文件中的时间库,以及 std::chrono::system_clock 类和 std::chrono::milliseconds 类。它获取当前时间点,然后将其转换为毫秒,并输出时间戳。

C++ std::this_thread::yield()函数后面的语句还会执行吗

std::this_thread::yield() 函数是一个线程库提供的函数,用于提示调度器让出当前线程的执行权,以便其他线程有机会执行。调用 std::this_thread::yield() 后,当前线程会主动让出 CPU 的执行时间片,但它的执行权并不会立即转移到其他线程上。相反,操作系统调度器会在合适的时机(通常是在同一线程队列中的其他线程都执行完毕后)再次调度当前线程。因此,std::this_thread::yield() 后面的语句仍然会执行,只是在稍后的时间点。

考虑以下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <thread>

void foo() {
    std::cout << "Before yield" << std::endl;
    std::this_thread::yield();
    std::cout << "After yield" << std::endl;
}

int main() {
    std::thread t(foo);
    t.join();
    return 0;
}

在这个示例中,foo() 函数中的 std::cout << "Before yield" << std::endl;std::cout << "After yield" << std::endl; 语句都会执行,即使在 std::this_thread::yield(); 被调用后。std::this_thread::yield() 只是提示调度器让出 CPU 的执行时间片,而不是中断线程的执行。

C++ 使用原子操作通知指定线程退出 示例 详解

使用原子操作通知指定线程退出的示例需要借助原子布尔标志(std::atomic<bool>)来控制线程的执行状态。下面是一个详细的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <thread>
#include <atomic>

std::atomic<bool> shouldExit(false);

void worker(int id) {
    while (!shouldExit.load(std::memory_order_acquire)) {
        std::cout << "Worker " << id << " is working..." << std::endl;
        // Simulate some work
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    std::cout << "Worker " << id << " exiting." << std::endl;
}

int main() {
    std::thread t1(worker, 1);
    std::thread t2(worker, 2);
    
    // Simulate some work
    std::this_thread::sleep_for(std::chrono::seconds(3));
    
    // Set shouldExit to true
    shouldExit.store(true, std::memory_order_release);
    
    t1.join();
    t2.join();
    
    return 0;
}

在这个示例中,shouldExit 是一个原子布尔变量,用来控制线程是否应该退出。在 worker 函数中,线程会在循环中检查 shouldExit 变量的值,如果值为 false,则继续执行工作,否则退出循环并退出线程。

main 函数中,首先创建了两个工作线程 t1t2,然后模拟了一些工作。随后,shouldExit 被设置为 true,通知工作线程退出。最后,main 函数等待两个工作线程退出。

通过使用原子操作,我们可以避免使用额外的同步机制(如互斥锁、条件变量等),从而更加高效地控制线程的退出。

C++ 使用条件变量通知指定线程退出 示例 详解

使用条件变量(std::condition_variable)通知指定线程退出的示例可以通过以下步骤实现:

  1. 创建一个标志变量来指示线程是否应该退出。
  2. 使用条件变量来等待通知。
  3. 当需要退出线程时,设置标志变量,并通知等待线程退出。
  4. 等待线程收到通知后退出。

让我们通过一个简单的示例来详细说明这个过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool shouldExit = false;

void worker(int id) {
    std::unique_lock<std::mutex> lock(mtx);
    while (!shouldExit) {
        // Wait until notified or the timeout expires
        cv.wait(lock);
        std::cout << "Worker " << id << " is working..." << std::endl;
        // Simulate some work
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    std::cout << "Worker " << id << " exiting." << std::endl;
}

int main() {
    std::thread t1(worker, 1);
    std::thread t2(worker, 2);
    
    // Simulate some work
    std::this_thread::sleep_for(std::chrono::seconds(3));
    
    {
        // Set shouldExit to true and notify the worker threads to exit
        std::lock_guard<std::mutex> lock(mtx);
        shouldExit = true;
        cv.notify_all();
    }
    
    t1.join();
    t2.join();
    
    return 0;
}

在这个示例中,有两个工作线程(t1t2),它们会等待条件变量 cv 的通知。shouldExit 是一个标志变量,用于指示线程是否应该退出。

main() 函数中,首先创建了两个工作线程,然后模拟了一些工作。随后,shouldExit 被设置为 true,并且通过 cv.notify_all() 通知所有等待的线程退出。最后,main() 函数等待两个工作线程退出。

这样,通过条件变量和标志变量,我们可以控制线程的退出。当需要退出线程时,只需设置标志变量并通知等待的线程即可。

C++ std::thread 线程间通信的几种方式 详解

在线程间通信时,C++ 的 std::thread 提供了几种常用的方式:

  1. 共享内存(Shared Memory)
    • 这是最简单和直接的方法之一,多个线程可以访问同一块内存区域。但要小心并发访问的问题,需要使用诸如互斥锁(mutex)或原子操作等机制来确保线程安全。
  2. 互斥锁(Mutex)
    • 使用 std::mutex 来确保一次只有一个线程可以访问共享资源。线程在访问共享资源之前先锁定互斥锁,访问完毕后再解锁。这确保了对共享资源的排他性访问,防止数据竞争。
  3. 条件变量(Condition Variable)
    • std::condition_variable 可以在多线程间进行通信,一般与互斥锁一起使用。一个线程等待条件变量的某个条件为真,而另一个线程在适当的时候发出通知,告诉等待线程条件已经满足。
  4. 原子操作(Atomic Operations)
    • std::atomic 类型提供了一种线程安全的方式来访问共享数据,它们确保在没有互斥锁的情况下执行单个操作时的线程安全性。原子操作通常用于简单的数据类型(如整数、布尔值等)。
  5. 消息队列(Message Queue)
    • 使用 std::queue 或其他自定义队列结构,不同线程可以通过向队列发送消息来进行通信。这种方式一般需要配合互斥锁或原子操作来确保队列操作的线程安全。
  6. 管道(Pipe)
    • 如果是在不同进程之间进行通信,可以使用管道。在C++中,可以使用 popen() 或者在POSIX环境下使用 pipe() 函数。
  7. 信号量(Semaphore)
    • std::semaphore 或者第三方库提供的信号量机制可以用于控制同时访问某个共享资源的线程数量。
  8. 屏障(Barrier)
    • 屏障允许多个线程在某一点上相互等待,直到所有线程都到达后才继续执行。C++中可以使用 std::barrier(C++20 引入)或者其他第三方库提供的屏障机制。

选择哪种通信方式取决于具体情况,包括线程之间的关系、共享资源的类型、性能需求等。

C++ std::for_each() 函数中lambda捕获局部变量是以什么规则捕获的

在 C++ 中,std::for_each() 函数通常与 lambda 表达式一起使用。当你在 lambda 表达式中捕获局部变量时,捕获方式是由捕获列表来确定的。捕获列表控制了 lambda 表达式访问外部变量的方式。

捕获列表有两种方式:按值捕获和按引用捕获。

  1. 按值捕获: 按值捕获会在 lambda 表达式内部创建外部变量的副本,这样 lambda 表达式内部的代码就可以使用该副本。即使外部变量的值发生变化,lambda 表达式内部也不会受到影响。
1
2
3
4
5
6
7
8
9
10
11
int main() {
    int x = 42;
    std::vector<int> vec = {1, 2, 3, 4, 5};
    
    // Lambda 按值捕获 x
    std::for_each(vec.begin(), vec.end(), [x](int n) {
        std::cout << n + x << std::endl;
    });
    
    return 0;
}
  1. 按引用捕获: 按引用捕获不会创建变量的副本,而是直接引用外部变量。这意味着 lambda 表达式内部的代码可以访问并修改外部变量的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
int main() {
    int x = 42;
    std::vector<int> vec = {1, 2, 3, 4, 5};
    
    // Lambda 按引用捕获 x
    std::for_each(vec.begin(), vec.end(), [&x](int n) {
        x += n;
    });
    
    std::cout << "x: " << x << std::endl; // 输出:x: 57
    
    return 0;
}

在使用 std::for_each() 函数时,你可以根据需要选择按值捕获还是按引用捕获,以确保 lambda 表达式的行为符合预期。

C++ std::vector 指定被赋值容器的范围进行赋值

如果你想要在C++中使用std::vector,并且想要将一个vector的内容赋值给另一个vector的特定范围,你可以使用std::copy算法或者std::vector的迭代器来实现。

下面是使用std::copy算法的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> source = {1, 2, 3, 4, 5};
    std::vector<int> destination(7); // 创建一个有7个元素的目标vector

    // 将source的第2到第4个元素(不包括第4个元素)赋值给destination的第3个位置开始
    std::copy(source.begin() + 1, source.begin() + 4, destination.begin() + 2);

    // 打印destination vector的内容
    std::cout << "destination contains:";
    for (int elem : destination) {
        std::cout << ' ' << elem;
    }
    std::cout << '\n';

    return 0;
}

这段代码会输出:

1
destination contains: 0 0 2 3 4 0 0

在这个例子中,std::copy算法从源vector的第2到第4个元素(不包括第4个元素)复制到目标vector的第3个位置开始。

另一种方法是使用std::vector的迭代器来实现。你可以使用std::vectorassign函数,结合迭代器来指定赋值的范围。

C++ std::thread 和 std::move

在 C++ 中,std::thread 是一个用于创建和管理线程的类,而 std::move 是一个用于移动语义的函数。这两者通常可以结合使用来传递线程的所有权。

在使用 std::thread 时,通常需要注意线程对象的所有权问题。当你想要将一个线程对象所有权从一个变量转移给另一个变量时,你可以使用 std::move 来实现移动语义,而不是复制线程对象。

以下是一个简单的示例,演示了如何使用 std::move 来传递 std::thread 对象的所有权:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <thread>

void threadFunction() {
    std::cout << "Inside thread function\n";
}

int main() {
    // 创建一个线程
    std::thread t1(threadFunction);

    // 将线程对象的所有权移动给新的变量 t2
    std::thread t2 = std::move(t1);

    // t1 不再拥有线程,因此在这里无法join
    // 如果尝试 join t1,则会导致程序崩溃或未定义行为
    // t1.join(); 

    // t2 拥有线程,因此可以 join
    t2.join();

    return 0;
}

在这个示例中,t1 在创建时拥有线程,然后通过 std::move 将其所有权转移到了 t2。现在 t1 不再拥有线程,因此不能调用 join() 方法。相反,t2 现在拥有线程,可以调用 join() 方法。

使用 std::move 可以有效地避免资源的复制,而是直接转移资源的所有权,从而提高了性能并避免不必要的资源浪费。

C++ 表示一个函数不应该出现异常

在C++中,表示一个函数不应该出现异常通常可以通过以下方式来实现:

  1. 使用异常说明(Exception Specification):C++语言允许在函数声明中使用异常说明,指明函数不会抛出任何异常。例如:
1
void myFunction() throw(); // 表示 myFunction 不会抛出任何异常
  1. 使用 noexcept 关键字:C++11 引入了 noexcept 关键字,用于指示函数是否抛出异常。如果函数确实不会抛出异常,应该使用 noexcept 关键字来声明。例如:
1
void myFunction() noexcept; // 表示 myFunction 不会抛出异常
  1. 编写良好的代码并避免抛出异常:在编写代码时,可以通过良好的设计和编码实践来避免抛出异常。这可能涉及到使用异常安全的编程技术,如RAII(资源获取即初始化)等。

需要注意的是,尽管可以使用这些方法来表示函数不应该抛出异常,但并不能完全保证函数不会抛出异常。例如,如果函数调用了不受控制的外部库或操作系统函数,它们可能会引发异常。因此,在编写代码时,应该仔细考虑异常安全性,并尽量避免或者明确处理异常。