C++ 多个库定义相同的类名

bjg*_*222 5 c++ namespaces arduino header-files name-clash

我正在开发一个项目,其中有一个供应商库,例如vendor.h,针对我正在使用的特定 Arduino 兼容板,它定义了class HTTPClient与 Arduino 系统库冲突的内容,HTTPClient.h该库也定义了class HTTPClient.

这两个类除了具有相同的名称之外没有任何关系,并且 HTTP 客户端的供应商实现的能力远远低于 Arduino 系统库的实现,因此我更愿意使用后者。但我不能忽略前者,因为我需要从vendor.h. 本质上,我在这里提出了问题,但问题是类而不是函数。我拥有两者的完整代码,但考虑到一个是系统库,另一个是供应商库,我不愿意分叉和编辑其中任何一个,因为如果其中任何一个被更新,这会增加大量的合并工作,所以我的偏好是找到一个不编辑任何标题的整洁的解决方案。

我尝试过其他问题中发布的各种解决方案:

  • 我不想遗漏任何一个标头,因为我需要vendor.h很多东西并且需要 的HTTPClient.h客户端实现的功能
  • 标头中正确的命名空间可以解决问题,我宁愿避免编辑任一标头
  • 我尝试将 包装#include <HTTPClient.h>在 my 的命名空间中main.cpp,但这导致了链接错误,因为它不是仅包含标头的库,因此标头和 cpp 不在同一命名空间中
  • 我尝试了一个简单的包装器,如上面链接的 SO 问题中建议的函数,其中标头仅包含我的包装器类的前向声明,而关联的 cpp 包含实际的类定义。这给出了编译器错误error: aggregate 'HTTP::Client client' has incomplete type and cannot be defined(下面是此尝试的代码示例)

主要.cpp:

#include <vendor.h>
#include "httpclientwrapper.h"

HTTP::Client client;
Run Code Online (Sandbox Code Playgroud)

httpclientwrapper.h:

#ifndef INC_HTTPCLIENTWRAPPER_H
#define INC_HTTPCLIENTWRAPPER_H

namespace HTTP {

class Client;

}

#endif
Run Code Online (Sandbox Code Playgroud)

httpclientwrapper.cpp:

#include "httpclientwrapper.h"

#include <HTTPClient.h>

namespace HTTP {

class Client : public ::HTTPClient {};

}
Run Code Online (Sandbox Code Playgroud)

在该示例中,我无法继承HTTPClient标头中的类定义,因为这会将重复的类名重新引入到我的主程序中的全局命名空间(因此可能会被误导尝试查看前向声明是否可以解决问题)。我怀疑我可以通过完全复制HTTPClient上面的包装类中的类定义来解决该问题,而不是尝试使用继承。然后,我将成员定义添加到我的包装器 cpp 中,它将调用传递给HTTPClient的成员。在我经历将整个HTTPClient定义重写(或更可能是复制/粘贴)HTTPClient.h到我自己的包装器中的麻烦之前,我想知道是否有更好或更合适的方法来解决冲突?

谢谢你的帮助!

bjg*_*222 8

由于从未提出解决方案,因此我发布了一个总结我的研究和最终解决方案的答案。大多数情况下,我鼓励使用名称空间,因为正确使用名称空间可以消除冲突。然而,Arduino 环境试图保持简单以降低进入门槛,避开 C++ 的“复杂”功能,因此更高级的用例可能会继续遇到这样的问题。从其他答案和论坛帖子(我可以引用的地方)中,这里有一些避免名称冲突的方法,如下所示:

如果可以编辑源

编辑源代码以消除冲突或将命名空间添加到两个库之一。如果这是一个开源库,请提交拉取请求。这是最干净的解决方案。但是,如果您无法将更改推回到上游(例如当某个库是某些硬件的系统库时),那么当维护者/开发人员更新库时,您可能最终会遇到合并问题。

如果您无法编辑源

部分内容归功于:如何避免 C++ 中两个库的变量/函数冲突

