简体   繁体   English

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

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

Encapsulation (information hiding) is a very useful concept, ensuring that only the barest minimal details are published in the API of a class.封装(信息隐藏)是一个非常有用的概念,它确保在类的 API 中只发布最少的细节。

But I can't help thinking that the way C++ does this is a little deficient.但我不禁想到 C++ 这样做的方式有点不足。 Take, for example, a (Celsius-based) temperature class like:以一个(基于摄氏度的)温度等级为例,如:

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

Now, that's a very simple case but it illustrates a point that the encapsulation isn't perfect.现在,这是一个非常简单的案例,但它说明了封装并不完美的一点。 "Real" encapsulation would hide all unnecessary information such as: “真实”封装会隐藏所有不必要的信息,例如:

  • the fact that the data is maintained internally in the temp variable (and its type).数据在temp变量(及其类型)中内部维护的事实。
  • the fact that there is an internal routine for Fahrenheit/Celsius conversion.事实上,华氏/摄氏度转换有一个内部例程。

So, ideally, it seems to me that the implementor of the class would use the above header but any client of the class would see just the public bits.因此,理想情况下,在我看来,该类的实现者将使用上述标头,但该类的任何客户端只会看到公共位。

Don't get me wrong, I'm not criticising C++ since it meets the stated purpose of preventing clients from using the private bits but, for more complex classes, you could easily work out internal details based on the names, types and signatures of private data and functions.不要误会我的意思,我不是在批评 C++,因为它达到了防止客户端使用私有位的既定目的,但是,对于更复杂的类,您可以根据名称、类型和签名轻松计算出内部细节私有数据和函数。

How does C++ allow implementors to hide this information (assuming it is possible)? C++ 如何允许实现者隐藏这些信息(假设这可能的)? In C, I'd simply use an opaque type so that the internal details would be hidden but how would you do that in C++?在 C 中,我会简单地使用不透明类型,以便隐藏内部细节,但是在 C++ 中您将如何做到这一点?

I suppose I could maintain an separate class, totally hidden from the client and known only to my own code, and then keep an instance of it with a void * in the visible class (casting within my code), but that seems a rather painful process.我想我可以维护一个单独的类,对客户端完全隐藏并且只有我自己的代码知道,然后在可见类中保留一个带有void *的实例(在我的代码中强制转换),但这似乎相当痛苦过程。 Is there an easier way in C++ to achieve the same end?在 C++ 中是否有更简单的方法来达到同样的目的?

C++ uses an idiom known as "pimpl" (private implementation / pointer to implementation) to hide implementation details. C++ 使用称为“pimpl”(私有实现/指向实现的指针)的习语来隐藏实现细节。 Take a look at this MSDN article for details.有关详细信息,请查看这篇 MSDN 文章

In short, you expose your interface in a header file as normal.简而言之,您可以像往常一样在头文件中公开您的接口。 Let's use your code as an example:让我们以您的代码为例:

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

The public interface remains, but the private internals have been replaced with a smart pointer to a private implementation class.公共接口仍然存在,但私有内部结构已被替换为指向私有实现类的智能指针。 This implementation class is located only in the header's corresponding .cpp file, it is not exposed publicly.该实现类仅位于头文件对应的 .cpp 文件中,不公开。

tTemp.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) {}

This also has the added advantage of allowing you to change the internals of your class without changing the header, which means less recompiling for users of your class.这还有一个额外的好处,即允许您在不更改标题的情况下更改类的内部结构,这意味着您的类用户可以减少重新编译。


For a full solution as shown in paxdiablo's pre-C++11 answer, but with unique_ptr instead of void * , you can use the following.对于 paxdiablo 的 pre-C++11 答案中所示的完整解决方案,但使用unique_ptr而不是void * ,您可以使用以下内容。 First ttemp.h :第一个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;
};

Next, the "hidden" implementation in ttemp.cpp :接下来, 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(); }

And, finally, ttemp_test.cpp :最后, 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;
}

And, like paxdiablo's solution, the output is:而且,就像 paxdiablo 的解决方案一样,输出是:

0C is 32F

with the added advantage of more type safety.具有更多类型安全性的额外优势。 This answer is the ideal solution for C++11, see paxdiablo's answer if your compiler is pre-C++11.这个答案是 C++11 的理想解决方案,如果您的编译器是 C++11 之前的,请参阅 paxdiablo 的答案。

Thought I would flesh out the "interface class / factory" technique that Don Wakefield mentions in his comment.以为我会充实 Don Wakefield 在他的评论中提到的“接口类/工厂”技术。 To start with, we abstract away all implementation detail from the interface and define an abstract class that contains only the interface to a Temp :首先,我们从接口中抽象出所有实现细节,并定义一个仅包含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();
};

Clients that want a Temp object call the factory to build one.需要Temp对象的客户调用工厂来构建一个。 The factory could provide some complicated infrastructure that returns different implementations of the interface in different conditions, or something as simple as the "just give me a Temp" factory in this example.工厂可以提供一些复杂的基础设施,在不同的条件下返回接口的不同实现,或者像这个例子中的“只给我一个临时”工厂一样简单。

It's possible for implementation classes to implement the interface by providing overrides for all of the pure virtual function declarations:实现类可以通过为所有纯虚函数声明提供覆盖来实现接口:

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

and somewhere (possibly in the same implementation.cpp ) we need to define the factory:在某处(可能在同一个implementation.cpp )我们需要定义工厂:

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

