Convenient approach to composite pattern in C++

Started by
14 comments, last by Brain 9 years, 1 month ago

Hi,

I have a class (A) which contains 'Listeners' or 'Observers' which need to be informed when certain things happen in class A. This currently involves iterating over all listeners and calling a certain notification method for example:


for(Listener listener : listeners)
{
   listener->InformOfSomething(1, 2, 3, 4);
}

I was wondering if there is any convenient way in c++11 to reduce this to a one-liner without having to create additional functions or use lambdas (or even remove the brackets ;)

The approach I'm looking for would be something similar to jQuery where functions act on collections of objects as opposed to single items. For e.g. maybe something as simple as this:


$(listeners).InformOfSomething(1, 2, 3, 4);

(please forgive the shameless copy of jQuery syntax)

Where the $ syntax acts in a similar way to range-based for loops and requires begin() and end() functions on the 'listeners' param which then iterates the collection and substitutes the item in the collection with first parameter of the proceeding function (or more specifically the 'this' pointer when using a class member function).

Anything like this in c++11 or any better way to do it?

Thanks.

Advertisement
You could write a horrible macro to condense your loop to:
DOLLAR(listeners, InformOfSomething(1, 2, 3, 4));

:D (obviously call it something other than $ :lol:)
It may be worth considering a more off-the-shelf solution to the problem, such as boost::signals2.

As for the code in question, C++'s (sometimes questionable) rules around braces and line breaks allow for an obvious one-liner:
for (Listener listener : listeners) listener->InformOfSomething(1, 2, 3, 4);
If you are willing to allow the use of lambdas, std::for_each becomes an option:
std::for_each(listeners.begin(), listeners(), [](Listener &l) {l->InformOfSomething(1, 2, 3, 4);});

There isn't any standard way to forward functions called against containers to the members of those containers (and this is in general a *very* tricky topic for compiled languages).

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

Just write some templated helper functions, if std::for_each() doesn't do what you want.

Feel free to take mine. The header needs to be cleaned up a bit, but meh.


ForEach(array, DoSomething, 17, "Text"); //Global functions, functors, or lambdas.
ForEach(array, &Element::DoSomething, 17, "Text"); //Member functions
ForEachPtr(arrayOfPtrs, &Element::DoSomething, 17, "Text"); //Vector of smart pointers.

If you really wanted to, you could probably cobble together something that looked like this:


ForEach(array, &Element::DoSomething)(17, "Text"); 

Or this:


ForEach(array).(&Element::DoSomething, 17, "Text");  

...but I don't think this is possible:


ForEach(array).DoSomething(17, "Text"); //Not possible.

Not in straight C++ anyway - with the preprocessor you could do:


#define ForEach(container) for(auto &element : container) element

ForEach(arrayOfObjects).DoSomething(17, "Test");

[ideone]

You'd need a separate macro for const iteration. I'd stick to the templated functions, though.

There's a billion options. I'd refrain from all of them, though.

Terseness does not improve readability. Horrible non-standard macros actively work against readability.

Write with an eye towards maintaining your code in the future, not for lazy typists in the present.

Sean Middleditch – Game Systems Engineer – Join my team!



#include <iostream>
#include <vector>

class Listener {
	public:
		void InformOfSomething(int a, int b, int c, int d) {
			std::cout << "Thank you for informing me about " << a << ", " << b << ", " << c << ", " << d << '\n';
		}
};

template<typename C, typename F, typename ...Args>
static inline void callVector(C &&it, F &&f, Args&& ...args) {
	for(auto &t : it)
		(t.*f)(std::forward<Args>(args)...);
}

int main() {
	std::vector<Listener> listeners(3);

	callVector(listeners, &Listener::InformOfSomething, 7, 8, 9, 0);
}

Its probably something of an oversight that there's no version of for_each that simply takes a container and a function to apply to all its elements -- it isn't much more inconvenient to simply use the iterator-pair version, or a short range-for form, like "for (auto l in listeners) l->do_stuff(1,2,3); but a version of for_each that doesn't use an iterator pair would eliminate unnecessary verbosity of the most-common use case:

Luckily, with non-member begin() and non-member end(), I think (that is to say **disclaimer** no warranty expressed or implied) you can implement this easily in those terms and in the terms of the existing iterator-pair version of for_each. This is not tested, but it would look something like this:

template<class Input, class Function>

Function for_each(Input in, Function fn)

{

return for_each(std::begin(in), std::end(in), fn);

}

and usage would look something like so:

for_each(listeners, [](Listener &l) {l->InformOfSomething(1, 2, 3, 4);}

I think then, that you could reduce out the lambda with a function object template of some kind -- something like a "call_member_with_args" template, who's usage could look something like this:

for_each(listners, call(Listener::InformOfSomething)(1,2,3,4));

But I'll leave implementation as an exercise to the reader. (I think probably you could use std::function, but its not lightweight -- I wonder, though, if its exactly as heavy as an encapsulated member function anyways)

[EDIT] ... and Erik's C++11 variadic-template-fu is both stronger and quicker than my outdated C++06 template-fu

throw table_exception("(? ???)? ? ???");

Almost at the $ syntax, and just for fun :)


#include <iostream>
#include <vector>

class Listener {
	public:
		void InformOfSomething(int a, int b, int c, int d) {
			std::cout << "Thank you for informing me about " << a << ", " << b << ", " << c << ", " << d << '\n';
		}
};

template<typename C, typename F>
struct Caller {
	C &c;
	F f;
	Caller(C &c, F &f) : c(c), f(f) {}

	template<typename ...Args>
	void operator()(Args&& ...args) {
		for(auto &t : c)
			(t.*f)(std::forward<Args>(args)...);
	}
};

template<typename C, typename F>
static inline Caller<C, F> operator->*(C &c, F &&f) {
	return Caller<C, F>(c, f);
}

struct Int {
	operator int() {
		return rand();
	}
};

int main() {
	std::vector<Listener> listeners(3);

	(listeners->*&Listener::InformOfSomething)(2, Int(), 4, 5);
}


template<typename C, typename F>
static inline Caller<C, F> operator->*(C &c, F &&f) {


Yeah, okay. I'm totally okay with what just happened and am totally fine with how readable this is.

static ::operator->*... Yup!
I've been playing with Erik's code a bit, and another candidate is the almost-never overloaded comma operator. I didn't post the code since I'm unfamiliar with troubles that might arise. I suspect its safe, as comma has lowest precedence, and this use is similar to other uses in C++ DSL implementations, or as its used to collect terms in parts of boost, I'm just unfamiliar with potential hazards that might be found.

Usage looks like:

(Listeners, &Listener::InformOfSomething)(1,2,3,4);

Which doesn't read too poorly is you read the first set of parens as something like "cartesean product of this collection and this member function"

throw table_exception("(? ???)? ? ???");

This topic is closed to new replies.

Advertisement