Как разрешить глобальным функциям доступ к частным членам

Как разрешить глобальным функциям доступ к закрытым членам?

Ограничения заключаются в том, что вам не разрешается непосредственно friend глобальная функция в объявлении класса. Причина в том, что я не хочу, чтобы пользователям приходилось видеть все эти глобальные функции в файле заголовка. Сами функции определены в файлах реализации, и я бы хотел, чтобы они были спрятаны там как можно лучше.

Теперь вам, наверное, интересно, почему у меня так много таких глобальных функций. Чтобы это было просто, я регистрирую различные функции WNDPROC с окнами в качестве обратных вызовов, и они должны быть глобальными. Кроме того, они должны иметь возможность обновлять информацию, которая в противном случае является частной для различных классов.

Я придумал 2 решения, но оба они немного липкие.

Решение 1. Сделайте все элементы, которым нужны задние двери protected, а не private. В файле реализации объявите класс-чейнджер, который наследуется от исходного класса, но предоставляет публичные геттеры защищенным членам. Когда вам нужны защищенные члены, вы можете просто передать класс смены:

//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, поэтому нет никаких segfaults. Конечно, это ползучее в домене undefined С++, поскольку это предположение зависит от компилятора, но все компиляторы, которые меня волнуют (MSVC и GCC), не изменят размер памяти каждого экземпляра, если не добавлена ​​новая переменная-член.

Решение 2. В заголовочном файле объявите класс 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 и теперь свободно изменить любые собственные частные переменные.

Есть ли более приемлемый способ достичь того, чего я хочу? Я понимаю, что я пытаюсь прокрасться по защите классов С++, но я действительно не хочу связывать каждую глобальную функцию в каждом классе, для которого нужны его частные члены; он уродлив в файлах заголовков и не достаточно просто добавить/удалить больше функций.

РЕДАКТИРОВАТЬ: Используя смесь ответов из озера и Джоэла, я придумал идею, которая делает именно то, что я хотел, однако это делает реализации очень грязными. В принципе, вы определяете класс с различными общедоступными/частными интерфейсами, но фактические данные хранятся как указатель на структуру. Структура определена в файле 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 и получить все функциональные возможности, потому что конструктор в файле реализации создаст _DeviceData и назначит его указателю, который даст вам все возможности api(). Вы должны быть более осторожными в отношении операций перемещения/копирования, а также утечек памяти.

Озеро дало мне основу идеи, поэтому я даю ему кредит. Спасибо, сэр!

Ответ 1

Обычно я решаю эту проблему, извлекая интерфейс прикладного программиста в виде абстрактных классов, который представляет собой набор типов и операций, которые сможет использовать прикладной программник (т.е. пользователь вашей библиотеки).

Затем в моей реализации я объявляю общедоступный все методы и типы, которые будут использоваться в моем пакете другими классами.

Например:

  • 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, я просто перетащу указатель вниз устройство.

Предположим, вам нужен класс Screen, который использует устройство класса, но полностью скрыт от пользователя (и поэтому не будет иметь абстрактного класса API):

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

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

Таким образом, вам не нужно делать что-то приватное только потому, что вы не хотите, чтобы пользователь видел/использовал его. Вы получаете еще один слой абстракции (1 над публикой), который я называю API. У вас будет:

  • API//Метод/Тип, видимый прикладному программисту
  • public//Метод/Тип, видимый для всего пакета библиотеки, но НЕ для пользователя api
  • protected//Метод/Тип видны только для подклассов класса, где он определен
  • private//Метод/Тип local для определяющего класса

Таким образом, вы можете объявлять общедоступные методы, которые необходимо зарегистрировать в качестве метода обратного вызова, без просмотра пользователем.

Наконец, я доставляю содержимое API пользователю вместе с двоичным кодом, так что пользователь будет иметь доступ точно к тому, что я явно указал в API, и ничего больше.

Ответ 2

Возможно, вы задаете конкретный вопрос о кодировании, но я хотел бы сделать шаг назад и изучить причину, по которой вы хотели бы это сделать, и решения для этого.

Нарушение абстракции

Вы принимаете решение на основе частного состояния?

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

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

Это относительно плохо, потому что абстракция Kettle теперь протекает снаружи в функцию SwitchOffKettleIfBoiling в виде связи с частным temperatureC. Это немного лучше:

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

Этот шаблон называется Фасадное изображение. Это полезно для поддержания хорошей абстракции без ограничения вашей реализации.

Ответ 3

Здесь (очень) примерный пример 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
}

Ответ 4

Теперь вам, наверное, интересно, почему у меня так много таких глобальных функции. Чтобы это было просто, я регистрирую различные 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);

EDIT: на основе некоторых ответов на другие ответы похоже, что жизнеспособное решение будет выглядеть более похоже.

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

Ответ 5

Почему бы не использовать методы factory для возврата интерфейса к вашему внутреннему классу, но все же предоставить глобальным доступа к этим внутренним классам? Пример:

// 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 только внутри своего собственного кода. Этот подход хорошо известен и использовал мои многие существующие библиотеки С++ (такие как Java JNI), чтобы разрешить доступ к собственным низкоуровневым битам ваших классов, не подвергая их пользователям.