如何在 C++ 共享库中隐藏业务对象的实现细节并提供接口

Posted

技术标签:

【中文标题】如何在 C++ 共享库中隐藏业务对象的实现细节并提供接口【英文标题】:How to hide implementation details of business objects in a C++ shared library and provide an interface 【发布时间】:2013-12-23 21:33:36 【问题描述】:

我想要一个共享库并将这个库传递给一些 Qt 插件。 Qt 插件与库/库接口一起使用(创建一些东西,编辑它们等)。 我的计划

启动Qt应用程序和共享库(共享库在编译时链接,因此无需使用dlopen/LoadLibrary和resolve。应用程序只需找到该库) 搜索 Qt 插件并加载它们 将共享库传递给 Qt 插件 让 Qt 插件与库一起工作

整个 Qt 很清楚,并且在 Linux、Unix 和 Windows(使用 GCC 和 MSVC)下工作。请注意以下几点

我来自 Java 背景。在Java中会有一个接口,一个实现接口的抽象类和一个扩展抽象类的业务类。也许是一个用于创建业务对象并将接口返回给库用户的工厂类。但是业务对象永远无法从类外部访问,因为它是包保护的 根据信息 1:如何使用 C++ 实现相同的功能?我的想法如下(代码在最后): 工厂:创建业务对象,但返回接口 接口:与虚函数的接口 业务类:实现接口 导出工厂和接口,但隐藏业务类 解决方案必须独立于平台 在编译时链接库,避免 dlopen/LoadLibrary & resolve 使业务对象只能通过接口或工厂访问

这会导致以下问题

    为什么dataobjectabuseddataobject BOTH 都可以访问?为什么不仅是工厂创建的对象(接口)(而且abuseddataobject 会触发编译器错误)?这个想法是隐藏业务对象并使其只能通过接口访问并且只能由工厂创建。该库确实负责 GCC __attributes__ ?有什么错误/问题? 这是在 C++ 中执行此操作的常用方法,还是我“对 Java 视而不见”(参见 C++ 代码)并将 Java 和 C++ 混为一谈? 如果我对 Java 视而不见,在 C++ 中执行此操作的(常见)方法是什么(请提供代码示例)?

main.cpp

#include <iostream>
#include "library/Extern.h"
#include "library/Factory.h"
#include "library/DataInterface.h"
#include "library/Data.h" // NOTE: User abuses our interface and includes the data object

int main(void)

    // Create the factory and get an interface object
    Factory factory;
    DataInterface *dataobject = factory.getDataObject();

    // Play around
    std::cout << "Value:" << dataobject->getValue() << std::endl;

    // But now someone abuses our data object ... WHY IS THIS POSSIBLE ?
    Data *abuseddateobject = new Data();
    std::cout << "Abused value:" << abuseddateobject->getValue() << std::endl;

    // Cleanup
    delete dataobject;
    delete abuseddateobject;

    return 0;

工厂.h

#ifndef FACTORY_H
#define FACTORY_H

#include "Extern.h"
#include "DataInterface.h"
#include "Data.h"

class DLL_PUBLIC Factory

public:
    DLL_PUBLIC DataInterface *getDataObject();
;
#endif // FACTORY_H

Factory.cpp

#include "Factory.h"

DataInterface *Factory::getDataObject()

    return new Data();

数据接口.h

#ifndef DATAINTERFACE_H
#define DATAINTERFACE_H

#include "Extern.h"

class DLL_PUBLIC DataInterface

public:
    DLL_PUBLIC virtual ~DataInterface()         
    DLL_PUBLIC virtual int getValue() = 0;
;
#endif // DATAINTERFACE_H

数据.h

#ifndef DATA_H
#define DATA_H

#include "Extern.h"
#include "DataInterface.h"

class DLL_LOCAL Data : public DataInterface

public:
    DLL_LOCAL int getValue()
    
        return 42;
    
;

#endif // DATA_H

最后是 Extern.h。库构建系统 (CMake) 确实为共享库定义了 BUILDING_DLL,所以这不是问题。目前我只是在为 Linux 编写一个原型,所以 Windows 支持并不重要

外部.h

#ifndef SOMECLASSEXTERN_H
#define SOMECLASSEXTERN_H

// Taken from: http://gcc.gnu.org/wiki/Visibility

