如何在C++中"true"封装?

How do you do "true" encapsulation in C++?

本文关键字:true 封装 C++      更新时间:2023-10-16

封装(信息隐藏(是一个非常有用的概念,确保在类的API中只发布最微小的细节。

但我不禁觉得C++这样做的方式有点不足。以(基于摄氏度的(温度等级为例,例如:

class tTemp {
    private:
        double temp;
        double tempF (double);
    public:
        tTemp ();
        ~tTemp ();
        setTemp (double);
        double getTemp ();
        double getTempF ();
};

现在,这是一个非常简单的情况,但它说明了封装并不完美的一点。"真实"封装将隐藏所有不必要的信息,例如:

  • 数据在 temp 变量(及其类型(中内部维护的事实。
  • 事实上,华氏/摄氏度转换有一个内部例程。
因此,理想情况下,在我看来,该类

的实现者将使用上述标头,但该类的任何客户端都只会看到公共位。

不要误会我的意思,我不是在批评C++因为它符合防止客户端使用私有位的既定目的,但是,对于更复杂的类,您可以轻松地根据私有数据和函数的名称、类型和签名来计算内部细节。

C++如何允许实现者隐藏此信息(假设可能(?在 C 中,我只是使用不透明类型,以便隐藏内部细节,但您将如何在C++中做到这一点?

我想我可以维护一个单独的类,对客户端完全隐藏,只有我自己的代码知道,然后在可见类中保留一个带有void *的实例(在我的代码中强制转换(,但这似乎是一个相当痛苦的过程。C++有没有更简单的方法来实现相同的目标?

C++使用称为"pimpl"(私有实现/指向实现的指针(的习语来隐藏实现细节。 有关详细信息,请查看此 MSDN 文章。

简而言之,您可以像往常一样在头文件中公开接口。 让我们以您的代码为例:

温度

class tTemp {
    private:
        class ttemp_impl; // forward declare the implementation class
        std::unique_ptr<ttemp_impl> pimpl;
    public:
        tTemp ();
       ~tTemp ();
       setTemp (double);
       double getTemp (void);
       double getTempF (void);
};

公共接口保留,但私有内部已替换为指向私有实现类的智能指针。 此实现类仅位于标头的相应.cpp文件中,不会公开

温度.cpp

class tTemp::ttemp_impl
{
    // put your implementation details here
}
// use the pimpl as necessary from the public interface
// be sure to initialize the pimpl!
tTtemp::tTemp() : pimpl(new ttemp_impl) {}

这还具有额外的优势,即允许您在不更改标头的情况下更改类的内部结构,这意味着类用户的重新编译更少。


对于Paxdiablo在C++11之前的答案中所示的完整解决方案,但使用unique_ptr而不是void *,您可以使用以下内容。第一ttemp.h

#include <memory>
class tTemp {
public:
    tTemp();
    ~tTemp();
    void setTemp(double);
    double getTemp (void);
    double getTempF (void);
private:
    class impl;
    std::unique_ptr<impl> pimpl;
};

接下来,ttemp.cpp中的"隐藏"实现:

#include "ttemp.h"
struct tTemp::impl {
    double temp;
    impl() { temp = 0; };
    double tempF (void) { return temp * 9 / 5 + 32; };
};
tTemp::tTemp() : pimpl (new tTemp::impl()) {};
tTemp::~tTemp() {}
void tTemp::setTemp (double t) { pimpl->temp = t; }
double tTemp::getTemp (void) { return pimpl->temp; }
double tTemp::getTempF (void) { return pimpl->tempF(); }

最后,ttemp_test.cpp

#include <iostream>
#include <cstdlib>
#include "ttemp.h"
int main (void) {
    tTemp t;
    std::cout << t.getTemp() << "C is " << t.getTempF() << "Fn";
    return 0;
}

而且,就像paxdiablo的解决方案一样,输出是:

0C is 32F

具有更高的类型安全性的额外优势。这个答案是 C++11 的理想解决方案,如果您的编译器是 C++11 之前的,请参阅 paxdiablo 的答案。

以为我会充实Don Wakefield在他的评论中提到的"接口类/工厂"技术。首先,我们从接口中抽象出所有实现细节,并定义一个仅包含Temp接口的抽象类:

// in interface.h:
class Temp {
    public:
        virtual ~Temp() {}
        virtual void setTemp(double) = 0;
        virtual double getTemp() const = 0;
        virtual double getTempF() const = 0;
        static std::unique_ptr<Temp> factory();
};

需要Temp对象的客户端调用工厂来构建一个对象。工厂可以提供一些复杂的基础结构,在不同条件下返回接口的不同实现,或者像此示例中的"只给我一个 Temp"工厂这样简单的东西。

实现类可以通过为所有纯虚函数声明提供覆盖来实现接口:

// in implementation.cpp:
class ConcreteTemp : public Temp {
    private:
        double temp;
        static double tempF(double t) { return t * (9.0 / 5) + 32; }
    public:
        ConcreteTemp() : temp() {}
        void setTemp(double t) { temp = t; }
        double getTemp() const { return temp; }
        double getTempF() const { return tempF(temp); }
};

在某个地方(可能在同一implementation.cpp中(,我们需要定义工厂:

std::unique_ptr<Temp> Temp::factory() {
    return std::unique_ptr<Temp>(new ConcreteTemp);
}

这种方法比 pimpl 更容易扩展:任何想要实现 Temp 接口的人都可以实现,而不是只有一个"秘密"实现。样板也少了一点,因为它使用该语言的内置机制进行虚拟调度,以调度接口函数调用到实现。

我从 pugixml 库中看到 pugi::xml_document 使用了一种非正统的方法,它没有 pimpl 或抽象类的开销。它是这样的:

在公开的类中保留一个char数组:

class tTemp {
public:
    tTemp();
    ~tTemp();
    void setTemp(double);
    double getTemp();
    double getTempF();
    alignas(8) char _[8]; // reserved for private use.
};

请注意,

  • 此示例中的对齐方式和大小是硬编码的。对于实际应用程序,您将使用表达式根据机器字的大小(例如sizeof(void*)*8或类似(来估计。
  • 添加private不会提供任何额外的保护,因为对_的任何访问都可以替换为 char* 的转换。提供封装的标头中缺少实现详细信息。

接下来,在翻译单元中,您可以按如下方式实现tTemp

struct tTempImpl {
    double temp;
};
static_assert(sizeof(tTempImpl) <= sizeof(tTemp::_), "reserved memory is too small");
static double tempF(tTemp &that) {
    tTempImpl *p = (tTempImpl*)&that._[0];
    return p->temp * 9 / 5 + 32;
}
tTemp::tTemp() {
    tTempImpl *p = new(_) tTempImpl();
}
tTemp::~tTemp() {
    ((tTempImpl*)_)->~tTempImpl();
}
tTemp::tTemp(const tTemp& orig) {
    new(_) tTempImpl(*(const tTempImpl*)orig._);
}
void tTemp::setTemp(double t) {
    tTempImpl *p = (tTempImpl*)_;
    p->temp = t;
}
double tTemp::getTemp() {
    tTempImpl *p = (tTempImpl*)_;
    return p->temp;
}
double tTemp::getTempF() {
    return tempF(*this);
}

与其他提出的方法相比,这肯定更冗长。但这是我所知道的唯一一种零开销方法,可以真正隐藏标头中的所有编译时依赖项。请注意,它还提供了一定程度的 ABI 稳定性 - 只要其大小不超过保留内存,您就可以更改tTempImpl

有关C++封装的更详细讨论,请参阅我的 True 封装C++博客文章。

私有实现(PIMPL(是C++提供此功能的方式。由于我在使用 CygWin g++ 4.3.4 编译unique_ptr变体时遇到了麻烦,因此另一种方法是在可见类中使用void *,如下所示。这将允许您使用C++11之前的编译器,以及像前面提到的gcc这样的编译器,它只对C++11有实验性支持。

首先,头文件ttemp.h,客户端包含的文件。这不透明地声明了内部实现结构,以便这些内部结构完全隐藏。您可以看到,唯一显示的细节是内部类和变量的名称,它们都不需要透露有关内部工作原理的任何信息:

struct tTempImpl;
class tTemp {
public:
    tTemp();
    ~tTemp();
    tTemp (const tTemp&);
    void setTemp(double);
    double getTemp (void);
    double getTempF (void);
private:
    tTempImpl *pimpl;
};

接下来,实现文件ttemp.cpp,它既声明和定义不透明的东西,也定义用户可见的细节。由于用户从未看到此代码,因此他们不知道它是如何实现的:

#include "ttemp.h"
struct tTempImpl {
    double temp;
    tTempImpl() { temp = 0; };
    double tempF (void) { return temp * 9 / 5 + 32; };
};
tTemp::tTemp() : pimpl (new tTempImpl()) {
};
tTemp::~tTemp() {
    delete pimpl;
}
tTemp::tTemp (const tTemp& orig) {
    pimpl = new tTempImpl;
    pimpl->temp = orig.pimpl->temp;
}
void tTemp::setTemp (double t) {
    pimpl->temp = t;
}
double tTemp::getTemp (void) {
    return pimpl->temp;
}
double tTemp::getTempF (void) {
    return pimpl->tempF();
}

请注意,内部实现细节不受可见类本身的任何保护。您可以将内部结构定义为具有访问器和突变器的类,但这似乎是不必要的,因为在这种情况下它应该紧密耦合。

上面需要注意的一点是:由于您使用指针来控制隐藏方面,因此默认的浅拷贝构造函数会因为有两个可见对象引用同一个私有成员(导致析构函数中的双重删除(而引起痛苦。所以你需要(就像我一样(提供一个深拷贝复制构造函数来防止这种情况。

最后,一个测试程序显示了整个事情是如何结合在一起的:

#include <iostream>
#include "ttemp.h"
int main (void) {
    tTemp t;
    std::cout << t.getTemp() << "C is " << t.getTempF() << "Fn";
    return 0;
}

当然,该代码的输出是:

0C is 32F