【问题标题】:Can templates be used to access struct variables by name?可以使用模板按名称访问结构变量吗?
【发布时间】:2009-03-23 10:20:37
【问题描述】:

假设我有一个这样的结构:

struct my_struct
{
  int a;
  int b; 
}

我有一个函数应该为“a”或“b”设置一个新值。此功能还需要指定要设置的变量。一个典型的例子是这样的:

void f(int which, my_struct* s, int new_value)
{
  if(which == 0)
     s->a = new_value;
  else
     s->b = new_value; 
}

由于我不会在这里写的原因,我不能将指向 a/b 的指针传递给 f。所以我不能用 my_struct::a 或 my_struct::b 的地址调用 f。 我不能做的另一件事是在 my_struct 中声明一个向量 (int vars[2]) 并将一个整数作为索引传递给 f。基本上在 f 中我需要按名称访问变量。

上一个例子的问题是,将来我计划向 struct 添加更多变量,在这种情况下,我会记得向 f 添加更多 if 语句,这不利于可移植性。 我可以做的就是把 f 写成一个宏,像这样:

#define FUNC(which)
void f(my_struct* s, int new_value) \
{ \
        s->which = new_value; \
} 

然后我可以调用 FUNC(a) 或 FUNC(b)。

这可行,但我不喜欢使用宏。 所以我的问题是:有没有办法使用模板而不是宏来实现相同的目标?

编辑:我将尝试解释为什么我不能使用指针并且我需要按名称访问变量。 基本上,结构包含系统的状态。该系统需要在请求时“撤消”其状态。撤消是使用一个名为 undo_token 的接口来处理的,如下所示:

class undo_token
{
public:
   void undo(my_struct* s) = 0;
};

因此,由于多态性,我无法将指针传递给 undo 方法(mystruct 也包含其他类型的变量)。

当我向结构中添加一个新变量时,我通常也会添加一个新类,如下所示:

class undo_a : public undo_token
{
  int new_value;
public:
  undo_a(int new_value) { this->new_value = new_value; }
  void undo(my_struct *s) { s->a = new_value}
};

问题是我在创建令牌时不知道指向 s 的指针,因此我无法在构造函数中保存指向 s::a 的指针(这本来可以解决问题)。 “b”的类是一样的,只是我必须写“s->b”而不是s->a

也许这是一个设计问题:我需要每个变量类型一个撤消标记,而不是每个变量一个...

【问题讨论】:

  • FUNC(a) - 将定义 f(s, new_value)。但是那你怎么称呼它呢?
  • 是的,抱歉,这是一个错误。但我还是希望你明白这一点:)
  • 即使在您编辑之后,使用我的答案和 Mykola Golubyev 的答案,您仍然可以使用指向类数据成员的指针参数化您的撤消类,并在成员调用时将其绑定到类/结构。
  • 我已经更新了我的答案。是你需要的吗?

标签: c++ templates


【解决方案1】:

要回答确切的问题,有,但它相当复杂,而且纯粹是编译时的事情。 (如果您需要运行时查找,请使用指向成员的指针 - 根据您更新的问题,您可能误解了它们的工作原理。)

首先,您需要一些可以在编译时用来表示“成员名称”的东西。在编译时元编程中,除整数之外的所有内容都必须由类型表示。所以你将使用一个类型来代表一个成员。

例如,一个整数类型的成员存储一个人的年龄,另一个用于存储他们的姓氏:

struct age { typedef int value_type; };
struct last_name { typedef std::string value_type; };

然后你需要像map 这样在编译时进行查找的东西。我们称之为ctmap。让我们最多支持 8 名成员。首先我们需要一个占位符来表示字段的缺失:

struct none { struct value_type {}; };

然后我们可以前向声明ctmap的形状:

template <
    class T0 = none, class T1 = none,
    class T2 = none, class T3 = none,
    class T4 = none, class T5 = none,
    class T6 = none, class T7 = none
    >
struct ctmap;

然后我们专门针对没有字段的情况:

template <>
struct ctmap<
    none, none, none, none,
    none, none, none, none
    >
{
    void operator[](const int &) {};
};

稍后(可能)会清楚原因。最后,所有其他情况的定义:

template <
    class T0, class T1, class T2, class T3,
    class T4, class T5, class T6, class T7
    >
    struct ctmap : public ctmap<T1, T2, T3, T4, T5, T6, T7, none>
    {
        typedef ctmap<T1, T2, T3, T4, T5, T6, T7, none> base_type;

        using base_type::operator[];
        typename T0::value_type storage;

        typename T0::value_type &operator[](const T0 &c)
        { return storage; }
};

