Anthony
Anthony

Reputation: 12407

Can I statically prevent one function from calling another?

I have the following interfaces:

class T {
public:
    // Called in parallel
    virtual unsigned validate () = 0;

    // Called with a lock taken out
    virtual unsigned update () = 0;
};

template <typename DataType>
class Cache {
public:
    // Gets the requested object.
    // If it doesn't exist in memory, go to SQL.
    unsigned fetch (DataType &data);


    // Gets the requested object.
    // If it's not in memory, returns NOT_FOUND.
    unsigned find (DataType &data);
};

What I'd like to achieve: I would like to be able to have compilation fail if fetch is called during update. Effectively, I'd like to disable the function statically, based on the call site. Something like,

std::enable_if <callsite_is_not_implementation_of_T_update, unsigned> 
fetch (DataType &data);

Usage would work something like this:

class A_T : public T {
public:
    virtual unsigned validate () {
        global_cache.fetch (object); // OK
    }

    virtual unsigned update () {
        global_cache.find (object); // Also OK

        global_cache.fetch (object); // FAIL!
    }
};


Background

There are approximately 500 implementations of T in my project.

The application loops in many threads and calls validate for many instances of T in parallel. Then a global lock is taken out, and update is called. Hence, the speed of update is crucial. The general attitude is to take whatever time you need during validate, but update should be as lean as possible.

My problem is with the use ofCache. A Cache is basically an in-memory cache of data objects from SQL.

The policy is to never call Cache::fetch during update because of the potential SQL round-trip while holding a lock. We're all working hard to foster this mindset within the team. Unfortunately, some of these still sneak in, and get past code review. We only notice them when the system is under heavy load and everything grinds to a halt.

I'd like to develop a safety net and prevent this kind of thing from being allowed at all. What I'd like to achieve is to have compilation fail if Cache::fetch is called from T::update.

I don't mind if it can be worked around. The point is to have it as a barrier; the only way you could make a mistake is to intentionally do it.


What I've Got So Far

I've made it a little bit of the way there, although not quite what I'm really after. For instance, I'd prefer not to have to change every single call to fetch.

template <typename Impl>
class cache_key  {
    cache_key() { }
    friend unsigned Impl::validate();
};

#define CACHE_KEY cache_key<std::remove_pointer<decltype(this)>::type> ()

So now Cache::fetch looks like:

unsigned fetch (DataType &object, const cache_key &key);

And an implementation of T might look like this:

class A_T : public T {
public:
    virtual unsigned validate () {
        global_cache.fetch (object, CACHE_KEY); // OK
    }

    virtual unsigned update () {
        global_cache.fetch (object, CACHE_KEY); // Can't do it!
    }
};

Upvotes: 11

Views: 286

Answers (2)

oblitum
oblitum

Reputation: 12016

This is just a stupid POC, which I don't recommend, and may not fulfill your expectations:

struct T {
    // Called in parallel
    virtual unsigned validate () = 0;

    // Called with a lock taken out
    virtual unsigned update () = 0;
};

struct A_T : T {
    unsigned validate () override;
    unsigned update () override;
};

template <typename DataType>
class Cache {
private:
    class Privileged {
        friend class Cache<DataType>;

        friend unsigned A_T::validate();

        Privileged( Cache<DataType> &outer ) : outer(outer) {}

        // Gets the requested object.
        // If it doesn't exist in memory, go to SQL.
        unsigned fetch (DataType &data);

        Cache<DataType> &outer;
    };

public:
    Privileged privileged { *this };

    // Gets the requested object.
    // If it's not in memory, returns NOT_FOUND.
    unsigned find (DataType &data);
};

Cache<int> global_cache;

unsigned A_T::validate () {
    int object;
    global_cache.privileged.fetch (object); // OK
    return 1;
}

unsigned A_T::update () {
    int object;

    global_cache.find (object); // Also OK
    global_cache.privileged.fetch (object); // FAIL!

    return 1;
}

Upvotes: 0

Ilya Kobelevskiy
Ilya Kobelevskiy

Reputation: 5345

I'm not aware of compilation time error generation, but it can be done to generate run-time error with updates only to base class.

A way to do it is to call update through a non-virtual proxy function in a base class, which would set state to the base class to detect that we are in update and therefore fetch should not be called.

class updateWatcher()
{
public:
updateWatcher(bool *valIn) : val(valIn) {*val=true;}
~updateWatcher() {*val=false;}
private:
bool* val;
}

class T {
public:
    // Called in parallel
    virtual unsigned validate () = 0;

    unsigned updateProxy()
    {
         updateWatcher(&inUpdate); //exception safe tracker we are in update
         return update();
    }

    void
protected:
    // Called with a lock taken out
    virtual unsigned update () = 0;

    bool inUpdate; // tells if we are in update or not
};

class A_T : public T {

public:
    virtual unsigned validate () {
        global_cache.fetch (object,inUpdate); // OK
    }

    virtual unsigned update () {
        global_cache.find (object); // Also OK

        global_cache.fetch (object,inUpdate); // FAIL (put assert in global_cache.fetch) !
    }
};

This will not produce compilation but run-time error, benefit is that there is no need to update any implementations (except replacing all global_cache.fetch (...); by global_cache.fetch (...,inUpdate); and calls to update() to updateProxy(); in all implementations, which can be automated efficiently). You can then integrate some automated tests as part of the build environment to catch the asserts.

Upvotes: 1

Related Questions