0%

C++常用STL技巧

本人原先也是在学校系统学过C,在工作之前对于C++的运用只是停留在”C With std::cout“的阶段,而工作后也没有时间系统阅读C++的大部头书籍,于是本文便作为一个碎片化学习历程的记录,记录一些常用的C++标准库使用技巧。

std::isdigit

判断一个字符是否是数字,通常可以这样实现:

bool IsDigit(char ch) { return ch >= '0' && ch <= '9'; }

也可以直接用std::isdigit
char ch = '8';
if (std::isdigit(ch)) {
// ...
}

std::transform

std::transform有两类操作,一类是一元操作,另一种是二元操作。一元操作接受一个容器的迭代器区间,再次区间内执行参数4的定义函数,将结果存放在参数3指定的迭代器位置。

template <class InputIterator, class OutputIterator, class UnaryOperation>
OutputIterator transform (InputIterator first1, InputIterator last1,
OutputIterator result, UnaryOperation op);

例如,将字符串原地转大写:
std::string str = "I will kick your ass";
std::transform(str.begin(), str.end(), str.begin(), std::toupper); // str: "I WILL KICK YOUR ASS"

std::transform的二元操作版本将接受来自两对迭代器区间,同时遍历两个区间,对两个操作数执行参数5指定的二元操作函数,并将结果设置在参数4定义的区间位置:

template <class InputIterator1, class InputIterator2,
class OutputIterator, class BinaryOperation>
OutputIterator transform (InputIterator1 first1, InputIterator1 last1,
InputIterator2 first2, OutputIterator result,
BinaryOperation binary_op);

例如,实现两个数组的逐项累加:
std::vector<int> vec1 { 1, 2, 3, 4, 5 };
std::list<int> list1 { 5, 4, 3, 2, 1 };
std::transform(vec1.begin(), vec1.end(), list1.begin(), vec1.begin(), std::plus<int>());
// vec1: { 6, 6, 6, 6, 6} list1: {5, 4, 3, 2, 1}

std::to_string

由于C++强类型的约束,不能直接把std::string和数字类型相加。C++11以后可以直接用std::to_string(num)把数字转为std::string

int a = 10;
std::string message = "Num is: ";
message += std::to_string(a); // "Num is: 10"

std::replace

std::replace通过传入一组迭代器区间,将其范围内值等于_Oldval的元素替换为_Newval

template<class _FwdIt,class _Ty> inline
void replace(_FwdIt _First, _FwdIt _Last, const _Ty& _Oldval, const _Ty& _Newval)
{ // replace each matching _Oldval with _Newval
_DEBUG_RANGE(_First, _Last);
_Replace_unchecked(_Unchecked(_First), _Unchecked(_Last),
_Oldval, _Newval);
}
_Replace_unchecked 的源码:

_Replace_unchecked 的源码:
template<class _FwdIt,class _Ty> inline
void _Replace_unchecked(_FwdIt _First, _FwdIt _Last, const _Ty& _Oldval, const _Ty& _Newval)
{ // replace each matching _Oldval with _Newval
for (; _First != _Last; ++_First)
if (*_First == _Oldval)
*_First = _Newval;
}

例如:检查文件路径,将所有斜杠替换成反斜杠:
std::string path = R"(C:\Users\XUranus/Desktop/1.txt)";
std::replace(path.begin(), path.end(), '/', '\\'); // path: C:\Users\XUranus\Desktop\1.txt

如果要替换的元素需要满足非相等的更复杂的逻辑,可以用std::replace_if,和std::replace类似,只是其中的参数3从接受_Oldval改为接受一个匿名函数。

例如:将数组中所有大于100的值置为-1:

std::vector<int> vec = { 10, 104, 20, 67, 300};
std::replace_if(
vec.begin(),
vec.end(),
std::bind(std::greater<int>(), std::placeholder::_1, 100),
-1); // vec: { 10, -1, 20, 67, -1}

std::copy

std::copy(Iterator _first, Iterator _end, Iterator _dst)拷贝[_first, _end)范围中的元素到始于_dst往后的位置。例:

std::vector<int> src = { 1, 2, 3 };
std::vector<int> container(3);
std::copy(src.begin(), src.end(), container.begin());

此处copy只负责复制,不负责申请空间,所以复制前必须有足够的空间。如果container的大小小于输入序列的长度的话,这段代码会导致崩溃(crash)。所以此时引入了std::back_inserter:

std::vector<int> src = { 1, 2, 3 };
std::vector<int> container;
std::copy(src.begin(), src.end(), std::back_inserter(container))

标准库提供的std::back_inserter模板函数很方便,因为它为container返回一个back_insert_iterator迭代器:

// iterator header
template <class _Container>
_NODISCARD _CONSTEXPR20 back_insert_iterator<_Container> back_inserter(_Container& _Cont) noexcept /* strengthened */ {
// return a back_insert_iterator
return back_insert_iterator<_Container>(_Cont);
}

这样,复制的元素都被追加到container的末尾了。(就算container为空也没事)。该迭代器扩展目的容器为每一次复制扩展元素,确保了容器有足够的大小来容纳每个元素。

std::next_permutation

std::next_permutation用于找到数组的下一个排列,常用于获得数组的全排列。

std::vector<int> vec {1, 3, 2, 5, 4};
std::sort(vec.begin(), vec.end());
while (std::next_permutation(vec.begin(), vec.end())) { // return false if vec is in descending sequence
// print current vector ...
}

RAII构造defer

Golang中提供了一个defer关键字用于在函数完成后例机执行一个代码块,常用于执行资源清理任务:

package main

import (
"fmt"
"os"
)

