尝试使用模块时标准库内容的多重定义错误

use*_*869 3 c++ c++20 c++-modules

我有两个文件 Interface.cppm(主模块接口单元)和 main.cpp。我没有该模块的任何其他模块单元。

在Interface.cppm中,我有以下内容

module;

#include <cstdint>

export module Interface; 

import <algorithm>;
import <iostream>;
import <memory>;
import <sstream>;
import <string>;
import <tuple>;
import <type_traits>;
import <vector>;

//Code that this interface exports and
//implementation details.
Run Code Online (Sandbox Code Playgroud)

我是 main.cpp,我有以下代码:

import Interface;
import <iostream>;
import <memory>;
import <string>;

int main(){
    //Using the contents of Interface module
}
Run Code Online (Sandbox Code Playgroud)

我预编译了标头单元并将它们放在名为 header-units 的文件夹中。然后我使用以下命令编译我的代码:

clang++ -std=c++20 Interface.cppm -fmodule-file=./header-units/algorithm.pcm -fmodule-file=./header-units/iostream.pcm --precompile -fmodule-file=./header-units/memory.pcm -fmodule-file=./header-units/sstream.pcm -fmodule-file=./header-units/string.pcm -fmodule-file=./header-units/tuple.pcm -fmodule-file=./header-units/type_traits.pcm -fmodule-file=./header-units/vector.pcm -fmodule-file=./header-units/unordered_map.pcm -o Interface.pcm    //This works fine

clang++ -std=c++20 main.cpp -fmodule-file=Interface.pcm -fmodule-file=./header-units/iostream.pcm -fmodule-file=./header-units/string.pcm -fmodule-file=./header-units/memory.pcm -c -o main.o   //This works fine

clang++ -std=c++20 Interface.pcm -c -o Interface.o   //This works fine

clang++ -std=c++20 Interface.o main.o -o output
Run Code Online (Sandbox Code Playgroud)

在执行最后一个命令之后,我收到一系列类似于以下内容的链接器错误:

