1

封装(信息隐藏)是一个非常有用的概念,可确保仅在类的 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++ 中有没有更简单的方法来达到同样的目的?

4

4 回答 4

8

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

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

tTemp.h

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 文件中,不公开。

tTemp.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 的 pre-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() << "F\n";
    return 0;
}

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

0C is 32F

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

于 2013-07-25T02:05:59.467 回答
4

以为我会充实 Don Wakefield 在评论中提到的“接口类/工厂”技术。首先,我们从接口中抽象出所有实现细节,并定义一个只包含 a 接口的抽象类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对象的客户调用工厂来构建一个对象。工厂可以提供一些复杂的基础设施,在不同的条件下返回接口的不同实现,或者像本例中的“给我一个临时”工厂这样简单的东西。

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

// 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接口的人都可以实现,而不是只有一个“秘密”实现。还有一点样板,因为它使用语言的内置虚拟分派机制来分派接口函数调用到实现。

于 2013-07-25T05:19:46.550 回答
1

我从 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 encapsulation in C++博客文章。

于 2019-09-29T21:10:21.130 回答
0

私有实现 (PIMPL) 是 C++ 可以提​​供此功能的方式。由于我无法unique_ptr使用 CygWin g++ 4.3.4 编译变体,另一种方法是void *在可见类中使用 a ,如下所示。这将允许您使用 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() << "F\n";
    return 0;
}

该代码的输出当然是:

0C is 32F
于 2013-07-25T04:18:40.477 回答