【问题标题】:Accessing struct members with array subscript operator使用数组下标运算符访问结构成员
【发布时间】:2010-07-05 09:17:40
【问题描述】:

让我们有一个类型 T 和一个只有 T 类型的统一元素的结构。

struct Foo {
    T one,
    T two,
    T three
};

我想以休闲方式访问它们:

struct Foo {
    T one,
    T two,
    T three

    T &operator [] (int i)
    {
        return *(T*)((size_t)this + i * cpp_offsetof(Foo, two));
    }
};

其中cpp_offsetof宏(被认为是正确的)是:

#define cpp_offsetof(s, m)   (((size_t)&reinterpret_cast<const volatile char&>((((s*)(char*)8)->m))) - 8)

C++ 标准不保证这一点,但我们可以假设成员之间的距离为固定偏移量及以上是正确的跨平台解决方案吗?


100% 兼容的解决方案是:

struct Foo {
    T one,
    T two,
    T three

    T &operator [] (int i) {
        const size_t offsets[] = { cpp_offsetof(Foo, one), cpp_offsetof(Foo, two), cpp_offsetof(Foo, three) };
        return *(T*)((size_t)this + offsets[i]);
    }
};

[edit]标准、合规和更快的版本由 snk_kid using pointers to data members[/edit]
但它需要额外的查找表,我试图避免。

//编辑
还有一个。我不能只使用数组和常量来索引这些字段,它们必须被命名为结构的字段(某些宏需要这样做)。

//EDIT2
为什么这些必须命名为结构的字段?什么是宏?它是一个更大项目的设置系统。简化它是这样的:

struct Foo {
    int one;
    int two;
}
foo;

struct Setting { void *obj, size_t filed_offset, const char *name, FieldType type }

#define SETTING(CLASS, OBJ, FIELD, TYPE) { OBJ, cpp_offsetof(CLASS, FIELD), #OBJ #FIELD, TYPE }

Setting settings[] = {
    SETTING(Foo, foo, one, INT_FIELD),
    SETTING(Foo, foo, two, INT_FIELD)
};

再说一次:我不是在寻找 100% 兼容的解决方案,而是 99%。我在问我们是否可以预期某些编译器会在统一字段之间放置非统一填充。

【问题讨论】:

  • 与 OP 无关,但有谁知道为什么那个 offsetof 宏使用 8 作为基础? MSVC 使用 0,GCC 使用内置函数,甚至在非零基数上存在 ICE 错误。
  • 即使我有答案,我也会避开这个问题,看看一切都被否决了......:S(开玩笑,自然)
  • 不,我自己在检查。阅读我的评论。现在我取消了我的下注。如果你不能证明我错了,就会被否决。为了清楚起见,我写道我已经知道使用表格查找的解决方案,这就是我投反对票的原因。
  • 为什么使用int 作为索引类型?您是否期望负索引值?
  • 上次我们在工作中遇到这个问题,我们只是写了一个Perl脚本来解析出结构字段名称并生成一堆打印字段名称的printfs和offsetof每个字段。我们没有奢侈的所有字段都是相同类型或对齐的。如今,我们最多只使用std::map&lt;std::string, T&gt;,然后继续我们的生活。

标签: c++ cross-platform memory-alignment


【解决方案1】:

您的代码不适用于非 POD 类型,例如使用虚拟成员函数的类型。使用指向数据成员的指针,有一种符合标准(且高效)的方式来实现您想要做的事情:

template< typename T >
struct Foo {

    typedef size_t size_type;

private:

    typedef T Foo<T>::* const vec[3];

    static const vec v;

public:

    T one;
    T two;
    T three;

    const T& operator[](size_type i) const {
        return this->*v[i];
    }

    T& operator[](size_type i) {
        return this->*v[i];
    }
};

template< typename T >
const typename Foo<T>::vec Foo<T>::v = { &Foo<T>::one, &Foo<T>::two, &Foo<T>::three };

只要确保你使用 const every 和指向数据成员的指针表来获得优化。检查here 看看我在说什么。

【讨论】:

  • 为什么我给出了一个符合标准的解决方案,但我却被低估了?
  • @adf88: snk_kid 写的和你的问题一样吗?
  • @adf88 你到底在说什么?
  • @adf88:WTF,它甚至与您在问题中所做的完全不同。
  • @adf88:它并没有摆脱查找表——它使查找表语言兼容(因此它可以在所有编译器、平台、架构、优化等下工作)。对于您的实施,情况并非如此。
【解决方案2】:

如果您尝试实现的目标仍然是编译时功能,则另一种方法是使用模板专业化。

class Foo {
    T one;
    T two;
    T three; 
};

template <int i> T & get(Foo& foo);

template T& get<1>(Foo& foo){ return foo.one;}
template T& get<2>(Foo& foo){ return foo.two;}
template T& get<3>(Foo& foo){ return foo.three;}

将 get 定义为成员函数会很好,但你不能 专门模板成员函数。现在,如果这只是一个编译时间 您正在寻找的扩展,那么这将避免查找表 以前的帖子之一的问题。如果您需要运行时解析 那么你显然需要一个查找表。

