Как разрешить глобальным функциям доступ к закрытым членам?
Ограничения заключаются в том, что вам не разрешается непосредственно 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()
. Вы должны быть более осторожными в отношении операций перемещения/копирования, а также утечек памяти.