10

如何允许全局函数访问私有成员?

限制是您不允许friend在类声明中直接使用全局函数。原因是我不希望用户必须在头文件中看到所有这些全局函数。函数本身是在实现文件中定义的,我想尽可能地将它们隐藏在那里。

现在你可能想知道为什么我有这么多这些全局函数。为了简单起见,我在窗口中注册了各种 WNDPROC 函数作为回调,它们必须是全局的。此外,他们必须能够更新对各种类来说是私有的信息。

我想出了 2 个解决方案,但都有些棘手。

解决方案 1. 让所有需要后门的成员,protected而不是private. 在实现文件中,声明一个从原始类继承但为受保护成员提供公共 getter 的类更改器。当您需要受保护的成员时,您可以简单地转换为 changer 类:

//Device.h
class Device{
protected:
  std::map<int,int> somethingPrivate;
};

//Device.cpp
DeviceChanger : public Device{
private:
  DeviceChanger(){} //these are not allowed to actually be constructed
public:
  inline std::map<int,int>& getMap(){ return somethingPrivate; }
};

void foo(Device* pDevice){ ((DeviceChanger*)pDevice)->getMap(); }

当然,继承这个类的用户现在可以访问受保护的变量,但它至少允许我隐藏大部分重要的私有变量,因为它们可以保持私有。

这是可行的,因为DeviceChanger实例具有与 完全相同的内存结构Device,因此没有任何段错误。当然,这正在蔓延到未定义的 C++ 域,因为该假设依赖于编译器,但我关心的所有编译器(MSVC 和 GCC)都不会改变每个实例的内存占用,除非添加了新的成员变量。

解决方案2. 在头文件中,声明一个friend changer类。在实现文件中,定义友元类并使用它通过静态函数获取私有成员。

//Device.h
class DeviceChanger;
class Device{
  friend DeviceChanger;
private:
  std::map<int,int> somethingPrivate;
};

//Device.cpp
class DeviceChanger{
public:
  static inline std::map<int,int>& getMap(Device* pDevice){ return pDevice->somethingPrivate; }
};

void foo(Device* pDevice){ DeviceChanger::getMap(pDevice); }

虽然这确实为我的所有课程添加了一个朋友(这很烦人),但只有一个朋友可以将信息转发给任何需要它的全局函数。当然,用户现在可以简单地定义自己的DeviceChanger类,然后自己自由更改任何私有变量。

有没有更可接受的方式来实现我想要的?我意识到我正试图绕过 C++ 类保护,但我真的不想让每个需要访问其私有成员的类中的每个全局函数都成为朋友。它在头文件中很丑陋,并且不容易添加/删除更多功能。

编辑: 混合使用 Lake 和 Joel 的答案,我想出了一个完全符合我要求的想法,但是它使实现变得非常肮脏。基本上,您定义了一个具有各种公共/私有接口的类,但它的实际数据存储为指向结构的指针。该结构在 cpp 文件中定义,因此它的所有成员对该 cpp 文件中的任何内容都是公共的。即使用户定义了自己的版本,也只会使用实现文件中的版本。

//Device.h
struct _DeviceData;
class Device {
private:
  _DeviceData* dd;
public:
  //there are ways around needing this function, however including 
  //this makes the example far more simple.
  //Users can't do anything with this because they don't know what a _DeviceData is.
  _DeviceData& _getdd(){ return *dd; }

  void api();
};

//Device.cpp
struct _DeviceData* { bool member; };
void foo(Device* pDevice){ pDevice->_getdd().member = true; }

这基本上意味着Device除了指向某个数据块的指针之外,每个实例都是完全空的,但它为访问用户可以使用的数据提供了一个接口。当然,接口完全是在cpp文件中实现的。

此外,这使得数据非常私密,甚至用户都看不到成员名称和类型,但您仍然可以在实现文件中自由使用它们。最后,您可以继承Device并获取所有功能,因为实现文件中的构造函数将创建 a_DeviceData并将其分配给指针,从而为您提供所有api()功能。您确实必须更加小心移动/复制操作以及内存泄漏。

莱克给了我这个想法的基础,所以我给了他信任。谢谢你,先生!

4

5 回答 5

3