usr/bin/ld: main.o: in function `std::bad_alloc::bad_alloc()':
main.cpp:(.text+0x0): multiple definition of `std::bad_alloc::bad_alloc()'; Interface.o:Interface.pcm:(.text+0x0): first defined here
/usr/bin/ld: main.o: in function `std::exception::exception()':
main.cpp:(.text+0x40): multiple definition of `std::exception::exception()'; Interface.o:Interface.pcm:(.text+0x40): first defined here
/usr/bin/ld: main.o: in function `std::bad_array_new_length::bad_array_new_length()':
<and many others>
Run Code Online (Sandbox Code Playgroud)

我尝试了其他方法,例如从 Interface 模块导出标头单元,而不是像这样在 main.cpp 中导入这些标头单元:

//Interface.cppm
module;
#include <cstdint>
export module Interface;
export import <iostream>;
export import <memory>;
export import <string>;
import <algorithm>;
....


//main.cpp
import Interface;

int main(){
    //Code using the Interface
}
Run Code Online (Sandbox Code Playgroud)

但这具有相同的效果,即标准库组件中多个定义的链接器错误。我不确定我在这里做错了什么。如果有人能帮助我解决这个问题,那就太好了。

更新- 我设法通过这样做来摆脱这个问题(通过反复试验的方法):

//Interface.cppm
module;
#include <algorithm>
#include <cstdint>
#include <iostream>
...
export module Interface;
//Code that this interface exports and
//implementation details.
Run Code Online (Sandbox Code Playgroud)

我将所有导入更改为包含在 Interface.cppm 的全局模块片段中。

//main.cpp
import Interface;
import <iostream>;
import <memory>;
import <string>;
int main(){
   //Code that uses the Interface module
}
Run Code Online (Sandbox Code Playgroud)

在 main.cpp 中,我只是将导入保留原样。

这能够很好地链接,但我仍然不确定为什么。

我试图了解当我们在全局模块片段中包含头文件时会发生什么。导入此类模块的代码会发生什么情况?

上述情况与导出导入的标头单元(如 )的模块有何不同export import <iostream>

对于导出的标头单元,标头单元中的宏是否会影响导入此模块的代码中包含的任何标头?

此外,如果模块仅导入标头单元而不导出它,那么导入此类模块的代码会受到怎样的影响?标头单元会影响导入模块的代码吗?如果不是,为什么我的问题中的第一个代码片段会抛出如此多的链接器错误,指出标准库组件中违反了 ODR?

如果有人可以帮助我理解这一点,这将有助于我更好地理解模块。

use*_*869 5

我自己找到了为什么会出现重新定义错误的答案。

我在查看了Nathan Sidwell 的 CPPCon 视频(从时间戳 9 分 50 秒开始)后得到了答案。Nathan Sidwell 尝试将 TinyXML2 转换为使用模块,但他像我一样遇到了标准库组件的多个定义错误。

我在这里总结一下他所说的话:

通常,当头文件在同一翻译单元中多次包含时,为了避免出现多个定义错误,我们使用包含保护。

假设我们有以下文件:

//widget.h
#ifndef _WIDGET_H
#define _WIDGET_H

class Widget {...};

#endif

//foo.h
#ifndef _FOO_H
#define _FOO_H

#include "widget.h"
...

#endif

//bar.cpp
#include "widget.h"
#include "foo.h"

...
Run Code Online (Sandbox Code Playgroud)

在这种情况下,widget.h 中的 include 保护将防止 widget 类定义在与 bar.cpp 对应的翻译单元中被包含两次。

但是如果我们这样做:

//widget.h and foo.h as above

//bar.cpp
#include "widget.h"

import "foo.h";
Run Code Online (Sandbox Code Playgroud)

由于bar.cpp对应的翻译单元中类Widget的多个定义错误,代码将无法编译。这是因为标头单元(这里我们将 foo.h 作为标头单元导入)在某种意义上是不同的,包含防护对它们不起作用。

这里#include "widget.h"foo.h 内是一个问题。widget.h 中的标头保护不会阻止其内容被复制到 bar.cpp 的翻译单元中,即使它已经被 bar.cpp 直接包含,这将导致类 Widget 在此翻译单元中被定义两次,这违反了ODR。

这与我的代码中发生的情况完全相同。问题出在我的主模块接口文件 Interface.cppm 上。

我将分析在我的原始问题中导致多个定义错误的前两个代码片段,然后回答为什么它在第三个代码片段中起作用。

我的第一个片段是

//Interface.cppm
module;

#include <cstdint>

export module Interface; 

import <algorithm>;
import <iostream>;
import <memory>;
import <sstream>;
import <string>;
import <tuple>;
import <type_traits>;
import <vector>;

//Code that this interface exports and
//implementation details.


//main.cpp
import Interface;
import <iostream>;
import <memory>;
import <string>;

int main(){
    //Using the contents of Interface module
}
Run Code Online (Sandbox Code Playgroud)

这里 Interface.cppm 导入多个标准库头文件作为头文件单元,而 main.cpp 再次导入其中一些头文件单元。问题之一是import <sstream>import <string>。这里头文件<sstream>有一个#include <string>,我再次导入< string>。标准库头文件<string>包括其他标准库头文件和一些内部实现头文件exceptioncompare等等。我得到的多个定义错误就是针对这些的。还<sstream>直接<iostream>包含常见标头,例如<ios><istream><ostream>。这些导致了另一大块重定义错误。还有其他问题,例如与<vector><string>都包括<initializer_list>等等。

本质上同样的问题发生在第二个代码片段中:

//Interface.cppm
module;
#include <cstdint>
export module Interface;
export import <iostream>;
export import <memory>;
export import <string>;
import <algorithm>;
....


//main.cpp
import Interface;

int main(){
    //Code using the Interface
}
Run Code Online (Sandbox Code Playgroud)

这里唯一的变化是 Interface.cppm 重新导出一些导入的标头单元,以便 main 不必导入它们。但事实上 Interface.cppm 导入头单元<sstream>仍然<string>是一个问题,会导致多个重新定义错误,并且这个问题没有得到解决。

然而,在第三个片段中:

//Interface.cppm
module;
#include <algorithm>
#include <cstdint>
#include <iostream>
...
export module Interface;
//Code that this interface exports and
//implementation details.

//main.cpp
import Interface;
import <iostream>;
import <memory>;
import <string>;
int main(){
   //Code that uses the Interface module
}
Run Code Online (Sandbox Code Playgroud)

没有重新定义错误。这是因为这里 Interface.cppm 不使用导入,而是使用全局模块片段中的包含,并且包含防护在这里发挥作用并防止多次包含。

然而,在 main.cpp 中,我有 3 个导入,即iostream,memorystring

我想了解为什么这 3 个标头单元导入不会导致多个定义错误,并且我深入研究了 libc++(我正在使用的标准库)代码。

除了名为version、的文件__assert以及__config一些其他实现定义的头文件(如 )之外<__memory/allocate_at_least.h>,它们与 Interface.cppm 中的其他头单元不同,没有任何共同点。我没有直接在 main.cpp 中包含/导入任何这些文件,因此没有冲突。

现在我发现了为什么我的代码有效或为什么它不起作用,但我仍然没有回答其他问题。我还有一个与导入标头单元相关的新问题,这可能会导致多个定义错误 - 那么我应该如何解决这个问题?我将在一个新问题中问这些。