3

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

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

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

  • 我不想遗漏任何一个标题,因为我需要vendor.h很多东西并且需要HTTPClient.h客户端实现的功能
  • 标题中适当的命名空间可以解决问题,我宁愿避免编辑任何一个标题
  • 我尝试将 包装#include <HTTPClient.h>在我的命名空间中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;

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 {};

}

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

谢谢你的帮助!

4

1 回答 1

4

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

如果您可以编辑源

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

如果您无法编辑源

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

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

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

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

main.cpp

namespace foo {
    #include library.h
}

int main() {
    foo::bar(1); 
}

对于具有函数的库

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

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

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); 
}

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

对于有类的库

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

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

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();
    } 
}

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

main.cpp

#include "wrapper.h"

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

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

包装头文件添加构造函数和私有成员来存储实例 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;
    };
}

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

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();  
}

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

于 2020-06-21T01:22:21.747 回答