3
\$\begingroup\$

I have created this interface as part of my C++ learning path. This post inspired me a lot.

The idea is to use its concrete implementations to perform some actions before and after any change of the wrapped variable.

In the same fashion, by overriding getValue() a mock can be created for unit testing.

For instance an implementation may notify an observer when the underlying variable changes, another one could be used as a mock for unit testing and so on.

#pragma once

#define CREATE_MEMORY_CELL_IMPLEMENTATION(NAME)                             \
    template<class T>                                                       \
    class NAME : public MemoryManager::IMemoryCell<T>                       \
    {                                                                       \
    public:                                                                 \
        using MemoryManager::IMemoryCell<T>::operator=;                     \
        using MemoryManager::IMemoryCell<T>::value;                         \
        NAME(T v) : IMemoryCell<T>(v) {}                                    \
    protected:                                                              \
        void    setValue(const T& val) override;                            \
        T       getValue() const override;                                  \
    };                                                                      


namespace MemoryManager{

    template<class T>
    class IMemoryCell {
    public:
        IMemoryCell(T v): value(v) {}
        operator T() const { return getValue(); }

        // modifiers
        IMemoryCell& operator=(T v)         { setValue(v); return *this;          }
        IMemoryCell& operator+=(T v)        { setValue(value + v); return *this;  }
        IMemoryCell& operator-=(T v)        { setValue(value - v); return *this;  }
        IMemoryCell& operator*=(T v)        { setValue(value * v); return *this;  }
        IMemoryCell& operator/=(T v)        { setValue(value / v); return *this;  }
        IMemoryCell& operator%=(T v)        { setValue(value % v); return *this;  }
        IMemoryCell& operator&=(T v)        { setValue(value & v); return *this;  }
        IMemoryCell& operator|=(T v)        { setValue(value | v); return *this;  }
        IMemoryCell& operator^=(T v)        { setValue(value ^ v); return *this;  }
        IMemoryCell& operator<<=(T v)       { setValue(value << v); return *this; }
        IMemoryCell& operator>>=(T v)       { setValue(value >> v); return *this; }

        // increment & decrement
        IMemoryCell& operator++()           { setValue(value + 1); return *this; }                  // prefix increment
        IMemoryCell& operator--()           { setValue(value - 1); return *this; }                  // prefix decrement
        T operator++(int)                   { T old = value; setValue(value + 1); return old; }     // postfix increment 
        T operator--(int)                   { T old = value; setValue(value - 1); return old; }     // postfix decrement            


    protected:
        T value;
        virtual void    setValue(const T& val) = 0;
        virtual T       getValue() const { return value; }
    };
}

Here is an example of its usage:

CREATE_MEMORY_CELL_IMPLEMENTATION(testMemory)

template <class T> void testMemory<T>::setValue(const T& val) {
    std::cout << "Old value: " << value << std::endl;
    value = val;
    std::cout << "New value: " << value << std::endl;
};

template <class T> T testMemory<T>::getValue() const {
    return 0x61;
};

What I don't like is that the derived class must explicitly declare the use of the base class assignment operator and value member. As you can see my first idea was to use a macro to create a skeleton for the implementation.

This approach has two major drawbacks:

  1. It limits the possibilities in terms of concrete implementation class. For instance a new member cannot be defined inside the skeleton.
  2. The definition of the overridden methods is cumbersome.

What do you think?

An alternative could be to define it as a concrete class and to pass getValue() and setValue() as function pointers into the constructor. Do you think that could be a better approach?

\$\endgroup\$
0

1 Answer 1

2
\$\begingroup\$

Answers to your questions

As you can see my first idea was to use a macro to create a skeleton for the implementation. [...] What do you think?

I think this is a bad idea. Macros should be avoided where possible. Manually creating a new class that inherits from MemoryManager::IMemoryCell is not much more work than calling the macro and then providing two out-of-class definitions for getValue() and setValue().

An alternative could be to define it as a concrete class and to pass the getValue and setValue as function pointers into the constructor. Do you think that it can be a better approach?

Yes, just like you can pass a comparator function to a std::map.

That said:

It will never be perfect

You will never be able to perfectly wrap a base type and have getter/setter functions that will always be called. It works in simple situations, but consider a function that takes a pointer or reference to a fundamental type, and performs operations via this pointer / reference. The type system will not allow an implicit cast from pointer-to-wrapper to pointer-to-fundamental-type. And if you would somehow provide a conversion operator to do so, then because the type information is lost, the getter and setter functions will no longer be called.

I think the best course of action here is to stop wanting to wrap fundamental types this way, and find other ways to do mocking or notification.

Apart from this, there are a few small things:

Incorrect constructor in macro

The constructor of class NAME is wrong, to access the base class you have to provide the whole type name of the base class:

NAME(T v) : MemoryManager::IMemoryCell<T>(v) {}

Missing default constructor

Your classes don't have default constructors, so the following won't compile:

testMemory<int> x;
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.