This approach is a little more easily extensible than pimpl: anyone who wants to can implement the Temp interface instead of there being only one "secret" implementation.这种方法比 pimpl 更容易扩展:任何想要实现Temp接口的人都可以实现,而不是只有一个“秘密”实现。 There's also a bit less boilerplate since it's using the language's builtin mechanisms for virtual dispatch to dispatch interface function calls to implementations.还有一点样板,因为它使用语言的内置机制进行虚拟分派来分派接口函数调用到实现。

There is a non-orthodox approach I've seen used by pugi::xml_document from the pugixml library, and it doesn't have the overheads of pimpl or abstract classes.我从 pugixml 库中看到pugi::xml_document使用了一种非正统的方法,它没有 pimpl 或抽象类的开销。 It goes like this:它是这样的:

You reserve a char array in your publicly exposed class:您在公开公开的类中保留一个char数组:

class tTemp {
public:
    tTemp();
    ~tTemp();
    void setTemp(double);
    double getTemp();
    double getTempF();

    alignas(8) char _[8]; // reserved for private use.
};

Note that注意

  • the alignment and size in this example are hardcoded.本例中的对齐方式和大小是硬编码的。 For a real application you would use an expression to estimate that based on the size of the machine word, for example sizeof(void*)*8 or similar.对于实际应用程序,您将使用表达式根据机器字的大小来估计它,例如sizeof(void*)*8或类似的。
  • adding private won't provide any additional protection because any access to _ can just as well be replaced with a cast to char* .添加private不会提供任何额外的保护,因为对_任何访问都可以用转换为char*来替换。 It's the lack of implementation details in the header that provides the encapsulation.提供封装的是头文件中缺少实现细节。

Next, in the translation unit, you can implement tTemp as follows:接下来,在翻译单元中,您可以按如下方式实现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);
}

This is, surely, more verbose compared to other presented approaches.与其他提出的方法相比,这无疑更加冗长。 But this is the only zero-overhead approach I know that can truly hide all compile-time dependencies from the headers.但这是我所知道的唯一一种零开销方法,可以真正隐藏头文件中的所有编译时依赖项。 Note that it also provides a degree of ABI stability -- you can change tTempImpl as long as its size does not exceed the reserved memory.请注意,它还提供了一定程度的 ABI 稳定性——您可以更改tTempImpl只要其大小不超过保留内存。

For a more detailed discussion about encapsulation in C++ see my True encapsulation in C++ blog post.有关 C++ 中封装的更详细讨论,请参阅我的True encapsulation in C++博客文章。

Private implementation (PIMPL) is the way in which C++ can provide this feature.私有实现 (PIMPL) 是 C++ 提供此功能的方式。 Since I had trouble getting the unique_ptr variation to compile with CygWin g++ 4.3.4, another way to do it is to use a void * within your visible class as follows.由于我无法使用 CygWin g++ 4.3.4 编译unique_ptr变体,因此另一种方法是在可见类中使用void * ,如下所示。 This will allow you to use pre-C++11 compilers, and compilers like the aforementioned gcc which only had experimental support for C++11.这将允许您使用 C++11 之前的编译器,以及前面提到的 gcc 之类的编译器,这些编译器只对 C++11 提供实验性支持。

First, the header file ttemp.h , the one the client includes.首先,头文件ttemp.h ,客户端包含的那个。 This declares opaquely the internal implementation structure so that those internals are fully hidden.这不透明地声明了内部实现结构,以便完全隐藏这些内部结构。 You can see that the only detail revealed is the name of the internal class and variable, neither of which need to reveal any information on how the internals work:您可以看到,唯一显示的细节是内部类和变量的名称,它们都不需要显示有关内部工作原理的任何信息:

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

Next, the implementation file ttemp.cpp which both declares and defines the opaque stuff, and also defines the user-visible details.接下来,实现文件ttemp.cpp既声明和定义了不透明的东西,也定义了用户可见的细节。 Since the user never sees this code, they do not know about how it's implemented:由于用户从未见过此代码,因此他们不知道它是如何实现的:

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

Note that the internal implementation details are not protected in any way from the visible class itself.请注意,内部实现细节不受可见类本身的任何保护。 You could define the internals as a class with accessors and mutators but it seems unnecessary since it should be tightly coupled in this case.可以将内部定义为具有访问器和修改器的类,但这似乎没有必要,因为在这种情况下它应该是紧密耦合的。

One word of note from above: because you're using a pointer to control the hidden aspects, the default shallow copy constructor would cause grief by having two visible objects referring to the same private member (leading to a double-delete in the destructor).上面的一句话:因为你使用一个指针来控制隐藏的方面,默认的浅拷贝构造函数会导致两个可见对象引用同一个私有成员(导致析构函数中的双重删除) . So you need to (as I have) provide a deep-copy copy constructor to prevent this.所以你需要(就像我一样)提供一个深拷贝复制构造函数来防止这种情况。

Lastly, a test program showing how the whole thing hangs together:最后,一个测试程序显示了整个事情是如何结合在一起的:

#include <iostream>
#include "ttemp.h"

int main (void) {
    tTemp t;
    std::cout << t.getTemp() << "C is " << t.getTempF() << "F\n";
    return 0;
}

The output of that code being, of course:该代码的输出当然是:

0C is 32F

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

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