这到底是怎么回事?如果你说:

ctmap<last_name, age> person;

C++ 将通过递归扩展模板为person 构建一个类型,因为ctmap 从自身继承,我们为第一个字段提供存储,然后在继承时将其丢弃。当没有更多字段时,这一切都突然停止,因为 all-none 的专业化开始了。

所以我们可以说:

person[last_name()] = "Smith";
person[age()] = 104;

这就像在 map 中查找,但在编译时,使用字段命名类作为键。

这意味着我们也可以这样做:

template <class TMember>
void print_member(ctmap<last_name, age> &person)
{
    std::cout << person[TMember()] << std::endl;
}

这是一个打印成员值的函数,其中要打印的成员是类型参数。所以我们可以这样称呼它:

print_member<age>(person);

所以是的,你可以写一个有点像struct,有点像编译时map的东西。

【讨论】:

  • 不完全是世界上最干净的东西,但仍然很聪明。 +1 只是为了教我一些新东西。
  • 一个非常完整的答案,但对于我需要的东西来说,这有点矫枉过正。正如 Dan 所说,无论如何,你因为教了我一些新东西而获得了 +1。
  • 谢谢。我希望有人偶然发现它并发现它很有用 - 问题的标题似乎很可能是这样的!
  • +1 用于阅读和实际理解 Abrahams/Gurtovoy “C++ 模板元编程”。
  • Abrahams 是老兄——我曾经提交了一个对 boost-parameter 的小改进,他向我解释说它需要修复,所以它在编译时以 O(n) 运行。当您刚刚习惯模板元编程的想法时,发现专家们在睡梦中做这件事时,这有点奇怪。
【解决方案2】:
#include <iostream>
#include <ostream>
#include <string>

struct my_struct
{
    int a;
    std::string b;
};

template <typename TObject, typename TMember, typename TValue>
void set( TObject* object, TMember member, TValue value )
{
    ( *object ).*member = value;
}

class undo_token {};

template <class TValue>
class undo_member : public undo_token
{
    TValue new_value_;
    typedef TValue my_struct::* TMember;
    TMember member_;

public:
    undo_member(TMember member, TValue new_value):
        new_value_( new_value ),
        member_( member )
    {}

    void undo(my_struct *s) 
    { 
        set( s, member_, new_value_ );
    }
};    

int main()
{
    my_struct s;

    set( &s, &my_struct::a, 2 );
    set( &s, &my_struct::b, "hello" );

    std::cout << "s.a = " << s.a << std::endl;
    std::cout << "s.b = " << s.b << std::endl;

    undo_member<int> um1( &my_struct::a, 4 );
    um1.undo( &s );

    std::cout << "s.a = " << s.a << std::endl;

    undo_member<std::string> um2( &my_struct::b, "goodbye" );
    um2.undo( &s );

    std::cout << "s.b = " << s.b << std::endl;

    return 0;
}

【讨论】:

  • 你读过他说他不能获取 a 或 b 的地址吗?
  • 他不是说他不能将 &s.a 之类的东西传递给函数吗?
  • 无法抗拒添加模板参数,以便 undo_member 可以应用于字符串字段以及 int。如果不请自来的编辑惹恼了您,请随时回滚! :)
  • +1。事实上,您可以完全避免存储 member_,因为指向成员的指针可以用作非类型模板参数——请参阅我的答案。
  • @Earwicker:当然,这段代码可以推广到所有可以通过一个赋值进行撤消的情况。
【解决方案3】:

除了Daniel Earwicker's answer,我们还可以使用新C++标准中的可变参数模板来实现。

template <typename T>
struct Field {
  typename T::value_type storage;

  typename T::value_type &operator[](const T &c) {
    return storage;
  }
};

template<typename... Fields>
struct ctmap : public Field<Fields>... {
};

这段代码更简洁,没有固定的成员绑定。你可以用同样的方式使用它

struct age { typedef int value_type; };
struct last_name { typedef std::string value_type; };

ctmap<last_name, age> person;

person[last_name()] = "Smith";
person[age()] = 104;

【讨论】:

【解决方案4】:

Mykola Golubyev's answer 不错,但可以通过使用指向成员的指针可以用作非类型模板参数的事实稍微改进一下:

#include <iostream>
#include <ostream>
#include <string>

struct my_struct
{
    int a;
    std::string b;
};

template <typename TObject, typename TMember, typename TValue>
void set( TObject* object, TMember member, TValue value )
{
    ( *object ).*member = value;
}

class undo_token {};

template <class TValue, TValue my_struct::* Member>
class undo_member : public undo_token
{
        // No longer need to store the pointer-to-member
        TValue new_value_;

public:
        undo_member(TValue new_value):
                new_value_(new_value)
        {}

