C++ 多个库定义相同的 Class 名称
C++ Multiple Libraries Define Same Class Name
我正在开发一个项目,其中我有一个供应商库,比如 vendor.h
,用于我正在使用的特定 Arduino-compatible 板,它定义了与 Arduino 冲突的 class HTTPClient
系统库 HTTPClient.h
,它还定义了 class HTTPClient
。
这两个 class 除了同名外没有任何关系,并且 HTTP 客户端的供应商实现远不如 Arduino 系统库的实现,所以我更愿意使用后者.但我不能省略包括前者,因为我需要从 vendor.h
中得到相当多的信息。本质上,我提出了 问题,但使用 class 而不是函数。我有两者的完整代码,但考虑到一个是系统库,另一个是供应商库,我不愿意分叉和编辑任何一个,因为如果其中一个更新,这会增加很多合并工作,所以我的偏好是找到一个不编辑 header.
的整洁解决方案
我尝试了其他 SO 问题中发布的各种解决方案:
- 我也不想遗漏 header,因为我需要
vendor.h
做很多事情,并且需要 HTTPClient.h
客户端实现的功能
- header 中的适当名称空间可以解决问题,我宁愿避免编辑 header
- 我尝试将
#include <HTTPClient.h>
包装在我的 main.cpp
的命名空间中,但这导致了链接错误,因为它不是 header-only 库,所以 header & cpp 不在同一个命名空间中
- 我尝试了一个简单的包装器,如上面链接的 SO 问题中的函数所建议的那样,其中 header 仅包含我的包装器 class 的前向声明&相关的 cpp 包含实际的 class定义。这给出了
error: aggregate 'HTTP::Client client' has incomplete type and cannot be defined
的编译器错误(下面这种尝试的代码示例)
main.cpp:
#include <vendor.h>
#include "httpclientwrapper.h"
HTTP::Client client;
httpclientwrapper.h:
#ifndef INC_HTTPCLIENTWRAPPER_H
#define INC_HTTPCLIENTWRAPPER_H
namespace HTTP {
class Client;
}
#endif
httpclientwrapper.cpp:
#include "httpclientwrapper.h"
#include <HTTPClient.h>
namespace HTTP {
class Client : public ::HTTPClient {};
}
在那个例子中,我无法在 header 的 class 定义中从 HTTPClient
继承,因为这会将重复的 class 名称重新引入我的主程序中的全局命名空间(因此可能误入歧途,试图查看前向声明是否可以解决问题)。我怀疑我可以通过在上面的包装器 class 中完全复制 HTTPClient
的 class 定义来解决这个问题,而不是尝试使用继承。然后我将成员定义添加到我的包装器 cpp 中,它将调用传递给 HTTPClient
的成员。在我经历将 HTTPClient.h
中的整个 HTTPClient
定义重写(或者更可能是 copy/pasting)到我自己的包装器之前,我想知道是否有更好或更合适的方法解决冲突?
感谢您的帮助!
由于从未提出解决方案,因此我发布了一个总结我的研究和最终解决方案的答案。大多数情况下,我鼓励使用名称空间,因为正确使用名称空间可以消除冲突。然而,Arduino 环境试图让事情变得简单以降低进入门槛,避开 C++ 的“复杂”功能,因此更高级的用例可能会继续 运行 进入这样的问题。从其他 SO 答案和论坛帖子(我可以引用的地方),这里有一些避免名称冲突的方法:
如果您可以编辑源代码
编辑源代码以消除冲突或向两个库之一添加命名空间。如果这是一个开源库,请提交拉取请求。这是最干净的解决方案。但是,如果您不能将您的更改推回到上游(例如,当一个是某些硬件的系统库时),当 maintainer/developer 更新库时,您可能最终会遇到合并问题。
如果您无法编辑源
部分功劳:How to avoid variable/function conflicts from two libraries in C++
对于仅 header 的库(或所有函数都是 inline
)
(即,他们只有一个 .h
文件,没有 .o
或 .cpp
)
将库包含在命名空间中。在大多数代码中,这被认为是糟糕的形式,但如果您已经处于这样一种情况,即您正在尝试处理一个不能很好地包含自身的库,那么将代码包含在一个库中是一种干净而简单的方法命名空间并避免名称冲突。
main.cpp
namespace foo {
#include library.h
}
int main() {
foo::bar(1);
}
对于具有函数的库
上述方法在编译时会失败link,因为header中的声明将在命名空间内,但那些函数的定义不在。
相反,创建一个包装器 header 和实现文件。在 header 中声明您的名称空间和您希望使用的函数,但 不要 导入原始库。在实现文件中,导入您的库,并使用新命名空间函数中的函数。这样一来,一个冲突的库就不会与另一个库导入到同一个地方。
wrapper.h
namespace foo {
int bar(int a);
}
wrapper.cpp
#include "wrapper.h"
#include "library.h"
namespace foo {
int bar(int a) {
return ::bar(a);
}
}
main.cpp
#include "wrapper.h"
int main() {
foo::bar(1);
}
为了保持一致性,您还可以包装两个库,使它们各自位于自己的命名空间中。这种方法确实意味着您将不得不努力为您计划使用的每个函数编写一个包装器。但是,当您需要使用库中的 classes 时(见下文),这会变得更加复杂。
对于具有 classes
的库
这是对上述包装函数模型的扩展,但您需要付出更多的努力,并且还有一些缺点。您不能编写从库的 class 继承的 class,因为这需要在定义 class 之前在包装器 header 中导入原始库,因此您必须写一个完整的包装器 class。您也不能拥有原始 class 类型的 class 的私有成员,出于同样的原因,您可以将调用委托给该成员。我在问题中描述的使用前向声明的尝试也没有奏效,因为 header 文件需要 class 的完整声明才能编译。这给我留下了下面的实现,它只适用于单例的情况(无论如何这是我的用例)。
包装 header 文件应该几乎完全复制您要使用的 class 的 public 接口。
wrapper.h
namespace foo {
Class Bar() {
public:
void f(int a);
bool g(char* b, int c, bool d);
char* h();
};
}
包装器实现文件然后创建一个实例并传递调用。
wrapper.cpp
#include "wrapper.h"
#include "library.h"
namespace foo {
::Bar obj;
void Bar::f(int a) {
return obj.f(a);
}
bool Bar::g(char* b, int c, bool d) {
return obj.g(b, c, d);
}
char* Bar::h() {
return obj.h();
}
}
主文件将仅与原始 class 的单个实例交互,无论您的包装器 class 实例化了多少次。
main.cpp
#include "wrapper.h"
int main() {
foo::Bar obj;
obj.f(1);
obj.g("hello",5,true);
obj.h();
}
总的来说,这是一个有缺陷的解决方案。要完全包装这个 class,我认为可以修改它以添加一个工厂 class,它将完全包含在包装器实现文件中。每次实例化包装器 class 时,此 class 都会实例化原始库 class,然后跟踪这些实例。通过这种方式,您的包装器 class 可以在工厂中保留其关联实例的索引,而无需将该实例作为其自己的私有成员。这看起来工作量很大,我没有尝试这样做,但看起来像下面的代码。 (这可能需要一些润色并真正查看其内存使用情况!)
包装器header文件添加了一个构造函数和私有成员来存储实例id
wrapper.h
namespace foo {
Class Bar() {
public:
Bar();
void f(int a);
bool g(char* b, int c, bool d);
char* h();
private:
unsigned int instance;
};
}
包装器实现文件然后添加一个工厂class来管理原始库的class
实例
wrapper.cpp
#include "wrapper.h"
#include "library.h"
namespace foo {
class BarFactory {
public:
static unsigned int new() {
instances[count] = new ::Bar();
return count++;
}
static ::Bar* get(unsigned int i) {
return instances[i];
}
private:
BarFactory();
::Bar* instances[MAX_COUNT]
int count;
};
void Bar::Bar() {
instance = BarFactory.new();
}
void Bar::f(int a) {
return BarFactory.get(i)->f(a);
}
bool Bar::g(char* b, int c, bool d) {
return BarFactory.get(i)->g(b, c, d);
}
char* Bar::h() {
return BarFactory.get(i)->h();
}
}
主文件不变
main.cpp
#include "wrapper.h"
int main() {
foo::bar obj;
obj.f(1);
obj.g("hello",5,true);
obj.h();
}
如果所有这些看起来工作量很大,那么您的想法和我一样。我实现了基本的 class 包装器,并意识到它不适用于我的用例。并考虑到硬件限制由于 Arduino 的使用,我最终决定与其添加更多代码以在任一库中使用 HTTPClient
实现,不如在最后编写自己的 HTTP 实现库,因此使用了 none以上并节省了数百千字节的内存。但我想在这里分享,以防其他人想回答同样的问题!
我正在开发一个项目,其中我有一个供应商库,比如 vendor.h
,用于我正在使用的特定 Arduino-compatible 板,它定义了与 Arduino 冲突的 class HTTPClient
系统库 HTTPClient.h
,它还定义了 class HTTPClient
。
这两个 class 除了同名外没有任何关系,并且 HTTP 客户端的供应商实现远不如 Arduino 系统库的实现,所以我更愿意使用后者.但我不能省略包括前者,因为我需要从 vendor.h
中得到相当多的信息。本质上,我提出了
我尝试了其他 SO 问题中发布的各种解决方案:
- 我也不想遗漏 header,因为我需要
vendor.h
做很多事情,并且需要HTTPClient.h
客户端实现的功能 - header 中的适当名称空间可以解决问题,我宁愿避免编辑 header
- 我尝试将
#include <HTTPClient.h>
包装在我的main.cpp
的命名空间中,但这导致了链接错误,因为它不是 header-only 库,所以 header & cpp 不在同一个命名空间中 - 我尝试了一个简单的包装器,如上面链接的 SO 问题中的函数所建议的那样,其中 header 仅包含我的包装器 class 的前向声明&相关的 cpp 包含实际的 class定义。这给出了
error: aggregate 'HTTP::Client client' has incomplete type and cannot be defined
的编译器错误(下面这种尝试的代码示例)
main.cpp:
#include <vendor.h>
#include "httpclientwrapper.h"
HTTP::Client client;
httpclientwrapper.h:
#ifndef INC_HTTPCLIENTWRAPPER_H
#define INC_HTTPCLIENTWRAPPER_H
namespace HTTP {
class Client;
}
#endif
httpclientwrapper.cpp:
#include "httpclientwrapper.h"
#include <HTTPClient.h>
namespace HTTP {
class Client : public ::HTTPClient {};
}
在那个例子中,我无法在 header 的 class 定义中从 HTTPClient
继承,因为这会将重复的 class 名称重新引入我的主程序中的全局命名空间(因此可能误入歧途,试图查看前向声明是否可以解决问题)。我怀疑我可以通过在上面的包装器 class 中完全复制 HTTPClient
的 class 定义来解决这个问题,而不是尝试使用继承。然后我将成员定义添加到我的包装器 cpp 中,它将调用传递给 HTTPClient
的成员。在我经历将 HTTPClient.h
中的整个 HTTPClient
定义重写(或者更可能是 copy/pasting)到我自己的包装器之前,我想知道是否有更好或更合适的方法解决冲突?
感谢您的帮助!
由于从未提出解决方案,因此我发布了一个总结我的研究和最终解决方案的答案。大多数情况下,我鼓励使用名称空间,因为正确使用名称空间可以消除冲突。然而,Arduino 环境试图让事情变得简单以降低进入门槛,避开 C++ 的“复杂”功能,因此更高级的用例可能会继续 运行 进入这样的问题。从其他 SO 答案和论坛帖子(我可以引用的地方),这里有一些避免名称冲突的方法:
如果您可以编辑源代码
编辑源代码以消除冲突或向两个库之一添加命名空间。如果这是一个开源库,请提交拉取请求。这是最干净的解决方案。但是,如果您不能将您的更改推回到上游(例如,当一个是某些硬件的系统库时),当 maintainer/developer 更新库时,您可能最终会遇到合并问题。
如果您无法编辑源
部分功劳:How to avoid variable/function conflicts from two libraries in C++
对于仅 header 的库(或所有函数都是 inline
)
(即,他们只有一个 .h
文件,没有 .o
或 .cpp
)
将库包含在命名空间中。在大多数代码中,这被认为是糟糕的形式,但如果您已经处于这样一种情况,即您正在尝试处理一个不能很好地包含自身的库,那么将代码包含在一个库中是一种干净而简单的方法命名空间并避免名称冲突。
main.cpp
namespace foo { #include library.h } int main() { foo::bar(1); }
对于具有函数的库
上述方法在编译时会失败link,因为header中的声明将在命名空间内,但那些函数的定义不在。
相反,创建一个包装器 header 和实现文件。在 header 中声明您的名称空间和您希望使用的函数,但 不要 导入原始库。在实现文件中,导入您的库,并使用新命名空间函数中的函数。这样一来,一个冲突的库就不会与另一个库导入到同一个地方。
wrapper.h
namespace foo { int bar(int a); }
wrapper.cpp
#include "wrapper.h" #include "library.h" namespace foo { int bar(int a) { return ::bar(a); } }
main.cpp
#include "wrapper.h" int main() { foo::bar(1); }
为了保持一致性,您还可以包装两个库,使它们各自位于自己的命名空间中。这种方法确实意味着您将不得不努力为您计划使用的每个函数编写一个包装器。但是,当您需要使用库中的 classes 时(见下文),这会变得更加复杂。
对于具有 classes
的库这是对上述包装函数模型的扩展,但您需要付出更多的努力,并且还有一些缺点。您不能编写从库的 class 继承的 class,因为这需要在定义 class 之前在包装器 header 中导入原始库,因此您必须写一个完整的包装器 class。您也不能拥有原始 class 类型的 class 的私有成员,出于同样的原因,您可以将调用委托给该成员。我在问题中描述的使用前向声明的尝试也没有奏效,因为 header 文件需要 class 的完整声明才能编译。这给我留下了下面的实现,它只适用于单例的情况(无论如何这是我的用例)。
包装 header 文件应该几乎完全复制您要使用的 class 的 public 接口。
wrapper.h
namespace foo { Class Bar() { public: void f(int a); bool g(char* b, int c, bool d); char* h(); }; }
包装器实现文件然后创建一个实例并传递调用。
wrapper.cpp
#include "wrapper.h" #include "library.h" namespace foo { ::Bar obj; void Bar::f(int a) { return obj.f(a); } bool Bar::g(char* b, int c, bool d) { return obj.g(b, c, d); } char* Bar::h() { return obj.h(); } }
主文件将仅与原始 class 的单个实例交互,无论您的包装器 class 实例化了多少次。
main.cpp
#include "wrapper.h" int main() { foo::Bar obj; obj.f(1); obj.g("hello",5,true); obj.h(); }
总的来说,这是一个有缺陷的解决方案。要完全包装这个 class,我认为可以修改它以添加一个工厂 class,它将完全包含在包装器实现文件中。每次实例化包装器 class 时,此 class 都会实例化原始库 class,然后跟踪这些实例。通过这种方式,您的包装器 class 可以在工厂中保留其关联实例的索引,而无需将该实例作为其自己的私有成员。这看起来工作量很大,我没有尝试这样做,但看起来像下面的代码。 (这可能需要一些润色并真正查看其内存使用情况!)
包装器header文件添加了一个构造函数和私有成员来存储实例id
wrapper.h
namespace foo { Class Bar() { public: Bar(); void f(int a); bool g(char* b, int c, bool d); char* h(); private: unsigned int instance; }; }
包装器实现文件然后添加一个工厂class来管理原始库的class
实例
wrapper.cpp
#include "wrapper.h" #include "library.h" namespace foo { class BarFactory { public: static unsigned int new() { instances[count] = new ::Bar(); return count++; } static ::Bar* get(unsigned int i) { return instances[i]; } private: BarFactory(); ::Bar* instances[MAX_COUNT] int count; }; void Bar::Bar() { instance = BarFactory.new(); } void Bar::f(int a) { return BarFactory.get(i)->f(a); } bool Bar::g(char* b, int c, bool d) { return BarFactory.get(i)->g(b, c, d); } char* Bar::h() { return BarFactory.get(i)->h(); } }
主文件不变
main.cpp
#include "wrapper.h" int main() { foo::bar obj; obj.f(1); obj.g("hello",5,true); obj.h(); }
如果所有这些看起来工作量很大,那么您的想法和我一样。我实现了基本的 class 包装器,并意识到它不适用于我的用例。并考虑到硬件限制由于 Arduino 的使用,我最终决定与其添加更多代码以在任一库中使用 HTTPClient
实现,不如在最后编写自己的 HTTP 实现库,因此使用了 none以上并节省了数百千字节的内存。但我想在这里分享,以防其他人想回答同样的问题!