成员变量模板化

6

Consider the following two classes:

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

并且

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

这两个类相似之处在于它们都包含一个对象的向量成员变量;然而,它们不同之处在于向量的对象不同且成员变量具有不同的名称。

我想编写一个模板,将LunchBoxClassRoom作为模板参数(或其他参数),以及一个现有的相同类型的对象(类似于std::shared_ptr)。该模板将返回一个对象,添加了一个getNthElement(int i);成员函数以改善访问方法。使用方法如下:

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

我希望能够做到这一点,而不需要为每个类编写模板特化(这可能需要以某种方式指定要操作的成员变量)。最好不要修改LunchBoxClassRoom类。是否可能编写这样的模板?

4个回答

5

您可以将每个类所需编写的代码量最小化 -- 它不必是模板专业化,也不必是整个类。

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);

理想情况下,简单的辅助函数应该与类型位于同一命名空间中,以使参数相关查找生效(也称为Koenig查找)。

如果您更喜欢这样做,也可以在构造点指定成员:

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);

3
一种简单的方法是定义一个特性结构体,其中包含每种情况不同的信息。然后您可以使用此特性类型的模板类:
// 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;
};

一些注释:

  • 在这种情况下,如果只使用每个类型的Accessor专业化,实现技术上可能会更短,而没有特征类型。然而,学习特征模式是一件好事,因为您现在有一种方法在其他上下文中静态反映LunchBoxClassRoom。解耦这些部分可能很有用。
  • 对于Accessor,使用operator[]而不是getNthElement会更符合C++的惯例。然后,您可以直接索引访问器对象。
  • AccessorTraits并不是特征类型的好名称,但我很难想出更好的名称。它不是访问者的特性,而是另外两个相关类的特性--但是哪个概念甚至涉及这两个类?(也许SchoolRelatedContainerTraits?看起来有点啰嗦...)

如果可能的话,我更愿意避免使用模板特化。难道没有其他方法吗?感谢您的回答! - chessofnerd
@chessofnerd,除非您想直接在“LunchBox”或“ClassRoom”上实现访问器方法,否则不行。必须有某种方式查找给定“T”的访问器。 - cdhowie
这正是我担心的。我希望你能做类似于 auto accessorizedLunchBox = Accessor<LunchBox.m_apples>(lunchBox) 这样指定要操作的成员变量。是否支持这种语法?如果有的话,我会感到惊讶。 - chessofnerd
1
我的意思是,有一些方法可以做到那样的事情,但那不是你问题中所问的。最简单的形式是一个直接的lambda表达式。auto alb = [&lunchBox] (std::size_t i) { return lunchBox.m_apples[i]; }; - cdhowie
你不需要为此使用模板特化,函数重载更简单且相当有效。 - Ben Voigt

2

您说:

我希望不用为每个类编写模板专用化来做到这一点

我不确定为什么有这样的限制。不清楚的是,您还有什么其他禁止使用。

如果您允许使用几个函数重载,您可以得到想要的结果。

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

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

您可以编写通用代码,可以同时适用于LaunchBoxClassRoom,而无需编写其他特殊内容。但是,编写函数重载是一种特化形式。


另一个选项是更新LaunchBoxClassRoom

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

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

然后,利用这个事实的优势。
LaunchBox b;
std::vector<Apple>* ptr = reinterpret_cast<std::vector<Apple>*>(&b);

是一个合法的结构。然后,以下类将正常工作。

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_;
};

你可以这样使用:

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

auto f = GetElementFunctor<LunchBox>(b);
auto item = f.getNthElement(0);

1
我使用了几个基础类来进行测试用例样本:
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_;
};

但是,对于我编写的模板函数,我必须更改每个类中容器的名称以使其起作用。以下是我的模板函数:

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

这是我的主函数的样子:

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;
}

我不确定是否有一种方法可以绕过这个函数可以使用的每个不同类别的不同变量名称;但是,如果有的话,我目前还没有想出来。我可以继续努力改进它;但是,这就是我目前想出来的。


1
感谢!请参考@Ben Voight的答案,他提供了一种聪明的解决方法。 - chessofnerd
1
好的,这就是我错过的Ben Voight明确指出的内容:“简单的帮助函数重载应该理想地与类型在同一个命名空间中,以使参数相关查找起作用(又称Koenig查找)。” - Francis Cugler

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接