【问题标题】:Can I provide an incomplete header for a C++ class to hide the implementation details?我可以为 C++ 类提供不完整的标头以隐藏实现细节吗?
【发布时间】:2019-09-27 12:59:43
【问题描述】:

我想将一个类实现分成三个部分,以避免用户需要处理实现细节,例如,我用来实现功能的库:

impl.cpp

#include <api.h>
#include <impl.h>
Class::Class() {
    init();
}
Class::init() {
    myData = SomeLibrary::Type(42);
}
Class::doSomething() {
    myData.doSomething();
}

impl.h

#include <somelibrary.h>
class Class {
public:
    Class();
    init();
    doSomething();
private:
    SomeLibary::Type myData;
}

api.h

class Class {
    Class();
    doSomething();
}

问题是,我不允许为类定义重新定义标题。当我仅在 api.h 中定义 Class()doSomething() 时,这也不起作用。


一个可能的选择是定义api.h,并且根本不在项目中使用它,而是安装它(并且不要安装impl.h)。

明显的缺点是,我需要确保api.himpl.h 中的常用方法始终具有相同的签名,否则使用该库的程序会出现链接器错误,我在编译时无法预测图书馆。

但是这种方法是否可行,或者我会遇到其他问题(例如,指向类成员的错误指针或类似问题),因为 obj 文件与标头不匹配?

【问题讨论】:

标签: c++ include api-design


【解决方案1】:

简短的回答是“不!”

原因:任何/所有需要使用您的 Class 类的“客户”项目都必须具有该类的完整声明,以便编译器可以正确确定成员变量的偏移量等。

private 成员的使用很好 - 客户端程序将无法更改它们 - 就像您当前的实现一样,标题中仅提供成员函数的最简要概述,所有实际定义都在您的(私有)源文件。

解决此问题的一种可能方法是在Class 中声明一个指向嵌套类的指针,该嵌套类仅在共享标头中声明:@987654324 @ 然后你可以在你的实现中使用嵌套的类指针做你喜欢的事情。您通常会将嵌套类指针设为private 成员;此外,由于共享标头中未给出其定义,因此“客户端”项目访问该类的任何尝试(作为指针除外)都将是编译器错误。

这是一个可能的代码故障(可能不是没有错误,因为它是一种快速输入):

// impl.h
struct MyInternal; // An 'opaque' structure - the definition is For Your Eyes Only
class Class {
public:
    Class();
    init();
    doSomething();
private:
    MyInternal* hidden; // CLient never needs to access this! Compiler error if attempted.
}

// impl.cpp
#include <api.h>
#include <impl.h>

struct MyInternal {
    SomeLibrary::Type myData;
};

Class::Class() {
    init();
}
Class::init() {
    hidden = new MyInternal; // MUCH BETTER TO USE unique_ptr, or some other STL.
    hidden->myData = SomeLibrary::Type(42);
}
Class::doSomething() {
    hidden->myData.doSomething();
}

注意:正如我在代码注释中所暗示的,使用std::unique_ptr&lt;MyInternal&gt; hidden 会更好。但是,这将要求您在 Class 中为析构函数、赋值运算符和其他(移动运算符?复制构造函数?)提供 explicit 定义,因为这些将需要访问MyInternal 结构体。

【讨论】:

  • 我的问题是,我想避免递归包含到 SomeLibrary,只是因为一些私有成员使用库中的类型。该库只是标题,因此程序无需知道我的库正在使用它。我目前尝试使用两个单独的标头,其中一个 api.h 标头根本不包含在我的实现中。这是否会因为您提到的偏移量计算而导致问题?
  • 我编辑了我的问题。没有 pimpl 的选项似乎是,我复制 impl.h 并删除所有私有的。然后我只链接库中的impl.h,只安装api.h。这行得通吗?
  • @allo 您可以(实际上,您应该)对导出的(客户端模式)项目和在您自己的实现文件中。将“相同”的类定义放在两个不同的标题中确实是在自找麻烦,更进一步。
  • impl.h 标头包含我不想公开的依赖项。由于类的重新定义,重用 api.h 标头不起作用。所以我可能要么需要尝试单独使用标题,要么我应该使用 pImpl 解决方案。
【解决方案2】:

私有实现 (PIMPL) 习语可以帮助您。它可能会导致 2 个头文件和 2 个源文件,而不是 2 和 1。有一个我实际上没有尝试编译的愚蠢示例:

api.h

#pragma once
#include <memory>
struct foo_impl;
struct foo {
    int do_something(int argument);
private:
    std::unique_ptr<foo_impl> impl;
}

api.c

#include "api.h"
#include "impl.h"
int foo::do_something(int a) { return impl->do_something(); }

impl.h

#pragma once
#include <iostream>
struct foo_impl {
    foo_impl();
    ~foo_impl();
    int do_something(int);
    int initialize_b();
private:  
    int b;
};

impl.c

#include <iostream>
foo_impl::foo_impl() : b(initialize_b()} {  }
foo_impl::~foo_impl() = default;
int foo_impl::do_something(int a) { return a+b++; }
int foo_impl::initialize_b() { ... }

foo_impl 可以有它需要的任何方法,因为foo 的标题(API)是用户将看到的所有内容。编译器编译foo 所需的全部内容是知道有一个指针作为数据成员,因此它可以正确调整foo 的大小。

【讨论】:

  • 这看起来不错,但是当我的程序尝试链接库时,我收到foo_impl 的错误,unique_ptr 无法删除不完整的类型,而断言在内存中:@987654331 @.
  • 我需要为foo 声明一个析构函数并将~foo() = default; 添加到impl.c,因此 foo 类在其(隐藏)实现中销毁了唯一指针。
  • @allo 谢谢。我会添加它。正如我所说,我没有尝试编译它。我很高兴它大部分都有效。
猜你喜欢
  • 2012-04-29
  • 1970-01-01
  • 1970-01-01
  • 2016-08-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多