func main() {
// Open the file.
file, err := os.Open("example.txt")
if err != nil {
fmt.Println("Error opening file:", err)
return
}

// Schedule the file to be closed when the surrounding function (main) returns.
defer file.Close()

// Perform file operations here.
// ...

fmt.Println("File operations completed successfully.")
}

这个例子中defer file.Close()用于保证该语句会在main函数返回前执行,从而保证了文件打开句柄使用后被关闭。

C++中可以用RAII机制实现Golang中defer的效果。RAII(Resource Acquisition Is Initialization)是一种C++编程技术,用于在对象的生命周期中自动管理资源。其核心思想是将资源的获取与对象的初始化绑定在一起,将资源的释放与对象的销毁绑定在一起。这种方式可以确保在对象生命周期结束时(例如离开作用域、抛出异常等情况),资源会被自动释放,从而避免资源泄漏。

RAII 在C++中通过构造函数和析构函数实现。构造函数负责初始化对象并获取资源,析构函数负责释放资源。当对象在栈上创建时,其析构函数会在对象离开作用域时自动调用,从而释放资源。C++ STL中的智能指针unique_ptrshared_ptr使用RAII管理指针的释放,unique_locklock_guard使用RAII管理mutex的释放。

C++可以用STL提供的现成的智能指针和lambda函数模拟defer

#include <iostream>
#include <memory>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
const std::string path = "/home/xuranus/example.txt";
const int BUFF_MAX = 1024;
char buff[BUFF_MAX] = { 0 };
int fd = ::open(path.c_str() , O_RDONLY | O_NONBLOCK);
if (fd < 0) {
std::cerr << "file open failed" << std::endl;
return -1;
}

// shared_ptr and lambda function to implement "defer"
std::shared_ptr<void> defer(nullptr, [&](...) {
::close(fd);
std::cout << "file closed" << std::endl;
});

int n = ::read(fd, buff, BUFF_MAX);
if (n < 0) {
return -1;
}
std::string str;
str.assign(buff, n);
std::cout << str << std::endl;
return 0;
}

这种方式实现最简单,就地取材,但是存在以下问题:

  1. shared_ptr会在堆上申请内存,频繁使用容易造成内存的碎片化
  2. 生成的defer对象可以被拷贝,而清理函数只能执行一次

于其滥用智能指针,更好的方案还是自己封装一个defer

// defer.h
#include <iostream>
#include <memory>

template<typename DeferFunc>
class DeferImpl
{
public:
// construct from function
inline DeferImpl(const DeferFunc& deferFunc);
inline DeferImpl(DeferFunc&& deferFunc);

DeferImpl(DeferImpl<DeferFunc>&& deferFunc);

~DeferImpl();
private:
// delete copy and assignment constructor
DeferImpl(const DeferImpl<DeferFunc>&) = delete;
DeferImpl& operator = (const DeferImpl&) = delete;
DeferImpl& operator = (DeferImpl&&) = delete;

private:
DeferFunc m_func;
bool m_valid;
};

template<typename DeferFunc> inline DeferImpl<DeferFunc>::DeferImpl(const DeferFunc& deferFunc)
: m_func(deferFunc), m_valid(true) {}

template<typename DeferFunc> inline DeferImpl<DeferFunc>::DeferImpl(DeferFunc&& deferFunc)
: m_func(std::move(deferFunc)), m_valid(true) {}

template<typename DeferFunc> inline DeferImpl<DeferFunc>::DeferImpl(DeferImpl<DeferFunc>&& deferImpl)
: m_func(std::move(deferImpl.m_func)), m_valid(true)
{
deferImpl.m_valid = false;
}

template<typename DeferFunc> inline DeferImpl<DeferFunc>::~DeferImpl()
{
if (m_valid) {
m_func();
}
}

template<typename DeferFunc> DeferImpl<DeferFunc> MakeDeferIns(DeferFunc&& func)
{
return DeferImpl<DeferFunc>(std::forward<DeferFunc>(func));
}

#define CONCAT_(a, b, c) a##b##c
#define CONCAT(a, b, c) CONCAT_(a, b, c)
#define defer(codeBlocks) \
auto CONCAT(defer_, __LINE__, __COUNTER__) = MakeDeferIns(codeBlocks)

该实现的基本思路是:

  1. 先实现一个DeferImpl类封装一个匿名函数,在析构发时候执行内部函数
  2. 用宏定义实现defer,使用__LINE____COUNTER__宏在调用defer时在栈上创建独一无二的变量,名称格式为为defer_$line$counter,类型为DeferImpl

写一个测试程序,其中包含了多个defer代码块:

// main.cpp
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "defer.h"

int main()
{
const std::string path = "/home/xuranus/file.txt";
const int BUFF_MAX = 1024;
char buff[BUFF_MAX] = { 0 };
int fd = ::open(path.c_str() , O_RDONLY | O_NONBLOCK);
if (fd < 0) {
std::cerr << "file open failed" << std::endl;
return -1;
}
defer([&](...){
::close(fd);
std::cout << "file closed" << std::endl;
});
defer([](){
std::cout << "defer block 1" << std::endl;
});
defer([](){
std::cout << "defer block 2" << std::endl;
});
defer([](){
std::cout << "defer block 3" << std::endl;
});
int n = ::read(fd, buff, BUFF_MAX);
if (n < 0) {
return -1;
}
std::string str;
str.assign(buff, n)
std::cout << str << std::endl; // hello world
return 0;
}

程序输出
helloworld

defer block 3
defer block 2
defer block 1
file closed

对于其中多个defer代码块,执行的顺序是从下往上。这是因为C++中为了保证资源释放的正确顺序,总是以和构造的相反的顺序执行析构函数。所以如果需要在一个函数中声明多个defer,需要注意他们的相对顺序。

Disqus评论区没有正常加载,请使用科学上网