#if defined _WIN32 || defined __CYGWIN__
    #ifdef BUILDING_DLL
        #ifdef __GNUC__
            #define DLL_PUBLIC __attribute__ ((dllexport))
        #else
            #define DLL_PUBLIC __declspec(dllexport) // Note: actually gcc seems to also supports this syntax.
        #endif
    #else
        #ifdef __GNUC__
            #define DLL_PUBLIC __attribute__ ((dllimport))
        #else
            #define DLL_PUBLIC __declspec(dllimport) // Note: actually gcc seems to also supports this syntax.
        #endif
    #endif
    #define DLL_LOCAL
#else
    #if __GNUC__ >= 4
        #define DLL_PUBLIC __attribute__ ((visibility ("default")))
        #define DLL_LOCAL  __attribute__ ((visibility ("hidden")))
    #else
        #define DLL_PUBLIC
        #define DLL_LOCAL
    #endif
#endif

#endif // SOMECLASSEXTERN_H

感谢您花时间回答这个问题

【问题讨论】:

【参考方案1】:

简而言之:#include "library/Data.h" 应该无法编译。

Data 类是库的实现细节。它永远不应该提供给用户!您应该将 interfaceimplementation 头文件分开,例如在单独的文件夹中,并设置构建以便库的用户无法访问实现文件.仅仅因为它是一个头文件并不意味着它是接口的一部分!

Nitpicks:我认为您同意您应该使用 C++,而不是一些卑鄙的 C/C++ 可憎之物。因此:

    我建议使用智能指针,例如QScopedPointerstd::unique_ptrdelete foo 的每个实例都应该被怀疑地看待,除非它是一个实现细节。编译器会处理 RAII 的尾部 - 使用它。

    您的 Factory 类是否应该是可实例化的存在疑问。可能您应该将其设置为仅具有静态成员的类,或者甚至是接口类中的静态成员。如果您要构建多种类型,工厂函数可以将所需类型作为参数。

    (void) 参数声明仅属于 C 语言,在 C++ 中绝对没有地位。这是因为在C 中,以下内容与void foo(void) 相同且不同:

    void foo();
    void foo(...);
    

    在 C++ 中不是这种情况

您的main 可能如下所示:

int main()

    QTextStream out(stdout);
    QScopedPointer<DataInterface> data1 = DataInterface::create("Data");

    out << "Value:" << data1->value() << endl;

    return 0;

【讨论】:

感谢您的回答。我不明白的事情:如果用户可以访问接口标头,他也可以访问实现标头(例如 debian 包:libfoo 和 libfoo-dev)。如何避免开发人员的 stupiditymaliciousness 包含实现标头(因为 devel 包提供了两个标头)?这只是我必须处理的事情还是有避免某些事情的好方法? @Albertus:libfoo-dev 不应该给出实现头文件!。如果是这样,它总是是一个错误。实现头文件是用于构建库的源代码的一部分,不应作为libfoo-dev 的一部分安装!例如,没有正常安装的 Qt“dev”包包含私有头文件(有很多!)。 @Albertus:那些实现头文件根本就不是使用库所必需的,而且没有任何用处。如果您的实现正确地使用了PIMPL 惯用语,那么实现标头无论如何都不会公开任何私有实现细节,因此它们不会给用户带来任何好处。 @Albertus:不过,您必须明白:C++ 语言特性并不能直接保护您免受恶意使用您的代码。用户总是可以使用 C/C++ 为他们提供的所有东西来转换指针并使用您提供给他们的实例做“邪恶”的事情。在图书馆方面,您无能为力。 @Albertus:C++ 智能指针已经存在了很长时间(在 Qt 和 boost 库中,在它们成为 C++11 的标准之前)。我真的建议您阅读 Strostrup 的 The C++ Programming Language, 4th edition,看看您错过了什么。现代 C++ 完全不像 Java。

以上是关于如何在 C++ 共享库中隐藏业务对象的实现细节并提供接口的主要内容,如果未能解决你的问题,请参考以下文章

C++面向对象:C++ 数据抽象

如何在 C++ 静态库中隐藏符号?

如何在共享库中找到 C++ isfinite() 的解析位置?

C++:动态共享库中的虚函数产生段错误

C++ 封装 & 信息隐藏

在共享库中全局声明的非 POD 对象的语义是啥?