奇怪的Arduino C++编译错误取决于文件位置



我最近在Arduino论坛上发布了一个可能的错误(https://forum.arduino.cc/index.php?topic=656968),但我怀疑他们会"哼,那又怎样?"因为这超出了他们对Arduino图书馆组织的标准规则。然而,我仍然想了解为什么它失败了,我怀疑这是我唯一能得到体面和/或明确答案的地方:

(记录在案,arduino库通常位于一个文件夹中,该文件夹包含library.properties元数据"manifest"和一个包含所有文件的src文件夹。他们的构建系统会自动编译(并链接!)src中的所有.cpp文件,无论它们是否使用。显然,在99%的情况下,所有都被使用/需要,但由于各种原因…)


开始包含

1.8.10版ESP8266核心2.6.2

在我们开始之前,我已经写了几个成功的库,我熟悉"标准"的做事方式。我这么说是因为下面的内容可能被认为是"非标准的",尽管它在语法上是有效的,并且在中,除了一个实例外,其他所有实例都能成功编译和运行。我在尝试绕过在src文件夹中编译所有内容的限制时偶然发现了它,无论你是否想要

在失败的例子中,我得到了一个从未见过、不理解、毫无意义的编译错误;但更糟糕的是:它根本不应该发生,因为它是a)有效的代码b)功能与工作案例相同,并且在不在单独的文件夹中时可以干净地编译,就像当前的情况一样。。。

那么,根本的问题是,这段有效C++代码的失败与成功之间的唯一显著区别是它在文件系统中的物理位置,这让我很困惑

首先绘制草图:

#include <H4P_WiFiX.h>
void f1(){}
void f2(){}
/*
* H4P_WiFi has defaults of [](){}, [](){}
*/
//H4P_WiFi h4wifi(f1,f2); // compiles OK
//H4P_WiFi h4wifi([](){},f2); // compiles OK
//H4P_WiFi h4wifi(f1,[](){}); // compiles OK
//H4P_WiFi h4wifi(f1); // compiles OK
//H4P_WiFi h4wifi([](){},[](){}); // compiles OK
//H4P_WiFi h4wifi([](){}); // compiles OK
H4P_WiFi h4wifi; // using BOTH defaults (and ONLY that case), fails with the "helpful":
/*
C:UsersphilAppDataLocalTempccnwDwvr.s: Assembler messages:
C:UsersphilAppDataLocalTempccnwDwvr.s:21: Error: symbol `_ZNSt17_Function_handlerIFvvEZN8H4P_WiFiC1ESt8functionIS0_ES3_Ed_NUlvE_EE9_M_invokeERKSt9_Any_data' is already defined
C:UsersphilAppDataLocalTempccnwDwvr.s:97: Error: symbol `_ZNSt14_Function_base13_Base_managerIZN8H4P_WiFiC1ESt8functionIFvvEES4_Ed_NUlvE_EE10_M_managerERSt9_Any_dataRKS7_St18_Manager_operation' is already defined
*/
void setup() {}
void loop() {}

现在奇怪的事情:"奇数"文件夹结构为:

libraries/H4Pwtf
|--optional
|
|--H4P_WiFi.cpp
|--H4P_WiFi.h
|--H4Plugins.h
!--src
|
|--H4P_WiFiX,h
|--H4Plugins.cpp
library.properties

src/H4P_WiFiX.h

#ifndef H4P_WiFi2_H
#define H4P_WiFi2_H
// Yes, I know, I know! See above - it's valid even though weird
#include"../optional/H4P_WiFi.cpp"
#endif

src/H4Plugins.cpp

#include"../optional/H4Plugins.h"
H4PluginService::H4PluginService(H4P_FN_VOID onConnect,H4P_FN_VOID onDisconnect){}

可选/H4P_插件.h

#ifndef H4P_HO
#define H4P_HO
#include<functional>
using H4P_FN_VOID = std::function<void()>;
class H4Plugin {};
class H4PluginService: public H4Plugin {
public:
H4PluginService(H4P_FN_VOID onConnect=[](){},H4P_FN_VOID onDisconnect=[](){});
};
#endif // H4P_HO

可选/H4P_WiFi.h

#ifndef H4P_WiFi_HO
#define H4P_WiFi_HO

#include"H4Plugins.h"
class H4P_WiFi: public H4PluginService{
public:
H4P_WiFi(H4P_FN_VOID onConnect=[](){},H4P_FN_VOID onDisconnect=[](){});
};
#endif // H4P_WiFi_HO

可选/H4P_WiFi.cpp

#include"H4P_WiFi.h"
H4P_WiFi::H4P_WiFi(H4P_FN_VOID onC,H4P_FN_VOID onD): H4PluginService(onC,onD){}

不用说,如果我将这一切移到src,并将"怪异"的include编辑回<>变体而不是"版本,并且不奇怪地包含.cpp(obvs,因为它无论如何都会因为"在那里"而被编译),那么不应该失败的行就不会失败,所有东西都按预期100%编译和运行。

但这并不是重点:在我看来,构建系统似乎包含了两次不应该包含的东西……我就是无法理解这个错误!有什么想法吗?

末端夹杂物


那么,overpower:有人能解释这个错误吗?当它只发生在两个默认值都被使用时?


由于"胡桃木"而得以解决。一个遗留问题是,只有当两个相同的lambda作为默认函数在类外定义时,编译器错误才会显现出来。我把它移回了里面,用我备受嘲笑(但仍在工作)的非标准文件组织重新编译:Bingo!代码现在正按预期运行,并且只在#包含的模块中编译。我是一只快乐的兔子。感谢其他做出贡献的人。案件结案+1至堆叠式