我通常通过以抽象类的形式提取应用程序程序员接口来解决这个问题,抽象类是应用程序程序员(即您的库的用户)将能够使用的一组类型和操作。

然后,在我的实现中,我将其他类将在我的包中使用的所有方法和类型声明为public 。

例如:

  • API:IDevice.h
  • 内部:Device.h Device.cpp

我以类似于以下方式定义 API 类:

class IDevice {
 public:
  // What the api user can do with the device
  virtual void useMe() = 0;
};

然后,在我的库中(不暴露给用户界面):

class Device : public IDevice {
 public:
   void useMe(); // Implementation

   void hiddenToUser(); // Method to use from other classes, but hidden to the user
}

然后,对于作为 API 一部分的每个标头(接口),我将使用 IDevice 类型而不是 Device 类型,并且当在内部我必须使用 Device 类时,我只需将指针向下转换为 Device。

假设您需要一个使用 Device 类的 Screen 类,但对用户完全隐藏(因此不会有任何 API 抽象类要实现):

#include "Device.h"
class Screen {
   void doSomethingWithADevice( Device* device );
}

// Screen.cpp
void Screen::doSomethingWithADevice( Device* device ){
   device->hiddenToUser();
}

这样,您不必仅仅因为您不希望用户看到/使用它而将其设为私有。您获得了我称之为 API 的进一步抽象层(公共之上的 1 层)。你将会有:

  1. API // 对应用程序程序员可见的方法/类型
  2. public // 方法/类型对整个库包可见,但对 api 用户不可见
  3. protected // 方法/类型仅对定义它的类的子类可见
  4. private // 定义类的本地方法/类型

因此,您可以将需要注册的公共方法声明为回调方法,而用户不会看到它们。

最后,我将 API 的内容与二进制文件一起提供给用户,这样用户就可以完全访问我在 API 中明确定义的内容,而不能访问其他任何内容。

于 2013-08-09T01:31:13.193 回答
1

您可能会问一个特定的编码问题,但我想退后一步,检查您想要这样做的原因以及解决方案。

打破抽象

您是否根据私人状态做出决定?

class Kettle {
private:
    int temperatureC;
public:
    void SwitchOff();
};

void SwitchOffKettleIfBoiling(Kettle& k) {
    if (k.temperatureC > 100) { // need to examine Kettle private state
        k.SwitchOff();
    }
}

这是相对糟糕的,因为 now 的抽象以耦合到 private 的形式Kettle泄漏到函数外部。这有点好:SwitchOffKettleIfBoilingtemperatureC

class Kettle {
private:
    int temperatureC;
public:
    void SwitchOffIfBoiling() {
        if (temperatureC > 100) {
            SwitchOff();
        }
    }
};

void SwitchOffKettleIfBoiling(Kettle& k) {
    k.SwitchOffIfBoiling();
}

这种做法称为告诉,不要问

多重责任

有时,您拥有明显相关但用于不同角色的数据。看这个例子:

class Car {
private:
    int statusFactor;
public:
    void Drive();
};

void DriveSomewhere(Car& c) {
    c.Drive();
    // ...
}
void ShowOffSomething(const Car &c) {
    // How can we access statusFactor, without also exposing it to DriveSomewhere?
}

处理这个问题的一种方法是使用代表这些职责的接口。

class IVehicle {
public:
    virtual void Drive() = 0;
};
class IStatusSymbol {
public:
    virtual int GetStatusFactor() const = 0;
};
class Car : public IVehicle, public IStatusSymbol {
    // ...
};

void DriveSomewhere(IVehicle& v) {
    v.Drive();
    // ...
}
void ShowOffSomething(const IStatusSymbol &s) {
    int status = s.GetStatusFactor();
    // ...
}

这种模式称为Facade 模式。它对于在不限制您的实现的情况下保持良好的抽象很有用。

于 2013-08-09T01:22:56.997 回答
1

这是一个(非常)粗略的 pimpl 示例。

 //Device.h
class DeviceImpl;

class Device {
public:
    Device();

private:
  std::unique_ptr<DeviceImpl> pimpl;
};

//Device.cpp
class DeviceImpl {
public:
    friend LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
private:
    std::map<int,int> somethingPrivate;
};