对于仅包含头文件的库(或所有函数都是inline

(即,他们只有一个没有或 的.h文件).o.cpp

将库包含在命名空间内。在大多数代码中,这被认为是一种糟糕的形式,但如果您已经处于尝试处理一个不能很好地包含自身的库的情况,那么这是一种将代码包含在命名空间并避免名称冲突。

main.cpp

namespace foo {
    #include library.h
}

int main() {
    foo::bar(1); 
}
Run Code Online (Sandbox Code Playgroud)

对于具有函数的库

上述方法在编译时将无法链接,因为标头中的声明将位于命名空间内,但这些函数的定义则不在命名空间内。

相反,创建一个包装器标头和实现文件。在标头中,声明您要使用的名称空间和函数,但不要导入原始库。在实现文件中,导入您的库,并使用新命名空间函数中的函数。这样,一个冲突的库就不会与另一个库导入到同一位置。

wrapper.h

namespace foo {
    int bar(int a);
}
Run Code Online (Sandbox Code Playgroud)

wrapper.cpp

#include "wrapper.h"
#include "library.h"

namespace foo {
    int bar(int a) {
        return ::bar(a);
    } 
}
Run Code Online (Sandbox Code Playgroud)

main.cpp

#include "wrapper.h"

int main() {
    foo::bar(1); 
}
Run Code Online (Sandbox Code Playgroud)

为了保持一致性,您还可以包装这两个库,使它们各自位于自己的命名空间中。这种方法确实意味着您必须花费精力为您计划使用的每个函数编写一个包装器。然而,当您需要使用库中的类时,这会变得更加复杂(见下文)。

对于有类的图书馆

这是上面包装函数模型的扩展,但是您需要投入更多的工作,并且还有一些缺点。您无法编写从库的类继承的类,因为这需要在定义类之前在包装器标头中导入原始库,因此您必须编写完整的包装器类。出于同样的原因,您也不能拥有原始类中类型的私有成员,您可以将调用委托给该原始类。我在问题中描述的使用前向声明的尝试也不起作用,因为头文件需要类的完整声明才能编译。这给我留下了下面的实现,它只适用于单例的情况(无论如何这是我的用例)。

包装器头文件应该几乎完全复制您要使用的类的公共接口。

wrapper.h

namespace foo {
    Class Bar() {
    public:
        void f(int a);
        bool g(char* b, int c, bool d);
        char* h();
    };
}
Run Code Online (Sandbox Code Playgroud)

然后,包装器实现文件创建一个实例并传递调用。

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();
    } 
}
Run Code Online (Sandbox Code Playgroud)

无论包装类实例化多少次,主文件都将仅与原始类的单个实例交互。

main.cpp

#include "wrapper.h"

int main() {
    foo::Bar obj;
    obj.f(1);
    obj.g("hello",5,true);
    obj.h();  
}
Run Code Online (Sandbox Code Playgroud)

总的来说,我认为这是一个有缺陷的解决方案。为了完全包装这个类,我认为可以修改它以添加一个工厂类,该工厂类将完全包含在包装器实现文件中。每次实例化包装类时,此类都会实例化原始库类,然后跟踪这些实例。通过这种方式,您的包装类可以在工厂中保留其关联实例的索引,并绕过将该实例作为其自己的私有成员的需要。这似乎是一项大量的工作,我并没有尝试这样做,但看起来像下面的代码。(这可能需要一些改进并真正查看其内存使用情况!)

包装器头文件添加构造函数和私有成员来存储实例 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;
    };
}
Run Code Online (Sandbox Code Playgroud)

然后,包装器实现文件添加一个工厂类来管理原始库类的实例

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();
    } 
}
Run Code Online (Sandbox Code Playgroud)

主文件保持不变

main.cpp

#include "wrapper.h"

int main() {
    foo::bar obj;
    obj.f(1);
    obj.g("hello",5,true);
    obj.h();  
}
Run Code Online (Sandbox Code Playgroud)

如果所有这些看起来工作量很大,那么您的想法和我是一样的。我实现了基本的类包装器,并意识到它不适用于我的用例。HTTPClient考虑到 Arduino 的硬件限制,我最终决定,我最终编写了自己的 HTTP 实现库,而不是添加更多代码来使用任一库中的实现,因此没有使用上述任何一个,而是保存了几个一百千字节内存。但我想在这里分享,以防其他人想要回答同样的问题!