2

The Code

I have a templated type that follows the Observer Pattern.

template <typename T>
class Subject {
public:
    template <template<typename> typename Observer>
    void AddObserver(Observer<T>& observer)
    {
        observers.push_back([&](const T& value) { observer.OnEvent(value); });
    }

    void Event(const T& value) const
    {
        for (auto& o : observers) { 
            o.OnEvent(value) 
        }
    }

private:
    std::vector<std::function<void(const T&)>> observers;
};

The above works great but only so long as the Observer is a templated type. So I can modify the template signature and accept any type, regardless of whether they are templated.

    template <typename Observer>
    void AddObserver(Observer& observer)
    {
        observers.push_back([&](const T& value) { observer.OnEvent(value); });
    }

The Issue

But now there aren't any compile time checks on type matching between the Subject and the Observer. (EDIT it has been pointed out that there is no guarantee that Observer<T> actually uses T in its OnEvent function signature)

I'd like to add a static cast to the function that checks that the two types Subject<**T**> and the Observer::OnEvent(const **T**&) are the same, to prevent automagic type conversions. Something like the below.

static_cast(std::is_same<T, **type_of_first_param**(Observer::OnEvent)>::value, "Subject/Observer type mistatch.");

Possible approach that I need help with

If I cannot extract the type from the function signature, perhaps I can construct a function signature to which I can compare it?

static_cast(std::is_same<**Observer::MadeUpFunc(const T&)**, Observer::OnEvent>::value, "Subject/Observer type mistatch.");

I'm investigating this QA, and I'm pretty sure it holds the answer I need, just need some time and perhaps guidance to figure it out.

I have tried the following, but get the compile time error

typename specifier refers to non-type member 'OnEvent' in 'Observer'

static_assert(std::is_same<typename Observer::OnEvent, void (Observer::*)(const T&...)>::value, "");

What I have tried


