我可以为 C++ class 提供一个不完整的 header 来隐藏实现细节吗?

Can I provide an incomplete header for a C++ class to hide the implementation details?

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

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

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


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

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

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

简短的回答是"No!"

原因:any/all 'client' 需要使用您的 Class class 的项目必须具有 完整声明 class,以便编译器可以正确确定成员变量的偏移量等内容。

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

一种可能的解决方法是声明一个 指针 指向 Class 中的嵌套 class,这个嵌套的 class 只是在共享的 header: class NestedClass 中声明,然后你可以在你的实现中用那个嵌套的 class 指针做你喜欢的事情。您通常会将嵌套的 class 指针设为 private 成员;此外,由于共享 header 中未给出其定义,因此 'client' 项目试图访问该 class(作为指针除外)的任何尝试都将是编译器错误。

这是一个可能的代码分解(可能不是 error-free,但是,因为它很快 type-up):

// 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<MyInternal> hidden 会更好。但是,这将需要您在 Class 中为析构函数、赋值运算符和其他(移动运算符?复制构造函数?)提供 显式 定义,因为这些将需要访问MyInternal 结构的完整定义。

私有实现 (PIMPL) 惯用语可以帮助您解决这个问题。它可能会产生 2 header 和 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 的 header(API)是用户将看到的所有内容。编译器编译 foo 所需的全部知识是有一个指针作为数据成员,因此它可以正确调整大小 foo