你如何在C ++中进行“真正的”封装?

时间:2013-07-25 01:54:52

标签: c++ encapsulation

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

但我不禁想到C ++的做法有点不足。例如,以(基于摄氏温度)温度等级为例:

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

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

  • 数据在temp变量(及其类型)内部维护。
  • 有一个华氏/摄氏转换的内部例程。

所以,理想情况下,在我看来,类的实现者会使用上面的标题,但是类的任何客户端都只会看到公共位。

不要误解我的意思,我不批评C ++,因为它符合规定的目的,即阻止客户使用私有位,但对于更复杂的类,你可以很容易地解决内部问题基于私人数据和功能的名称,类型和签名的详细信息。

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

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

4 个答案:

答案 0 :(得分:8)

C ++使用一种称为" pimpl"的成语。 (私有实现/实现指针)隐藏实现细节。有关详细信息,请查看this MSDN article

简而言之,您可以正常在头文件中公开您的界面。我们以您的代码为例:

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的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;
};

接下来,&#34;隐藏&#34; 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的答案。

答案 1 :(得分:4)

以为我会充实出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接口而不是只有一个“秘密”实现的人。还有一些样板,因为它使用语言的内置机制进行虚拟调度,以调度实现的接口函数调用。

答案 2 :(得分:0)

私有实现(PIMPL)是C ++提供此功能的方式。由于我无法使用CygWin g ++ 4.3.4获取unique_ptr变体,因此另一种方法是在可见类中使用void *,如下所示。这将允许您使用pre-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

答案 3 :(得分:0)

pugi::xml_document在pugixml库中使用了一种非正统的方法,它没有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++博客文章。