        void undo(my_struct *s) 
        { 
                set( s, Member, new_value_ );
        }
};    

int main()
{
    my_struct s;

    set( &s, &my_struct::a, 2 );
    set( &s, &my_struct::b, "hello" );

    std::cout << "s.a = " << s.a << std::endl;
    std::cout << "s.b = " << s.b << std::endl;

    undo_member<int, &my_struct::a> um1( 4 );
    um1.undo( &s );

    std::cout << "s.a = " << s.a << std::endl;

    undo_member<std::string, &my_struct::b> um2( "goodbye" );
    um2.undo( &s );

    std::cout << "s.b = " << s.b << std::endl;

    return 0;
}

这减少了每个 undo_member 实例中指向成员的指针的开销。

【讨论】:

  • +1。没见过这个。但这会为每个结构成员创建很多类。
  • @Mykola:确实如此,但有问题吗?这些方法都会被编译器内联,所以我认为不会有任何代码膨胀。
  • 它们不会被内联,因为在原始问题中,撤消方法是虚拟的(嗯,本意是 - 基类中的 = 0 放弃了这一点,但虚拟关键字看起来像不小心离开了)
  • @camh:如果编译器可以确定表达式的动态类型,它将直接调用方法,而不使用虚拟调度(甚至可能内联它们)。这是这种情况,因为 um1 和 um2 变量是完整的 undo_member 类型。
  • 编译器无法确定动态类型的一个例子是 um1 或 um2 是否被声明为基类 undo_token 的指针/引用。在这种情况下,使用虚拟调度。
【解决方案5】:

我不确定为什么不能使用指针,所以我不知道这是否合适,但请查看 C++: Pointer to class data member,它描述了一种将指针传递给结构数据成员的方法/class 不直接指向成员,但后来绑定到结构/类指针。 (在海报编辑后添加的重点解释了为什么不能使用指针)

这样您就不会将指针传递给成员 - 相反,它更像是对象内的偏移量。

【讨论】:

    【解决方案6】:

    听起来您正在寻找的内容称为“reflection”,是的,它通常是通过模板和宏的某种组合来实现的。请注意,反射解决方案通常很混乱且使用起来很烦人,因此您可能需要在深入研究代码之前对它们进行一些研究,以确定这是否真的是你想要的。 p>

    “C++ 反射模板”在 Google 上的第二次点击是一篇关于“Reflection support by means of template metaprogramming”的论文。这应该让你开始。即使它不是您想要的,它也可能会为您提供解决问题的方法。

    【讨论】:

      【解决方案7】:

      你不能使用模板来解决这个问题,但是为什么要首先使用结构呢?这似乎是 std::map 的理想用途,它将名称映射到值。

      【讨论】:

      • 因为“f”只是对结构进行辅助处理所需的一个函数。我不希望结构形状被绑定到“f”需要的东西。
      【解决方案8】:

      根据您的描述,我猜您无法重新定义结构。

      如果你这样做了,我建议你使用 Boost.Fusion 来用模板命名字段来描述你的结构。有关更多信息,请参阅associative tuples。这两种结构实际上可能是兼容的(内存中的组织相同),但我很确定没有办法从标准中获得这样的保证。

      如果您不这样做,您可以为结构创建一个补充,使您可以像关联元组一样访问字段。但这可能有点口头上的。

      编辑

      现在很明显,您可以按照自己的方式定义结构。所以我绝对建议你使用 boost.fusion。

      【讨论】:

        【解决方案9】:

        我想不出为什么在创建撤消命令时手头没有所有东西的原因。您希望能够撤消的,您已经完成了。因此,我相信您可以在创建撤消命令时使用指向类成员的指针,甚至可以使用指向特定类实例字段的指针。

        您的编辑部分是正确的。这设计问题。

        【讨论】:

        • 因为代码中有一些点系统状态是由“检查点”定义的。这是实现删除当前状态并创建一个新状态。确实在 90% 的情况下指针是相同的,但情况并非总是如此。
        • 如果您删除一个状态对象并创建一个新状态对象,为什么不找到一种方法不删除以前的状态,而是禁用它,这样,如果您撤消删除,您就是再次使用同一个对象?我可能不清楚,对不起,但我不确定我理解得很好!
        • 好吧,我想如果我可以断言系统状态指针始终有效,那么我的问题就没有意义了。不过,我不确定修改现有代码以使其适合“支持”一个是个好主意。另请参阅我对 Neil Butterworth 回答的评论。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2018-09-26
        • 1970-01-01
        • 2012-07-27
        • 2012-10-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多