繁体   English   中英

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

[英]How do you do “true” encapsulation in C++?

封装(信息隐藏)是一个非常有用的概念,它确保在类的 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 文件中,不公开。

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 的答案。

以为我会充实 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对象的客户调用工厂来构建一个。 工厂可以提供一些复杂的基础设施,在不同的条件下返回接口的不同实现,或者像这个例子中的“只给我一个临时”工厂一样简单。

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

// 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 encapsulation in 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() << "F\n";
    return 0;
}

该代码的输出当然是:

0C is 32F

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM