Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 398
chessofnerd Avatar asked May 07 '17 03:05

chessofnerd


4 Answers

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);
like image 142
Ben Voigt Avatar answered Oct 17 '22 10:10

Ben Voigt


A simple way to do this is define a trait struct that has specializations with just the information that makes each case different. Then you have a template class that uses this traits type:

// Declare traits type. There is no definition though. Only specializations.
template <typename>
struct AccessorTraits;

// Specialize traits type for LunchBox.
template <>
struct AccessorTraits<LunchBox>
{
    typedef Apple &reference_type;

    static reference_type getNthElement(LunchBox &box, std::size_t i)
    {
        return box.m_apples[i];
    }
};

// Specialize traits type for ClassRoom.
template <>
struct AccessorTraits<ClassRoom>
{
    typedef Student &reference_type;

    static reference_type getNthElement(ClassRoom &box, std::size_t i)
    {
        return box.m_students[i];
    }
};

// Template accessor; uses traits for types and implementation.
template <typename T>
class Accessor
{
public:
    Accessor(T &pv) : v(pv) { }

    typename AccessorTraits<T>::reference_type getNthElement(std::size_t i) const
    {
        return AccessorTraits<T>::getNthElement(v, i);
    }

    // Consider instead:
    typename AccessorTraits<T>::reference_type operator[](std::size_t i) const
    {
        return AccessorTraits<T>::getNthElement(v, i);
    }

private:
    T &v;
};

A few notes:

  • In this case, the implementation would technically be shorter without a traits type; with only specializations of Accessor for each type. However, the traits pattern is a good thing to learn as you now have a way to statically reflect on LunchBox and ClassRoom in other contexts. Decoupling these pieces can be useful.
  • It would be more idiomatic C++ to use operator[] instead of getNthElement for Accessor. Then you can directly index the accessor objects.
  • AccessorTraits really isn't a good name for the traits type, but I'm having trouble coming up with anything better. It's not the traits of the accessors, but the traits of the other two relevant classes -- but what concept even relates those two classes? (Perhaps SchoolRelatedContainerTraits? Seems a bit wordy...)
like image 45
cdhowie Avatar answered Oct 17 '22 09:10

cdhowie


You said:

I would like to do this without writing template specializations for each class

I am not sure why that is a constraint. What is not clear is what else are you not allowed to use.

If you are allowed to use couple of function overloads, you can get what you want.

std::vector<Apple> const& getObjects(LunchBox const& l)
{
   return l.m_apples;
}

std::vector<Student> const& getObjects(ClassRoom const& c)
{
   return c.m_students;
}

You can write generic code that works with both LaunchBox and ClassRoom without writing any other specializations. However, writing function overloads is a form of specialization.


Another option will be to update LaunchBox and ClassRoom with

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

class ClassRoom
{
  public:
    std::vector<Student> m_students;
    using ContainedType = Apple;
};

and then, take advantage of the fact that

LaunchBox b;
std::vector<Apple>* ptr = reinterpret_cast<std::vector<Apple>*>(&b);

is a legal construct. Then, the following class will work fine.

template <typename Container>
struct GetElementFunctor
{
   using ContainedType = typename Container::ContainedType;

   GetElementFunctor(Container const& c) : c_(c) {}

   ContainedType const& getNthElement(std::size_t n) const
   {
      return reinterpret_cast<std::vector<ContainedType> const*>(&c_)->operator[](n);
   }

   Container const& c_;
};

and you can use it as:

LunchBox b;
b.m_apples.push_back({});

auto f = GetElementFunctor<LunchBox>(b);
auto item = f.getNthElement(0);
like image 30
R Sahu Avatar answered Oct 17 '22 10:10

R Sahu


I did a test case sample using a few basic classes:

class Apple {
public:
    std::string color_;
};

class Student {
public:
    std::string name_;
};

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

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

However for the template function that I wrote I did however have to change the name of the containers in each class to match for this to work as this is my template function:

template<class T>
auto accessor(T obj, unsigned idx) {
    return obj.container_[idx];
}

And this is what my main looks like:

int main() {

    LunchBox lunchBox;
    Apple green, red, yellow;
    green.color_  = std::string( "Green" );
    red.color_    = std::string( "Red" );
    yellow.color_ = std::string( "Yellow" );

    lunchBox.container_.push_back(green);
    lunchBox.container_.push_back(red);
    lunchBox.container_.push_back(yellow);


    ClassRoom classRoom;
    Student s1, s2, s3;
    s1.name_ = std::string("John");
    s2.name_ = std::string("Sara");
    s3.name_ = std::string("Mike");

    classRoom.container_.push_back(s1);
    classRoom.container_.push_back(s2);
    classRoom.container_.push_back(s3); 

    for (unsigned u = 0; u < 3; u++) {

        auto somethingUsefull = accessor(lunchBox, u);
        std::cout << somethingUsefull.color_ << std::endl;

        auto somethingElseUsefull = accessor(classRoom, u);
        std::cout << somethingElseUsefull.name_ << std::endl;
    }

    return 0;
}

I'm not sure if there is a work around to have a different variable name from each different class this function can use; but if there is I haven't figured it out as of yet. I can continue to work on this to see if I can improve it; but this is what I have come up with so far.

like image 1
Francis Cugler Avatar answered Oct 17 '22 09:10

Francis Cugler