在替换#include指令并删除不会影响结果错误的部分后,您可以将有问题的代码归结为以下可重复的最小测试用例:

#include<functional>
using H4P_FN_VOID = std::function<void()>;
class H4P_WiFi{
public:
H4P_WiFi(H4P_FN_VOID onConnect=[](){}, H4P_FN_VOID onDisconnect=[](){});
};
H4P_WiFi::H4P_WiFi(H4P_FN_VOID, H4P_FN_VOID) {}
H4P_WiFi h4wifi;
int main() {}

将其作为一个单独的编译单元进行编译确实仍然会使用GCC生成错误消息,即使在最新的9.2版本中也是如此,而Clang对此没有任何问题,请参阅这个godbolt。

GCC 9.2在组装过程中给出的错误消息与您的非常相似:

/tmp/cceVlRkg.s: Assembler messages:
/tmp/cceVlRkg.s:77: Error: symbol `_ZNSt14_Function_base13_Base_managerIZN8H4P_WiFiC4ESt8functionIFvvEES4_Ed_UlvE_E10_M_managerERSt9_Any_dataRKS7_St18_Manager_operation' is already defined
/tmp/cceVlRkg.s:131: Error: symbol `_ZNSt17_Function_handlerIFvvEZN8H4P_WiFiC4ESt8functionIS0_ES3_Ed_UlvE_E9_M_invokeERKSt9_Any_data' is already defined

我们得到的符号名称:

std::_Function_handler<void (), H4P_WiFi::H4P_WiFi(std::function<void ()>, std::function<void ()>)::{default arg#1}::{lambda()#1}>::_M_invoke(std::_Any_data const&)
std::_Function_base::_Base_manager<H4P_WiFi::H4P_WiFi(std::function<void ()>, std::function<void ()>)::{default arg#1}::{lambda()#1}>::_M_manager(std::_Any_data&, std::_Function_base::_Base_manager<H4P_WiFi::H4P_WiFi(std::function<void ()>, std::function<void ()>)::{default arg#1}::{lambda()#1}> const&, std::_Manager_operation)

我确信这是GCC中的一个编译器错误。在GCC主干上(在godbolt上),我们甚至会得到一个ICE(内部编译器错误):

<source>:12:16: error: Two symbols with same comdat_group are not linked by the same_comdat_group list.
12 | H4P_WiFi h4wifi;
|                ^
_ZNSt9_Any_data9_M_accessIZN8H4P_WiFiC4ESt8functionIFvvEES4_Ed_UlvE_EERT_v/1079 (_Tp& std::_Any_data::_M_access() [with _Tp = H4P_WiFi::<lambda()>]) @0x7f019ddbd000
Type: function definition analyzed
Visibility: public weak comdat comdat_group:_ZNSt9_Any_data9_M_accessIZN8H4P_WiFiC4ESt8functionIFvvEES4_Ed_UlvE_EERT_v one_only visibility_specified
previous sharing asm name: 1078
References: 
Referring: 
Function flags: body
Called by: static void std::_Function_base::_Base_manager<_Functor>::_M_destroy(std::_Any_data&, std::true_type) [with _Functor = H4P_WiFi::<lambda()>]/1077 
Calls: void* std::_Any_data::_M_access()/217 
_ZNSt9_Any_data9_M_accessIZN8H4P_WiFiC4ESt8functionIFvvEES4_Ed_UlvE_EERT_v/1078 (_Tp& std::_Any_data::_M_access() [with _Tp = H4P_WiFi::<lambda()>]) @0x7f019ddb7e10
Type: function definition analyzed
Visibility: public weak comdat comdat_group:_ZNSt9_Any_data9_M_accessIZN8H4P_WiFiC4ESt8functionIFvvEES4_Ed_UlvE_EERT_v one_only visibility_specified
next sharing asm name: 1079
References:
Referring: 
Function flags: body
Called by: static void std::_Function_base::_Base_manager<_Functor>::_M_destroy(std::_Any_data&, std::true_type) [with _Functor = H4P_WiFi::<lambda()>]/1073 
Calls: void* std::_Any_data::_M_access()/217 
<source>:12:16: internal compiler error: symtab_node::verify failed
Please submit a full bug report,
with preprocessed source if appropriate.
See <https://gcc.gnu.org/bugs/> for instructions.
Compiler returned: 1

GCC错误报告中也报告了类似的错误。显然,当使用至少两个默认参数调用时,它只显示类的类外定义成员函数,每个参数都包含一个相同签名的lambda。

事实上,如果我例如将一个参数添加到lambda的参数列表中的一个(例如[](auto...){}[](int=0){}),而不添加其他参数,那么就没有错误。但是,如果我将两者相加,我们将再次出现相同的错误。

除非另有明确指示,否则大多数交互式构建系统都会为每个.cpp文件创建一个编译模块。我不知道有哪个IDE不会认为这是唯一可能的方法。这将导致违反ODR规则,因为HP4_WiFi模块将包含与HP4_Plugins模块相同的定义。在您的情况下,这就是构造函数的定义。

为什么在没有调用构造函数的情况下不会发生这种情况?未定义,但编译器可能只是做了一些优化并排除了未引用的符号。

玩<>和"您可能会引入另一个不确定性,因为这只会告诉预处理器先检查默认文件夹,而不是包含文件所在的文件夹。您的代码可能会从外部获取其他内容!

您必须避免包含.cpp文件(duh!),或者,如果有理由包含要内联(或模板化)的.cpp文件,请将其重命名为其他文件,例如.inl.

最新更新