这方面C++欠缺的就是一个模块系统。
举例说吧。
假如要做一个C++包管理器,怎么管理不同库之间的依赖呢?
容易想到的一个方案就是每个库都提供一个入口头文件,编译一个项目时由包管理器自己生成一个文件,把编译的项目的这个入口头文件和所依赖的每个库的文件都在生成的这个文件里面#include一次,然后让编译器直接去编译这个生成的文件就好了。
现在你有两个问题。
首先是名字空间冲突的问题。这个好说,只要包管理器统一管理名字,让一个库只能独自占用一个顶级的名字空间就好。
然后,假使你的库是A,依赖B和C两个库,B和C又同时依赖一个库D。那么这样的菱形依赖怎么解决呢?也好办,让包管理器在生成编译文件的时候考虑到顺序,使得D的入口文件的包含总是发生在B和C的前面,并且使每个库只在其中出现一次,否则包管理器报错。
到目前为止事情还好办。
还是用上面的例子,你的库是A,依赖B和C两个库,B和C又同时依赖一个库D。
现在,D有一个类型X,库B和C都对类型X做了全局重载,比如说重载了X+X,这个时候重复了。
这时候包管理器当然要报错。问题是B和C都是独立开发的,这个错要让谁去修复?难道是你,A的作者?
阻碍上面的程序完成构建的规则在C++中称为ODR,One Definition Rule。
在支持全局重载的其他编程语言中,包括Rust,对这种窘况有一个规则叫做coherence,有时候叫做orphan rule。
在上面的例子中,如果换了Rust的情况,类型X归属于crate(指一个编译单元你可以理解为模块)D,+运算符也有归属,是std标准库。这个时候B和C都不允许在X上做X+【随便某个类型】的重载。
如果B的作者需要做类似的事,需要自己包装类型X产生一个新的类型Y,然后在Y上做+运算符的全局重载。
C++就算要去掉全局重载也是很尴尬的,比方说iostream的灵活性就依赖于全局重载。
如果想像Rust(以及其他ML语言)那样,可以在C++加个预处理指令放在编译单元的入口文件的开头,标明对这个编译单元所有的全局重载要遵循orphan rule,同时引入外部模块的概念,这样类型才能所属于某个模块。这样能够在编译的时候报错,而不是像ODR那样在链接的时候报错那就晚了。代价就是iostream肯定没法正常用了。
如果采用像C#那种做法,在需要模块化的代码中去掉全局重载的使用,把运算符重载放在静态成员函数上实现,这样在模板特化方面没法做,还是要引入模块系统,但是不失为另一种做法。