Cpp_文件库

C++17之后,有新引入的文件库。屏蔽了操作系统的差异性,有统一的标准。
<filesystem>
示例:读取当前目录下(包括所有子目录)的Cpp、C相关的文件名,并统计其行数。

遍历搜索:

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
#include <filesystem>
std::vector<std::string>
file_directories()
{
std::vector<std::filesystem::directory_entry> entries;
std::copy_if(std::filesystem::recursive_directory_iterator("./"),
std::filesystem::recursive_directory_iterator(),
std::back_inserter(entries),
[](std::filesystem::directory_entry const& p) -> boll
{
if (p.is_regular_file())
if (p.path().extension().c_str() == L".cpp" ||
p.path().extension().c_str() == L".h" ||
p.path().extension().c_str() == L".c")
return true;
return false;
});
std::vector<std::string> file_names(entries.size());
// entry -> string name
std::transform(
entries.cbegin(),
entries.cend(),
file_names.begin(),
[](std::filesystem::directory_entry const& p) -> std::string
{
return p.path().generic_string();
});
return file_names;
}

在VS环境下,默认的当前文件目录是ProjectName.vcxproj所在的位置。

统计行数:通过<fstream>中的std::ifstream构造函数填入文件名打开文件,已经处理好了RAII,无需考虑关闭。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
std::vector<int>
count_lines_in_files(std::vector<string::string> & file_names)
{
std::vector<int> line_counts;
char ch = 0;
int line_count{ 0 };

for (auto const & file_name : file_names)
{
std::ifstream in{ file_name };
line_count = 0;
while (in.get(ch))
{
if (ch == '\n')
{
if (ch == '\n')
++line_count;
}
}
line_counts.emplace_back(line_count);
// in.close(); // RAII
}
return line_counts;
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main()
{
auto file_names = file_directories();
auto file_counts = count_lines_in_files(file_names);
std::for_each(
file_names.cbegin(),
file_names.cend(),
[](auto&& file_name) -> void
{
std::cout << file_name << std::endl;
});
std::for_each(
file_counts.cbegin(),
file_counts.cend(),
[](auto&& file_count) -> void
{
std::cout << file_count << std::endl;
});
std::cout << "Files Count: " << file_names.size() << std::endl;
}

结合算法库

结合算法库,可以大大简化我们自己写的代码。尤其是繁杂的for循环。

istream虽然不是容器,但也有相应的迭代器,在<iterator>中有Predefined iterators其中的istream_iterator
如果读写比较频繁,有效率更高的istreambuf_iterator

1
2
3
4
5
6
7
8
int count_lines(std::string const& file_name)
{
std::ifstream in{ file_name };
return std::count(
std::istreambuf_iterator<char>(in),
std::istreambuf_iterator<char>(),/* 不写默认是end */
'\n');
}

之前的函数可以简化为:

1
2
3
4
5
6
7
8
9
10
std::vector<int>
count_lines_in_files(std::vector<string::string> & file_names)
{
std::vector<int> line_counts;
for (auto const& file_name : file_names)
{
line_counts.emplace_back(count_lines(file_name));
}
return line_counts;
}

还能再次改进:

1
2
3
4
5
6
7
8
9
10
11
12
13
std::vector<int>
count_lines_in_files(std::vector<string::string> & file_names)
{
std::vector<int> line_counts(file_names.size());

std::transform(
std::execution::par,
file_names.cbegin(),
file_names.cend(),
line_counts.begin(),
count_lines);
return line_counts;
}

也可以结合ranges:一行代码解决!

C++23中可以通过std::ranges::to<Container>来输出到就地生成的容器中

1
2
3
4
5
6
std::vector<int>
count_lines_in_files(std::vector<string::string> & file_names)
{
return file_names | std::views::transform(count_lines) |
std::ranges::to<std::vector>();
}

Cpp_仿函数_lambda表达式

仿函数

Functor

仿函数是类。
相比于普通函数,仿函数作为类对象,有更多功能可以集成。

1
2
3
4
5
6
7
8
class IsOdd
{
public:
bool operator () (int const& v)
{
return v % 2 != 0;
}
};

使用如下:需要先实例化出一个对象。

1
2
3
4
5
6
7
8
int main()
{
std::vector<int> vec{ 0, 1, 2, 3, 4, 5, 6, 7, 8 };

IsOdd is_odd_functor;

auto it != std::find_if(vec.begin(), vec.end(), is_odd_functor);
}

剖析find_if

  1. first和last是迭代器,需要先解引用才能使用pred去判断值。
  2. first到last左闭右开,first等于last时结束。
  3. 内部封装了pred的实际调用形式pred(...)。所以不管是函数、仿函数,只要支持(...)的调用形式就能使用。
1
2
3
4
5
6
7
8
9
10
11
12
template <class InputIterator, class UnaryPredicate>
InputIterator find_if(InputIterator first, InputIterator last, UnaryPredicate pred)
{
while (first != last)
{
if (pred(*first))
return first;
else
++first;
}
return last;
}

lambda表达式

也叫lambda-calculus(演算)、lexical closure(词法闭包,闭包指穷举所有的状态)

C++中,本质上是生成了一个类,拥有一个static方法。

为什么叫做词法闭包呢?

假设,fn在内部定义了fn2、fn4,fn2内部定义了fn3。外界调用fn时是看不到它的内部的。但fn的结果依赖于fn2、fn4,fn2依赖于fn3。
总之,定义在内部,外界看不到,类似于黑盒,而且各部分的依赖关系不能断,需要穷尽路线,所以叫闭包。

  1. 功能不在全局定义,而是在内部定义。空间不易混乱
  2. fn可以直接使用fn2作为部分功能

    有了lambda表达式,就可以形式上在函数中嵌套定义函数了。
    但有个问题,fn中定义的变量,fn2可以访问吗?
1
2
3
4
5
6
7
8
9
#include <iostrean>
void bar(void)
{

}
int main()
{
return 0;
}

写法形式

1
2
3
[Capture List](Parameters) -> ReturnType {};
^ ^ ^ ^ ^
捕获列表 参数 goes to 返回类型 函数体

类似于函数,可以互相传递,还可以在中间捕获。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostrean>
int bar(void)
{
int a = 10;
// 以下生成了函数对象,可以就地调用:
int b = [a]() -> void
{
return a + 20;
}()/* 熟悉的函数调用标志:圆括号 */;
int c = [a](int v) -> void
{
return a + v + 20;
}(5);
return b;
}
int main(void)
{
int r = bar();
return 0;
}

按值捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int bar()
{
int a = 10, b = 11;
int c = [a, b](int v) -> int
{
return a + b + v + 20;
}(5);
// 也可以在中括号只填一个"=",代表按值捕获全部
int d = [=](int v) -> int
{
return a + b + v + 20;
}(5);
return c;
}

按值捕获时,内部lambda函数对捕获的变量修改时,外部值不变,相当于成为了自己的局部变量。
这个捕获的局部变量在内部也是默认不能改值的,但可以加mutable。明确告诉编译器自己要修改,并不是误会。但加了mutable依然不会影响外部。

1
2
3
4
5
6
7
8
9
10
11
12
13
int bar()
{
int a = 10, b = 11;
int c = [=](int v) -> int
{
return a + b++ + v + 20; // error, b can't be modified.
}(5);
int d = [=](int v) mutable -> int
{
return a + b++ + v + 20; // ok. , b = 12
}(5);
return b; // b = 11
}

按引用捕获

1
2
3
4
5
6
7
8
9
10
int bar()
{
int a = 10, b = 11;
// 如果想要按引用捕获全部变量,可以在[]中只写一个"&"
int c = [=, &b](int v) -> int
{
return a + b++ + v + 20;
}(5);
return b; // b = 12
}
  1. 如果想要按引用捕获全部变量,可以在[]中只写一个"&"
  2. 也可以在中括号只填一个"=",代表按值捕获全部
  3. 但是不能[=, &]既全部按值又全部按引用。同时存在时必须有一个是部分变量如[=, &b]
  4. 引用可以把外部值联动改变。

函数对象

  1. 可以用函数对象来存储定义的lambda函数。
  2. 不能用函数指针直接引用。
1
2
3
4
5
6
7
8
9
10
int bar()
{
int a = 10, b = 11;

auto lambda = [=, &b](int v) -> int
{
return a + b++ + v + 20;
};
return lambda(5);
}

以上是使用auto来自动推断、接收。
也可以使用std::function准确定义相应的函数对象类型来接收lambda函数。(也可以接收普通函数,只要返回值类型、参数类型符合即可)

1
2
3
4
5
6
7
8
9
10
11
#include <functional>
int bar()
{
int a = 10, b = 11;

std::function<int(int)> lambda = [=, &b](int v) -> int
{
return a + b++ + v + 20;
};
return lambda(5);
}

lambda函数对象的生存周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
std::function<int(int)> bar()
{
int a = 10, b = 11;
std::function<int(int)> lambda = [=, &b](int v) -> int
{
return a + b++ + v + 20;
}
}
int main()
{
auto lam = bar();
int r = lam(5);
r = lam(5);
}

以上main函数调用了两次bar()函数内部生成的lambda函数,其中lambda函数自己内部的b是按引用捕获的。
如果是普通函数的栈帧,则bar函数按理来说在第一行代码结束后就会塌陷,导致引用的b空间被污染。
在之前的C++11版本确实会存在这类问题,因为C++是静态编译期计算的,而Java、C#、Python是有垃圾回收机制的,有一个引用计数,如果仍有人引用b,则不会对其析构,因此往往让这些局部变量在堆上创建。但C++早期是建立在栈上,因此会发生问题。
后续版本C++17之后克服了此问题,每个厂商具体细节不同,微软公司也是把局部变量建立在堆上了,并用智能指针管理,即使bar函数栈帧塌陷了,也会帮你把类似a、b的局部变量存储到其他地方。

lambda函数与面向对象结合(捕获this)

比如让lambda函数调用某个对象的成员方法、成员变量,可以在lambda表达式中的捕获列表里写this指针。

面向函数编程

比较关心逻辑,不太关心细节。细节用高阶函数来完成。
就像是:如果不使用copy_if来做filter,那么就需要另写繁琐的for循环。