C++之旅(学习笔记)第3章 模块化

C++之旅(学习笔记)第3章 模块化

3.1 分离编译

C++支持一种名为分离编译的概念,用户代码只能看见所用类型和函数的声明。

有两种方法可以实现它:

  • 头文件:将声明放进一个名为头文件的独立文件,然后将头文件以文本方式#include到代码中你需要声明的地方。
  • 模块:定义module文件,独立地编译它们,然后在需要时import它们。在import对应module时,只有其中显示export的声明是可见的。

优点:

  • 可以尽可能地减少编译时间,并且强制要求程序中逻辑独立地部分分离开来(从而尽可能降低发生错误地概率)。

模块技术是在C++20中出现地新特性,其提供了实质性地优势,对改善代码组织与编译耗时都有好处。

一个单独编译的.cpp文件(包含它#include的.h文件)被称作一个翻译单元。

使用#include及头文件实现模块化是一种传统方法,它具有明显地缺点:

  • **编译时间:**如果你在101个翻译单元中#include header.h,这个header.h的头文件将被编译器处理101次。
  • **依赖顺序:**如果你在header2.h之前#include header1.h,在header1.h中的定义与宏可能会影响header2.h中代码的含义,反之亦然。
  • **不协调:**如果你在一个文件中定义一个实体,比如类型或者函数,然后在另一个文件中定义一个稍微不同的版本,则可能导致崩溃或者难以觉察的错误。
  • **传染性:**所有表达头文件中某一个声明所需的代码,都必须出现在头文件中。这会导致代码膨胀,因为头文件为了完成声明需要#include其他头文件,这会导致头文件的用户需要(有意或者无意地)依赖头文件包含地实现细节。

3.2 模块

C++20中出现了语言级的方式来直接实现模块化。

export module Vector;			//定义一个module,名为Vector
export class Vector {
public:
    Vector(int s);
    double& operator[](int i);
    int size();
private:
    double* elem;				//elem指向一个数组,该数组包含sz个double类型的元素
    int sz;
};
Vector::Vector(int s) : elem{new double[s], sz{s}} {}//初始化元素
double& Vector::operator[](int i){
    return elem[i];
}
int Vector::size() {
    return sz;
}
export bool operator==(const Vector& v1,const Vector& v2) {
    if(v1.size() != v2.size())
        return false;
    for(int i = 0; i < v1.size(); ++i)
        if(v1[i] != v2[i])
            return false;
    return true;
}

上诉代码定义了一个module,名为Vector,这个模块导出了Vector类及所有成员函数,还有成员函数的操作符==。

要使用上述module,只要在需要用到它的地方import就可以了。

例如:

//user.cpp
import Vector;							//获得Vector的相关接口
#include 						//获得标准库的数学函数接口,包含sqrt()
double sqrt_sum(Vector& v){
    double sum = 0;
    for(int i = 0; i != v.size(); ++i)
        sum +=std::sqrt(v[i]);			//平方根之和
    return sum;
}

#inclue 也可以改为import cmath;这里仅仅是为了演示新旧方式的混合。

3.3 模块与头文件两种方法的区别

头文件与模块的区别不仅仅是在语法上:

  • 模块只被编译一次,不会在每个用到它的翻译单元那里都被重新编译。
  • 两个模块import的顺序不影响其含义。
  • 如果你在模块内部import或者#include其他内容,模块的使用者不会隐式地获得那些模块地访问权:这意味着import没有传染性。
  • 模块在维护性与编译时间方面地改进非常显著。

例如:作者测试过使用import std;的“Hello,world!"程序,它的编译速度比使用#include 的版本**快10倍。**这还是在std模块因包含了整个标准库足足有10倍大小的前提下实现的。

提升的原理:

  • 模块只导出接口,但头文件需要传递所有直接的或者间接的信息给编译器。

**不幸的是,module std还没有进入C++20。**附录A介绍了如何获得一份module std的方法,这里不详细展开。

示例:

当定义一个模块时,不需要将实现与声明分开写成两个文件,如果想改进你的源代码,可以这么做:

export module Vector;		//定义module,名叫Vector
export class Vector {
    //...
};
export bool operator==(const Vector& v1, const Vector& v2){
    //...
}

编译器负责把(用export指定的)模块的接口从实现细节中分离出来,因此,Vector接口由编译器生成,不需要由用户指定。

使用module时,不需要为了在接口文件内隐藏实现细节而将代码变得复杂;因为模块只导出显示export的声明。

考虑如下代码:

export module vector_printer;
import std;
export
template
void print(std::vector& v)	//这是唯一能被用户看见的函数
{
    cout << "{\n";
    for(const T& val : v)
        std::cout << " " << val << '\n';
    cout << '}';
}

3.4 命名空间

  • C++还提供了一种名为命名空间的机制,一方面表达某些声明是属于一个整体的,另一方面表面它们的名字不会与其他命名空间中的名字冲突。
  • 要想访问其他命名空间中的某个名字,最简单的方法是在这个名字前加上命名空间的名字作为限定(例如,std::cout和My_code::main)。
  • ”真正的main()“定义在全局命名空间中,换句话说,它不属于任何自定义的命名空间、类或者函数。
  • 如果觉得反复使用命名空间限定显得冗长及干扰了可读性,可以使用using声明将命名空间中的名字放进当前作用域。
void my_code(vector& x,vector& y) {
    using std::swap; //将标准库的swap放进本地作用域
    //...
    swap(x,y);		 //std::swap()
    other::swap(x,y);//某个其他的swap()
}

3.5 函数参数与返回值

默认情况使用复制(传值),如果希望直接指向调用者环境中的对象,我们使用引用(传引用)的方式。

从性能方面考虑,我们通常对小数据传值、对大数据传引用。这里的小意味着”复制开销很低“。通常而言,”尺寸在两到三个指针以内“是一个不错的标准。

3.5.1 结构化绑定

一个函数只能返回一个值,但这个值可以是拥有很多成员的类对象。这往往是函数体面地返回多个值地方法。

例如:

struct Entry {
    string name;
    int value;
};
Entry read_entry(istream& is)//简单地读函数
{
    string s;
    int i;
    is >> s >> i;
    return {s,i};
}
auto e = read_entry(cin);
cout << "{" << e.name << "," << e.value << "}\n";

在这里,{s,i}被用于构造Entry类型地返回值。类似地,我们也可以将Entry的成员”解包"为局部变量:

auto [n,v] = read_entry(is);
cout << "{" << n << " , " << v << " }\n";

这里的auto[n,v]声明了两个变量n和v,它们的类型来自对read_entry()返回类型的推导。

这种把类对象成员的名称赋予局部变量名称的机制叫作结构化绑定。

3.6 建议

区分声明(用作接口)和定义(用作实现);

优先选择module而非头文件(在支持module的地方);

使用头文件描述接口、强调逻辑结构;

在头文件中应避免定义非内联函数;

不要在头文件中使用using指令;

采用传值方式传递“小”值,采用传引用方式传递“大”值;

优先选择传const引用方式而非传普通引用方式;

不要过度使用返回类型推断;

不要过度使用结构化绑定;使用命名的返回类型通常可以使代码更为清晰。

你可能感兴趣的:(C++,c++,学习,笔记)