Search code examples
c++templatesmember-variables

Template a member variable


Consider the following two classes:

class LunchBox
{
  public:
    std::vector<Apple> m_apples;
};

and

class ClassRoom
{
  public:
    std::vector<Student> m_students;
};

The classes are alike in that they both contain a member variable vector of objects; however, they are unalike in that the vector's objects are different and the member variables have different names.

I would like to write a template that takes either LunchBox or ClassRoom as a template argument (or some other parameter) and an existing object of the same type (similar to a std::shared_ptr). The template would return an object that adds a getNthElement(int i); member function to improve accessing the methods. Usage would be like:

// lunchBox is a previously initialized LunchBox
// object with apples already pushed into m_apples
auto lunchBoxWithAccessor = MyTemplate<LunchBox>(lunchBox);
auto apple3 = lunchBoxWithAccessor.getNthElement(3);

I would like to do this without writing template specializations for each class (which likely would require specifying the member variable to operate on in some way). Preferably, I do not want to modify the LunchBox or ClassRoom classes. Is writing such a template possible?


Solution

  • You can minimize the amount of code that has to be written for each class -- it doesn't have to be a template specialization and it doesn't have to be an entire class.

    class LunchBox
    {
      public:
        std::vector<Apple> m_apples;
    };
    
    class ClassRoom
    {
      public:
        std::vector<Student> m_students;
    };
    
    // you need one function per type, to provide the member name
    auto& get_associated_vector( Student& s ) { return s.m_apples; }
    auto& get_associated_vector( ClassRoom& r ) { return r.m_students; }
    
    // and then the decorator is generic
    template<typename T>
    class accessor_decorator
    {
         T& peer;
    public:
         auto& getNthElement( int i ) { return get_associated_vector(peer).at(i); }
    
         auto& takeRandomElement( int i ) { ... }
    
         // many more ways to manipulate the associated vector
    
         auto operator->() { return &peer; }
    };
    
    LunchBox lunchBox{};
    accessor_decorator<LunchBox> lunchBoxWithAccessor{lunchBox};
    auto apple3 = lunchBoxWithAccessor.getNthElement(3);
    

    The simple helper function overload should ideally be in the same namespace as the type, to make argument-dependent lookup work (aka Koenig lookup).

    It's also possible to specify the member at the point of construction, if you prefer to do that:

    template<typename T, typename TMemberCollection>
    struct accessor_decorator
    {
         // public to make aggregate initialization work
         // can be private if constructor is written
         T& peer;
         TMemberCollection const member;
    
    public:
         auto& getNthElement( int i ) { return (peer.*member).at(i); }
    
         auto& takeRandomElement( int i ) { ... }
    
         // many more ways to manipulate the associated vector
    
         auto operator->() { return &peer; }
    };
    
    template<typename T, typename TMemberCollection>
    auto make_accessor_decorator(T& object, TMemberCollection T::*member)
         -> accessor_decorator<T, decltype(member)>
    {
        return { object, member };
    }
    
    LunchBox lunchBox{};
    auto lunchBoxWithAccessor = make_accessor_decorator(lunchBox, &LunchBox::m_apples);
    auto apple3 = lunchBoxWithAccessor.getNthElement(3);