Troyseph
  • 4,960
  • 3
  • 38
  • 61
  • Good spot, I accidentally referred to `Observer` as `Listener`, will fix – Troyseph Aug 27 '20 at 09:29
  • It is not fully clear to me why how your first example and your second example should be different regarding the `observers.push_back([&](const T& value) { observer.OnEvent(value); });` part. – t.niese Aug 27 '20 at 09:31
  • @t.niese in the first example there is complile time type enforcement, any `Observer` and `Subject` had to have matching `T`, in the second, `T` may be different, so long as the compiler is happy with an implicit conversion, which I'd like to prevent. – Troyseph Aug 27 '20 at 09:34
  • 2
    Yes but how does the first one ensure that `OnEvent` has a parameter that matches `T`, that would only be the case if the templated class uses its template for the parameter of `OnEvent`. – t.niese Aug 27 '20 at 09:37
  • @t.niese This is true, I am happy that in our code base that the template type and the function signature will always match, the need for the assert is to help muppets like me notice when we have hooked up mismatching `Subject`s and `Observer`s. Your point basically just means that whatever assert we come up with be useful in both functions. – Troyseph Aug 27 '20 at 09:42
  • 1
    Yes, I just wanted to be sure that I understand it correctly, that the problem also persists in the first example. – t.niese Aug 27 '20 at 09:45
  • 2
    You already demand that `Observer` has a function `OnEvent(OtherT value)`. Why not also demand a typename: `using EventArg = OtherT`, and then using `static_assert >`? – Benny K Aug 27 '20 at 09:50
  • Something like https://stackoverflow.com/a/7943765/1023911? – Werner Henze Aug 27 '20 at 09:59
  • @BennyK I'm not sure I understand how you're specifying or extracting `OtherT`, the asert is exactly what I want but `OtherT` isn't obviously available in this context – Troyseph Aug 27 '20 at 10:02
  • @Troyseph That's what the `using` statement is for – Benny K Aug 27 '20 at 10:03
  • 2
    The point is if you require `template struct Observer { void OnEvent(T e) {}};`, then you also could require `struct Observer {using EventArg = SomeType; void OnEvent(EventArg e) {}]` and you could check against `Observer::EventArg`, it would have the same reliability as your first example. But for sure not the required reliability you are asking for. This `using EventArg = SomeType; ` exists in many places in the std library (and others too) to get information about certain types of a class (e.g. `value_type', `allocator_type`, `size_type`, … in `std::vector`). – t.niese Aug 27 '20 at 10:06
  • @t.niese It would have better reliability than in the first example. The only possible problem is that the definition of `OnEvent` may use a type different from `EventArg`, but for that to happen the user will have to be somewhat malicious. – Benny K Aug 27 '20 at 10:10
  • @BennyK in both cases the user could do `template struct Observer { void OnEvent(SomeOtherType e) {}};` or `struct Observer {using EventArg = SomeType; void OnEvent(SomeOtherType e) {}};`, I don't see much of a difference there. In both cases you don't know the type of the argument of `OnEvent` and need to rely in the user to do it correctly. – t.niese Aug 27 '20 at 10:11
  • @t.niese That's exactly what I said. The added reliability is in the fact that `template struct Observer { void OnEvent(SomeOtherType e) {}};` may be an innocent mistake, and `struct Observer {using EventArg = SomeType; void OnEvent(SomeOtherType e) {}};` is less likely to be innocent. – Benny K Aug 27 '20 at 10:17
  • @BennyK I don't agree with that - but that's opinion based - because normally you would name it like this `template struct Observer`, so it should be equivalent. Anyhow the `using EventArg = SomeType;` approach is so common in various libraries, that I would use that one, because you don't limit your self to the function signature. – t.niese Aug 27 '20 at 10:22
  • @t.niese Why is the function signature limiting? Or rather, why is that limitation bad? As far as I see, it is exactly the limitation I want, and doesn't rely on implementors remembering to correctly add additional boilerplate to each `Observer` type. – Troyseph Aug 27 '20 at 10:27
  • 1
    @Troyseph in this particular case it might not be a problem. I can't make up an example right now, but there can be situations for which you want to get the actual type of the argument or another type of the class. And defining that a class has to have a clear interface providing one or many `using sometype_t = its_actual_type;` and use them accordingly, makes many things easier and consistent. – t.niese Aug 27 '20 at 10:40
  • 1
    @Troyseph in case of events, you could imagine, that you have an inheritance for the events, like `MouseEvent : Event`, `MouseMoveEvent : MouseEvent`, and you could require, that the `OnEvent` consumes at least `MouseEvent` but not `Event`. Then the `static_assert(std::is_same_v)` would not work. This might also be some kind of a strange made up example, but I'm not able to come up with a better one right now. – t.niese Aug 27 '20 at 10:55
  • @t.niese Fair, in my case event types are always builtin types or `POD`s, but I can see how event type inheritance might muddy the water, I suppose in @mpark's answer, the solution would be to substitute `is_same` for something that allows for inheritance? – Troyseph Aug 27 '20 at 10:58
  • @Troyseph as I said, I can't come up with an example right now for which one can't discuss other solutions and whether it is possible to retrieve that type in this case or not. My point was just, that using the technique that is present in the std library and others where types of interest are exposed using the `using some_type_t = its_acutal_type` syntax, helps to get around many headaches and to keep things consistent ;). And because you can use those `some_type_t` in your member function signatures and bodies it can make things more maintainable. – t.niese Aug 27 '20 at 11:03

1 Answers1

2

It's not clear to me what restrictions you want here. The most restrictive would be to enforce something like:

static_assert(std::is_same_v<decltype(&Observer::OnEvent),
                             void (Observer::*)(const T&) const>);

where the OnEvent function couldn't be a template, it couldn't return anything aside from void, it would be required to be marked const, and it would have to take const T&, even if T is int.

mpark
  • 7,574
  • 2
  • 16
  • 18
  • Ah, I was just investigating decltype! Will try this out – Troyseph Aug 27 '20 at 09:59
  • My final code uses `static_assert(std::is_same_v);` with the only difference being that my real world code has a variadic `Subject` template. Very nice answer thank you, much simpler than all of the SFINAE options I was investigating, that required making multiple additional templated types. – Troyseph Aug 27 '20 at 10:30