Вступление
Недавно при работе над проектом учебной практики возникла потребность из своего кода порождать произвольный процесс и одновременно читать его stdout и stderr. Так как приложение пишется исключительно для linux, я решил заодно разобраться с epoll. Для запуска процесса на просторах интернета была найдена маленькая библиотека, делающая как раз то, что нужно, да еще и оборачивающая ввод-вывод в привычные потоки из стандартной библиотеки (речь о <iostream>).
Вооружившись несколькими статьями про epoll, я уже было собирался писать код, если бы не одно «но» — для epoll нужен доступ к «сырым» файловым дескрипторам, а автор библиотеки не предоставляет public-доступа к ним. Методы класса, возвращающие дескрипторы, скрыты под грифом «protected».
Что делать?
Самым простым было бы исправить код библиотеки и переместить нужные методы в public-секцию, еще лучше было бы форкнуть библиотеку и реализовать необходимый функционал самому. Но первое было бы некрасиво и сулило бы конфликтами при обновлении библиотеки, а второе заняло бы слишком много времени на разбор кода библиотеки и последующее тестирование под несколькими разными *nix-системами.
Поэтому в голову пришла безумная третья мысль: почему бы не попытаться как-то красиво «взломать» ООП и «легально» получить доступ к protected-методу без вмешательства в исходный код библиотеки? О том, какие преграды возникли на этом пути и как помог C++14 в их преодолении, и пойдет рассказ в данной публикации.
Тестовое окружение
Для примера используем следующий простой код:
#include <iostream>
class A {
protected:
int f(){ std::cout << "Protected" << std::endl; return 0; }
};
int main(int argc, char **argv){
A a;
int val = 1;
//val = a.f(); // как добраться до f()?
return val;
}
Компиляция всех примеров производится под Ubuntu 16.04 с помощью gcc (Ubuntu 5.4.0-6ubuntu1~16.04.4).
Предупреждение: в следующих разделах представлен код, который не рекомендуется применять на продакшене!
Идея 1 — static-метод
Итак, глаза зажглись, задача поставлена, но как ее решить? Вспоминаем правила наследования в ООП: protected поля и методы доступны только в областях видимости самого класса и классов, наследующих его.
Первый шаг понятен: создать класс, наследующий целевой (в нашем случае это класс «A»). А так как мы хотим вызвать защищенный метод у уже существующего объекта, добраться до него нам должен помочь статический метод нашей обертки:
class B : public A {
public:
static int _f(A &a){ return a.f(); }
};
//в main():
val = B::_f(a);
Все оказалось так просто? Не тут-то было! C++ запрещает обращение к защищенным членам родительского класса из дочернего, о чем нам вежливо напоминает компилятор:
access_protected_fields_hack.cpp: In member function ‘int B::_f(A&)’: access_protected_fields_hack.cpp:15:6: error: ‘int A::f()’ is protected int f(){ std::cout << "Protected" << std::endl; return 0; } ^ access_protected_fields_hack.cpp:20:27: error: within this context int _f(A &a){ return a.f(); }
Идея 2 — подмена типа
Чистые помыслы дали осечку, поэтому далее в ход идут более «грязные» методы: обманем компилятор таким образом, чтобы он считал, что «a» является объектом класса «B», и после этого вызовем у него наш публичный метод:
class B : public A {
public:
int _f(){ return f(); }
};
//в main():
B *b = (B *) &a;
val = b->_f();
Бинго! Этот код делает то, что нужно, в консоли мы видим заветное «protected» и код возврата 0.
Мы не используем виртуальное наследование и наследуем только один класс, поэтому структура класса «B» должна остаться в точности такой же, как у родительского «A». А значит и все виртуальные методы тоже останутся по тем же смещениям, что и у родительского класса. Получается, что мы как бы заставляем компилятор считать, что нужный нам метод не защищенный, а публичный, при этом никак не меняя сам объект.
Кажется, что задача решена. Для доступа к защищенному методу мы наследуемся от класса целевого объекта и засоряем этим область видимости; подсматриваем, какой тип возвращаемого значения нам нужен для функции или поля… И что, так каждый раз? Условие задачи было в красивом «взломе». Но является ли красивым такое решение? Очевидно, что нет.
Идея 3 — пишем макрос
Чтобы макрос был удобным, он должен обладать следующими свойствами:
- Встраиваемость в вычисляемые выражения;
- Не засорять область видимости переменными и классами, нужными только для работы макроса;
- Требовать передачи минимума аргументов для выполнения поставленной задачи;
- Ну и, желательно, чтобы не генерировал лишнего конечного кода.
Определимся, какие меняющиеся от класса к классу части кода нужно вынести «за скобки»:
- Целевой объект, к защищенному члену которого мы хотим обратиться;
- Имя защищенного члена;
- Тип возвращаемого значения для нашей публичной функции (соответствует типу поля/метода, к которому мы хотим получить доступ);
- Тип класса, от которого идет наследование.
Первые два пункта законно занимают свои места в списке аргументов макроса, а вот остальные два попытаемся выяснить внутри макроса с помощью C++14.
Таким образом, имеем такое объявление:
#define ACCESS_PROTECTED(OBJ, FLD) <код макроса>
Теперь решаем возникшие проблемы:
Встраиваемость
Чтобы обеспечить встраиваемость в другие выражения, код макроса сам должен быть выражением. Тут к нам на помощь приходит C++11 с лямбда-функциями. Можно обернуть весь код макроса в нее и тут же на месте вызвать.
Проблема засорения области видимости
С давних времен C++ позволяет определять анонимные классы и структуры, что как раз нам и нужно. А лямбда-функция создает свою изолированную область видимости, так что переменные, которые мы будем использовать внутри нее, не будут видны снаружи.
#define ACCESS_PROTECTED(OBJ, FLD) (([](??? &o) -> ??? {
class : ??? {
public: ??? _f(){ return this->FLD; }
} *a = (??? *) &o;
return a->_f();
})(OBJ))
Автоматический вывод типов
Начиная с C++11, в языке доступны ключевые слова «auto» и «decltype» для автоматического вывода типов. Однако только с C++14 их можно использовать в объявлениях лямбда-функций и методов. А это как раз то, что нам нужно.
Остается проблема только с типом класса, от которого идет наследование. Так как в лямбда-функцию объект попадает не копированием, а передачей на него ссылки, то decltype(o) от объекта вернет нам не сам тип класса, а тип ссылки на него. От такого типа наследоваться нельзя, и компилятор соответствующе поругается:
access_protected_fields_hack.cpp:7:8: error: base type ‘A&’ fails to be a struct or class type class : public decltype(o) {
На помощь приходит std::remove_reference из заголовочного файла <type_traits>. Эта шаблонная структура предоставляет доступ к типу класса объекта независимо от того, был ли передан сам класс или только ссылка на него.
Получаем окончательный код:
#include <iostream>
#include <type_traits>
#define ACCESS_PROTECTED(OBJ, FLD) (([](auto &o) -> auto {
class : public std::remove_reference<decltype(o)>::type {
public: auto _f(){ return this->FLD; }
} *a = (decltype(a)) &o;
return a->_f();
})(OBJ))
class A {
protected:
int f(){ std::cout << "Protected" << std::endl; return 0; }
};
int main(int argc, char **argv){
A a;
int val = 1;
val = ACCESS_PROTECTED(a, f());
return val;
}
Мне кажется, красиво. А как считаете вы?
А что же с конечным кодом?
Для сравнения с кодом, генерируемым при использовании макроса, был скомпилирован код, в котором метод f() класса «A» попросту сделан публичным и вызван. В обоих случаях при компиляции использовался флаг -O3.
Генерируемый компилятором код функции main() оказался одинаковым для обоих случаев:
0000000000400730 <main>: 400730: 48 83 ec 08 sub $0x8,%rsp 400734: ba 09 00 00 00 mov $0x9,%edx 400739: be 14 09 40 00 mov $0x400914,%esi 40073e: bf 60 10 60 00 mov $0x601060,%edi 400743: e8 b8 ff ff ff callq 400700 <_ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l@plt> 400748: bf 60 10 60 00 mov $0x601060,%edi 40074d: e8 be ff ff ff callq 400710 <_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@plt> 400752: 31 c0 xor %eax,%eax 400754: 48 83 c4 08 add $0x8,%rsp 400758: c3 retq 400759: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
В нем присутствует лишь заинлайненное тело функции A::f().
Заключение
Возможности из нового стандарта позволяют удобно решать задачи, которые ранее решить было проблематично или вовсе невозможно. Мне было интересно найти применение некоторым «фишкам» языка в реальном проекте. Надеюсь, для вас публикация тоже оказалась полезной и ее было интересно читать.
P.S.: Что же до основной задачи, которую я решал в своем приложении, то скрепя сердце пришлось выкинуть свеженаписанный макрос. Все-таки совесть не позволила применять такой код в опенсорсном приложении. Про epoll также пришлось забыть, а чтение из stderr и stdout было реализовано с помощью istream::read_some() и sleep-ом на 50 миллисекунд между вызовами.
Автор: iassasin