-- 布拉德·费兰 http://xtargets.heroku.com

【讨论】:

  • 布拉德,我进去并让代码可编译,我跳没关系。如果我搞砸了,请随时回滚。
  • 另外,class Foo { template&lt;int i&gt; get() {return ::get&lt;i&gt;(*this);} }; 应该做成员函数的事情。
  • 你编辑没问题。如果没有编译器为您进行拼写检查,总是很难写出正确的 c++!
【解决方案3】:

您也许可以使用数组来保存数据(因此您可以在不使用查找表的情况下获得索引访问)并引用各种数组元素(因此您可以为由您的宏使用)。

我不确定您的宏需要什么,所以我不能 100% 确定这会起作用,但它可能会起作用。另外,我不确定查找表方法的轻微开销是否值得跳过太多的圈子来避免。另一方面,我不认为我在这里建议的方法比指针表方法更复杂,所以这里供您考虑:

#include <stdio.h>

template< typename T >
struct Foo {

private:    
    T data_[3];

public:

    T& one;
    T& two;
    T& three;

    const T& operator[](size_t i) const {
        return data_[i];
    }

    T& operator[](size_t i) {
        return data_[i];
    }

    Foo() :
        one( data_[0]),
        two( data_[1]),
        three( data_[2])
        {};

};


int main()
{
    Foo<int> foo;

    foo[0] = 11;
    foo[1] = 22;
    foo[2] = 33;

    printf( "%d, %d, %d\n", foo.one, foo.two, foo.three);

    Foo<int> const cfoo( foo);

    printf( "%d, %d, %d\n", cfoo[0], cfoo[1], cfoo[2]);

    return 0;
}

【讨论】:

    【解决方案4】:

    你不能因为编译器可以在成员之间添加死字节以允许填充。

    有两种方法可以做你想做的事。

    第一个是使用编译器特定的关键字或编译指示宏,这将强制编译器不添加填充字节。但这不是便携式的。 也就是说,这可能是满足您的宏需求的最简单方法,因此我建议您探索这种可能性并准备在使用不同编译器时添加更多 pragma。

    另一种方法是首先确保您的成员对齐,然后添加访问器:

    struct Foo {
    
       T members[ 3 ]; // arrays are guarrantied to be contigu
    
    
       T& one() { return members[0]; } 
       const T& one() const { return members[0]; } 
       //etc... 
    
    };
    

    【讨论】:

    • 首先我问我们是否可以假设填充(如果添加)将为每个字段固定。其次,我要求跨平台解决方案。第三,必须有命名字段,而不是方法。
    • 那么在 C++ AFAIK 中是不可能的
    • 您的最后一个要求会扼杀任何其他可能性。也许你可以添加更多的宏来让你的类与你的宏一起工作,但这确实是一种糟糕的风格和邪恶。
    • 在 C++ 中编写高效的代码就像知道目标编译器所承诺的内容一样多,当然,标准 C++ 所承诺的内容......
    • 克莱姆是正确的。你的要求有点糟糕——难怪你不能得到一个跨平台的解决方案。
    【解决方案5】:

    如果您确定您使用的编译器会为此生成正确的代码(我想他们会,假设 T 无论如何都不是引用类型)最好的办法是放入某种检查结构是否按照您的想法布局。我想不出在相同类型的相邻成员之间插入非均匀填充的任何特殊原因,但是如果您手动检查结构布局,那么您至少会知道它是否发生。

    如果结构 (S) 恰好有 N 个类型为 T 的成员,例如,您可以在编译时检查它们是否紧密打包,只需使用 sizeof

    struct S {
        T a,b,c;
    };
    
    extern const char check_S_size[sizeof(S)==3*sizeof(T)?1:-1];
    

    如果编译成功,那么它们会被紧紧地打包,因为没有其他空间了。

    如果你碰巧有 N 个成员,你想确保一个接一个地直接放置,你可以使用 offsetof 做类似的事情:

    class S {
        char x;
        T a,b,c;
    };
    
    extern const char check_b_offset[offsetof(S,b)==offsetof(S,a)+sizeof(T)?1:-1];
    extern const char check_c_offset[offsetof(S,c)==offsetof(S,b)+sizeof(T)?1:-1];
    

    根据编译器的不同,这可能必须成为运行时检查,可能不使用offsetof——无论如何您可能希望对非 POD 类型执行此操作,因为没有为它们定义 offsetof

    S tmp;
    assert(&tmp.b==&tmp.a+1);
    assert(&tmp.c==&tmp.b+1);
    

    这并没有说明如果断言开始失败该怎么办,但您至少应该得到一些警告,说明假设不正确......

    (顺便说一下,在适当的地方插入适当的转换到 char 引用等等。为简洁起见,我将它们省略了。)

    【讨论】:

      猜你喜欢
      • 2013-02-02
      • 2017-06-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-02-05
      • 1970-01-01
      • 2023-03-16
      相关资源
      最近更新 更多