How to envelop a class with suffixes and prefixes and discover properties fields in C++
Enveloping a class
I found myself in the following situation.
There is a class foo
, with a lot of member functions.
In more than one case, before any of the operations, another function needed to be called, and after any operation, another function needed to be called.
Most usages looked thus like
void bar(){
call_before_using_foo();
foo.doit();
foo.doit2();
}
The code works but is very brittle as there are a lot of repetitions.
A possible approach would have been to create a wrapper class for foo
, let’s call it foo2
, and wrap every function call.
It’s a lot of boilerplate code, even if it has the advantage of providing a more clean and easy to use interface.
Fortunately, after experimenting for some time, I came up with a more generic solution, that I probably cannot use in this specific use case, but that provides a useful tool for other situations.
operator->
operator->
has the interesting property that it can hide easily function.
Let’s take for example std::unique_ptr
. It defines operator->
to return the internal pointer.
Thus in the following snippet
struct Foo{
void doit(){}
};
auto foo = std::make_unique<Foo>();
foo->doit();
foo->doit()
calls operator->
of std::unique_ptr
, it returns a pointer, and then calls the member function doit()
.
The code is equivalent to:
struct Foo{
void doit(){}
};
auto foo = std::make_unique<Foo>();
Foo* f = foo.get();
f->doit();
A prefix class
So, by overloading operator->
(or operator*
), it is possible to "hide" a function call.
It is trivial to create a class that calls another function when any member function of the internal class is called
template <class T>
struct prefix {
T obj;
template<typename... Args>
explicit prefix(Args&& ... args) :
obj(std::forward<Args>(args)..) {}
T* operator->() {
std::puts("prefix");
return &obj;
}
const T* operator->() const {
std::puts("prefix");
return &obj;
}
};
The class prefix
can be used in the following way:
struct X {
void doit(){
std::puts("doit");
}
};
void foo(){
prefix<X> x;
x->doit();
}
This should print to the console "prefix" and then "doit".
The wrap class; prefix and suffix hooks
It is a little more difficult to provide a suffix function, as it should get executed after operator->
returns.
As in other situations, adding another layer of indirection, in this case, a proxy type can solve the problem.
template <class T>
struct proxy_wrap {
T& obj;
proxy_wrap(T& obj_) : obj(obj_) {}
~proxy_wrap() {
std::puts("suffix");
}
T* operator->() {
return &obj;
}
};
template <class T>
struct wrap {
T obj;
template<typename... Args>
explicit wrap(Args&& ... args) :
obj(std::forward<Args>(args)...) {}
proxy_wrap<T> operator->() {
std::puts("prefix");
return proxy_wrap<S,T>(obj, static_cast<S&>(*this));
}
proxy_wrap<const T> operator->() const {
std::puts("prefix");
return proxy_wrap<const T>(obj);
}
};
struct X {
void doit() const {
std::puts("doit");
}
};
int main(){
wrapX x;
x->doit();
}
This will print "prefix", then "doit", and at last "suffix".
A generic wrap class, with a custom prefix and suffix
So far, so good, but wrap
is hardly reusable since our prefix and suffix actions are hardcoded.
We can pass as a separate template parameter what we want to execute. CRTP seems like a good solution to apply here.
template <class S, class T>
struct proxy_wrap {
T& obj;
S& pre;
proxy_wrap(T& obj_, S& pre_) : obj(obj_) , pre(pre_){}
~proxy_wrap() {
pre.suffix(obj);
}
T* operator->() {
return &obj;
}
};
template <class S, class T>
struct wrap {
T obj;
template<typename... Args>
explicit wrap(Args&& ... args) :
obj(std::forward<Args>(args)...) {}
proxy_wrap<S, T> operator->() {
static_cast<S&>(*this).prefix(obj);
return proxy_wrap<S,T>(obj, static_cast<S&>(*this));
}
proxy_wrap<const S, const T> operator->() const {
static_cast<const S&>(*this).prefix(obj);
return proxy_wrap<const S, const T>(obj, static_cast<const S&>(*this));
}
};
At this point, we can create a custom wrapper for our specific need, that just needs to provide suffix
and prefix
as member functions:
struct X {
int i = 0;
int doit() {
++i;
std::cout << "doit " << i << '\n';
return i;
}
int doit() const {
std::cout << "doit " << i << '\n';
return i;
}
};
struct wrapX : wrap<wrapX, X> {
void prefix(X& x) {
++x.i;
std::cout << "prefix " << x.i << '\n';
}
void prefix(const X& x) const {
std::cout << "prefix " << x.i << '\n';
}
void suffix(X& x) {
++x.i;
std::cout << "suffix " << x.i << '\n';
}
void suffix(const X& x) const {
std::cout << "suffix " << x.i << '\n';
}
};
and use it similarly to before
void foo() {
wrapX x;
int res = x->doit();
}
It should print, "prefix 1", "doit 2" and "suffix 3".
The complete example can be found at compiler explorer.
Side note: performance considerations:
In simple examples, GCC already with -O1
was able to optimize everything away. Clang, with -O2
, was also able to remove everything that was not necessary. MSVC, on the other hand, was not that good. It seems to remove all unnecessary calls but did not remove all the generated dead code, I should probably investigate further, maybe I’m missing some compiler flag.
Properties in C++
While testing, I tried to read directly the value of the member variable i
, so I wrote
void foo() {
wrapX x;
int res = x->doit();
int value = x->i;
}
To my surprise, the value of value
is not 3
, but 5
, and in the console, two additional lines appeared: "prefix 4" and "suffix 5".
Of course operator->
is also called when accessing member variables, not only when calling member functions.
Thus without realizing it, the wrap
does not only permit to add prefixes and suffixes to every function call but also to implement properties in C++.
The main use case I’ve encountered with them are with GUIS, as they make the code much easier to read and write.
In those languages, it is possible to write something like
procedure TForm1.Button1Click(Sender: TObject);
begin
Button1.Caption:='Button was clicked '; // Changes the caption of the GUI in real-time
end;
instead of explicitly calling getter and setter functions.
It’s syntactic sugar, but so are lambdas, operator->
, operator++
, and many other things that give us the possibility to design more powerful interfaces.
wrap
can be used the same way, for the sake of brevity, instead of developing a whole GUI, let’s just change the terminal output when the button is "clicked".
struct button {
std::string caption;
};
struct wrap_button : wrap<wrap_button, button> {
void prefix(const button&) const {
}
void suffix(const button& b) const {
std::cout << b.caption << '\n';
}
};
void obButtonClick(wrap_button& b){
b->caption = "Button was clicked!";
}
int main(){
wrap_button b;
obButtonClick(b);
}
In a similar way, the suffix can be used to read values, for example, from an input field.
#include <string>
struct input_field {
std::string content;
input_field(std::string content_) : content(std::move(content_)){}
};
struct wrap_input_field : wrap<wrap_input_field, input_field> {
using wrap::wrap;
void prefix(const input_field& f) const {
std::cout << "query text from gui: " << f.content << "length: " << f.content.size() << '\n';
}
void suffix(const input_field& f) const {
std::cout << "write text to gui: " << f.content << "length: " << f.content.size() << '\n';
}
};
void rtrim(std::string& s){
s.erase(s.find_last_not_of(" \n\r\t")+1);
}
void rtrim_input_field(wrap_input_field& b){
rtrim(b->content);
}
int main(){
wrap_input_field ff("Hello World! ");
rtrim_input_field(ff);
}
The downside of those properties is that as they are so hidden, that perfectly valid-looking code might be troublesome. For example, the following snippet queries the text twice and then writes it twice.
void rtrim_input_field(wrap_input_field& b){
b->content.erase(b->content.find_last_not_of(" \n\r\t")+1);
}
In this case, as the operation is idempotent, it is "only" a performance issue, but generally, it can have much bigger and more problematic side effects.
Do you want to share your opinion? Or is there an error, some parts that are not clear enough?
You can contact me anytime.