Device::Device()
    : pimpl(new DeviceImpl)
{
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    DeviceImpl* pimpl = reinterpret_cast<DeviceImpl*>(GetWindowLongPtr(hWnd, GWLP_USERDATA));

    use(pimpl->somethingPrivate);

    // omitting the SetWindowLongPtr that you have to do before calling GetWindowLongPtr,
    // but the concept is the same - you'd probably do it in WM_CREATE
}
于 2013-08-09T01:25:07.977 回答
1

现在你可能想知道为什么我有这么多这些全局函数。为了简单起见,我在窗口中注册了各种 WNDPROC 函数作为回调,它们必须是全局的。此外,他们必须能够更新对各种类来说是私有的信息。

您可以使用静态成员函数而不是全局函数来执行此操作。然后你就可以得到私人成员就好了。代码看起来有点像这样。

class MyClass {
  private:
    std::string some_data;
    static void onEvent( void * user_data );
};

void MyClass::onEvent( void * user_data ) {
  MyClass* obj = (MyClass*)(user_data);
  std::cout<<some_data<<std::endl;
};

...

register_callback( &MyClass::onEvent, &myClassInstance);

唯一的问题是onEvent函数名称的公开。解决方案是提取一个接口,这样您的私有数据或函数就不会被暴露(因为 IMO 泄漏私有实现与泄漏私有函数的名称一样糟糕。)

// Header File.
class IMyClass {
  //...
  // public stuff goes here
  //...

};

// Implementation file.
class MyClass : public IMyClass {
  private:
    std::string some_data;
    static void onEvent( void * user_data );
};

void MyClass::onEvent( void * user_data ) {
  MyClass* obj = (MyClass*)(user_data);
  std::cout<<some_data<<std::endl;
};

...

register_callback( &MyClass::onEvent, &myClassInstance);

编辑:根据对其他答案的一些回应,一个可行的解决方案看起来更像这样。

// IUSBDeviceBackend.h (private)
class IUSBDeviceBackend {
public:
   virtual void update(USBUpdateData data)=0;
   virtual bool resondsTo(USBUpdateCode code)=0
   virtual ~IUSBDeviveBackend() {}
};

// IUSBDeviceUI.h (public)   
class IUSBDeviceUI {
public:
  virtual void showit()=0;
};

// MyDevice.h & MyDevice.cpp (both private)
class MyDevice : public IUSBDeviceBackend, public IUSBDeviceUI {
  void update(USBUpdateData data) { dataMap[data.key]=data.value; }
  bool resondsTo(USBUpdateCode code) { return code==7; }
  void showit(){ ... }
};

// main.cpp
main() {
  std::vector<IUSBDeviceBackedn*> registry;
  MyDevice dev;
  registry.push_back(this);
  set_user_data(&registry);
  // ...
}

void mycallback(void* user_daya) {
  std::vector<IUSBDeviceBackedn>* devices = reinterpret_cast<std::vector<IUSBDeviceBackedn>*>(user_data);

  for(unsigned int i=0; i<devices->size(); ++i) {
    if( (*devices)[i]->resondsTo( data.code ) ) { (*devices)[i]->update(data); }
  }
}
于 2013-08-09T02:32:00.700 回答
0

为什么不使用工厂方法将接口返回到您的内部类,但仍让全局变量访问这些内部类?例子:

// IDriver.h public interface:
class IDriver {
  public:
    virtual int getFoo() = 0;
    // ... other public interface methods.

    // The implementation of this method will contain code to return a Driver:
    static IDriver* getDriver();
};

// Driver.h internal interface (available to WNDPROC functions):
class Driver : public IDriver {
  public:
    int getFoo();           // Must provide this in the real Driver.
    void setFoo(int aFoo);  // Provide internal methods that are not in the public interface,
                            // but still available to your WNDPROC functions
}

// In Driver.cc
IDriver* IDriver::getDriver() { return new Driver(); }

使用这种方法,IDriver.h 将是一个众所周知的公共头文件,但您只能在自己的代码内部使用 Driver.h。这种方法是众所周知的,并且使用了我的许多现有 C+ 库(例如 Java 的 JNI)来允许访问您的类的本机低级位,而不会将其暴露给用户。

于 2013-08-09T01:52:53.307 回答