C++支持一种名为分离编译的概念,用户代码只能看见所用类型和函数的声明。
有两种方法可以实现它:
优点:
模块技术是在C++20中出现地新特性,其提供了实质性地优势,对改善代码组织与编译耗时都有好处。
一个单独编译的.cpp文件(包含它#include的.h文件)被称作一个翻译单元。
使用#include及头文件实现模块化是一种传统方法,它具有明显地缺点:
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;
这里仅仅是为了演示新旧方式的混合。
头文件与模块的区别不仅仅是在语法上:
例如:作者测试过使用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 << '}';
}
void my_code(vector& x,vector& y) {
using std::swap; //将标准库的swap放进本地作用域
//...
swap(x,y); //std::swap()
other::swap(x,y);//某个其他的swap()
}
默认情况使用复制(传值),如果希望直接指向调用者环境中的对象,我们使用引用(传引用)的方式。
从性能方面考虑,我们通常对小数据传值、对大数据传引用。这里的小意味着”复制开销很低“。通常而言,”尺寸在两到三个指针以内“是一个不错的标准。
一个函数只能返回一个值,但这个值可以是拥有很多成员的类对象。这往往是函数体面地返回多个值地方法。
例如:
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()返回类型的推导。
这种把类对象成员的名称赋予局部变量名称的机制叫作结构化绑定。
区分声明(用作接口)和定义(用作实现);
优先选择module而非头文件(在支持module的地方);
使用头文件描述接口、强调逻辑结构;
在头文件中应避免定义非内联函数;
不要在头文件中使用using指令;
采用传值方式传递“小”值,采用传引用方式传递“大”值;
优先选择传const引用方式而非传普通引用方式;
不要过度使用返回类型推断;
不要过度使用结构化绑定;使用命名的返回类型通常可以使代码更为清晰。