📜 ⬆️ ⬇️

OOP is beautifully cracked with C ++ 14

Introduction


Recently, when working on a project of educational practice, the need arose from one's own code to generate an arbitrary process and simultaneously read its stdout and stderr. Since the application is written exclusively for linux, I decided to deal with epoll at the same time. To start the process on the Internet, a small library was found that does exactly what is needed, and it also wraps the input-output into the usual streams from the standard library (this is <iostream>).


Armed with several articles about epoll, I was already going to write code, if not for one “but” - for epoll, you need access to raw file descriptors, and the author of the library does not provide public access to them. Class methods that return handles are hidden under the heading "protected".

What to do?


The easiest way would be to fix the library code and move the necessary methods to the public section, it would be even better to fork the library and implement the necessary functionality yourself. But the first would be ugly and would give rise to conflicts when updating the library, and the second would take too much time to parse the library code and then test it under several different * nix-systems.
')
Therefore, an insane third thought occurred to me: why not try to somehow “crack” the OOP and “legally” get access to the protected method without interfering with the source code of the library? About what barriers arose on this way and how C ++ 14 helped to overcome them, and the story in this publication will go.

Test environment


For example, use the following simple code:

#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 = af(); //    f()? return val; } 

All examples are compiled under Ubuntu 16.04 using gcc (Ubuntu 5.4.0-6ubuntu1 ~ 16.04.4).

Warning: the following sections provide code that is not recommended for use in production!

Idea 1 is a static method


So, my eyes lit up, the task was set, but how to solve it? We recall the inheritance rules in OOP: protected fields and methods are available only in the scope of the class itself and the classes that inherit it.

The first step is clear: create a class that inherits the target class (in our case, this is the “A” class). And since we want to call a protected method on an already existing object, the static method of our wrapper should help us to get to it:

 class B : public A { public: static int _f(A &a){ return af(); } }; // main(): val = B::_f(a); 

Everything turned out so simple? Not here it was! C ++ prohibits accessing the protected members of the parent class from the child, which the compiler politely reminds us of:

Compilation log
  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 af ();  }


Idea 2 - Type Substitution


Pure thoughts misfired, so the more “dirty” methods go further: deceive the compiler in such a way that he considers that “a” is an object of class “B”, and after that we will call our public method:

 class B : public A { public: int _f(){ return f(); } }; // main(): B *b = (B *) &a; val = b->_f(); 

Bingo! This code does what you need, in the console we see the cherished "protected" and return code 0.

We do not use virtual inheritance and inherit only one class, so the structure of class “B” should remain exactly the same as that of the parent “A”. This means that all virtual methods will also remain at the same offsets as the parent class. It turns out that we, as it were, force the compiler to assume that the method we need is not protected, but public, without at the same time changing the object itself.

It seems that the problem is solved. To access a protected method, we inherit from the class of the target object and by this we clog the scope; spying what type of return value we need for a function or a field ... And so what, every time? The condition of the problem was in a beautiful "hacking". But is such a beautiful decision? Obviously not.

Idea 3 - we write macro


For a macro to be convenient, it must have the following properties:


We define which parts of the code that change from class to class need to be put out of the box:


The first two items legally occupy their places in the macro argument list, but the other two will try to figure out inside the macro using C ++ 14.

Thus, we have the following declaration:

 #define ACCESS_PROTECTED(OBJ, FLD) < > 

Now we solve the problems:

Buildability

To ensure embedding in other expressions, the macro code itself must be an expression. This is where C ++ 11 comes with lambda functions. You can wrap all the macro code into it and call it right there on the spot.

Visibility area clogged problem

Since ancient times, C ++ allows us to define anonymous classes and structures, which is exactly what we need. And the lambda function creates its isolated scope, so that the variables that we will use inside it will not be visible from the outside.

At this point, the macro code looks like this
 #define ACCESS_PROTECTED(OBJ, FLD) (([](??? &o) -> ??? {\ class : ??? {\ public: ??? _f(){ return this->FLD; }\ } *a = (??? *) &o;\ return a->_f();\ })(OBJ)) 


Automatic type inference

Starting with C ++ 11, the keywords “auto” and “decltype” are available in the language for automatic type inference. However, only with C ++ 14 they can be used in declarations of lambda functions and methods. And this is exactly what we need.

The only problem remains with the type of the class from which the inheritance comes. Since an object gets into the lambda function not by copying, but by passing a link to it, decltype (o) from the object will not return the type of the class itself, but the type of reference to it. It is impossible to inherit from this type, and the compiler appropriately scolds:

  access_protected_fields_hack.cpp: 7: 8 error: base type
   class: public decltype (o) {\ 

Std :: remove_reference from the <type_traits> header file comes to the rescue. This template structure provides access to the class type of an object, regardless of whether the class itself was passed or only a reference to it.

We get the final code:

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

It seems to me beautiful. What do you think?

And what about the end code?


For comparison with the code generated when using the macro, the code was compiled, in which the f () method of class “A” was simply made public and called. In both cases, the -O3 flag was used when compiling.

The code of the main () function generated by the compiler turned out to be the same for both cases:

Assembly code
  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) 


It contains only the inline body of the function A :: f ().

Conclusion


The capabilities of the new standard make it possible to conveniently solve problems that were previously difficult to solve or completely impossible. I was interested to find the use of some "chips" of the language in a real project. I hope the publication was also useful for you and it was interesting to read.

PS: As for the main task that I solved in my application, reluctantly I had to throw out the freshly written macro. Still, the conscience is not allowed to apply such code in the open source application. I also had to forget about the epoll, and the reading from stderr and stdout was implemented using istream :: read_some () and sleep for 50 milliseconds between calls.

Source: https://habr.com/ru/post/323412/


All Articles