使用结构体成员指针在C++中填充结构体

3

所以我有以下可用的选项:

struct data_t {
    char field1[10];
    char field2[20];
    char field3[30];
};
const char *getData(const char *key);
const char *field_keys[] = { "key1", "key2", "key3" };

这段代码是给我的,我不能对其进行任何修改。它来自一些旧的C项目。

我需要使用getData函数填充结构体中的不同键,类似于以下内容:

struct data_t my_data;
strncpy(my_data.field1, getData(field_keys[0]), sizeof(my_data.field1));
strncpy(my_data.field1, getData(field_keys[1]), sizeof(my_data.field2));
strncpy(my_data.field1, getData(field_keys[2]), sizeof(my_data.field3));

当然,这只是一种简化方式,每个赋值语句中还有更多的操作。重点是我想用一个常量结构表示键和结构成员之间的映射关系,并使用它来改写循环中的最后一段代码。我需要类似以下代码:

struct data_t {
    char field1[10];
    char field2[20];
    char field3[30];
};

typedef char *(data_t:: *my_struct_member);
const std::vector<std::pair<const char *, my_struct_member>> mapping = {
    { "FIRST_KEY" , &my_struct_t::field1},
    { "SECOND_KEY", &my_struct_t::field2},
    { "THIRD_KEY",  &my_struct_t::field3},
};

int main()
{
    data_t data;

    for (auto const& it : mapping) {
        strcpy(data.*(it.second), getData(it.first));
        // Ideally, I would like to do
        // strlcpy(data.*(it.second), getData(it.first), <the right sizeof here>);
    }
}

然而,这里有两个问题:
  1. 它无法编译 :) 但我相信这应该很容易解决。
  2. 我不确定如何获取 strncpy/strlcpysizeof() 参数,而不是使用 strcpy。我使用 char * 作为成员类型,因此我失去了关于每个数组长度的类型信息。另一方面,我也不确定如何使用每个成员的特定 char[T] 类型,因为如果每个结构体成员指针都有不同的类型,我不认为我能将它们放在 std::vector<T> 中。

2
半相关的是,您不使用动态std::string的原因是因为它们是用于某个固定记录大小存储的固定长度结构吗?如果是这样的话,那就可以理解;只是好奇。 - WhozCraig
1
这是C和C++之间的一些粘合代码,不幸的是,我无法修改这个结构体。 - José D.
1
@Acorn按顺序回答了你的问题:所以我可以使用strncpy / strlcpy来安全地复制,而不会超出结构体的范围。我不认为strcpy是正确的解决方案,因为它可能会超出结构体的布局。我不需要循环遍历成员,但我认为这简化了代码,并避免了重复,所以我想知道如何做到这一点。 - José D.
sizeof(my_data.field1) 应该作为 strncpy() 的参数,而不是 getData() - Barmar
@Acorn "如果映射是固定的,我看不到它的价值。" 这可以避免重复代码。这只是一个简化,但如果结构体有更多的字段,并且工作不像 strlcpy 那么容易呢? - José D.
显示剩余10条评论
5个回答

1
一种基于可变参数模板的解决方案:
struct my_struct_t {
    char one_field[30];
    char another_field[40];
};

template<typename T1, typename T2>
void do_mapping(T1& a, T2& b) {
    std::cout << sizeof(b) << std::endl;
    strncpy(b, a, sizeof(b));
}

template<typename T1, typename T2, typename... Args>
void do_mapping(T1& a, T2& b, Args&... args) {
    do_mapping(a, b);
    do_mapping(args...);
}

int main()
{
    my_struct_t ms;
    do_mapping(
        "FIRST_MAPPING",  ms.one_field, 
        "SECOND_MAPPING", ms.another_field
    );
    return 0;
}

1
如我在评论中所解释的,如果您可以存储足够的信息以处理映射中的字段,则可以编写执行相同操作的函数。
因此,请编写一个函数来实现此功能,使用数组引用来确保您所做的操作是安全的,例如:
template <std::size_t N>
void process_field(char (&dest)[N], const char * src)
{
    strlcpy(dest, getData(src), N);

    // more work with the field...
};

然后,简单地说,取代你的for循环:
process_field(data.field1, "foo");
process_field(data.field2, "bar");
// ...

请注意,行数与映射相同(每个字段一个),因此在重复方面,这不比映射解决方案更差。
现在,优点如下:
- 更易理解。 - 更快:不需要记住映射,更容易优化等。 - 如果需要,可以轻松编写不同字段的不同函数。
此外,如果您的两个字符串在编译时已知,您甚至可以这样做:
template <std::size_t N, std::size_t M>
void process_field(char (&dest)[N], const char (&src)[M])
{
    static_assert(N >= M);
    std::memcpy(dest, src, M);

    // more work with the field...
};

哪些是始终安全的,例如:

process_field(data.field1, "123456789");  // just fits!
process_field(data.field1, "1234567890"); // error

这种方法有更多的优点:

  • 比任何strcpy变体都要快(如果调用是在运行时完成的)。

  • 保证在编译时而不是运行时安全


这很好,但对于原始问题不起作用。请注意,原始问题涉及一个API函数“getData”,该函数在运行时可能返回不同的字符串,因此与在编译时使用字符串文字有所不同。 - José D.
@JoséD。在这种情况下,只需使用单个模板参数即可。我已经更新了解决方案。我认为getData是一个const/pure函数,并且考虑到您传递给它的参数是固定的(它们来自映射),我假设输出将提前知道。 - Acorn

0

映射可以是一个函数,将数据写入适当的成员中

struct mapping_t
{
    const char * name;
    std::function<void(my_struct_t *, const char *)> write;
};

const std::vector<mapping_t> mapping = {
    { "FIRST_KEY",  [](data_t & data, const char * str) { strlcpy(data.field1, str, sizeof(data.field1); } }
    { "SECOND_KEY", [](data_t & data, const char * str) { strlcpy(data.field2, str, sizeof(data.field2); } },
    { "THIRD_KEY",  [](data_t & data, const char * str) { strlcpy(data.field3, str, sizeof(data.field3); } },
};

int main()
{
    data_t data;

    for (auto const& it : mapping) {
        it.write(data, getData(it.name));
    }
}

0

由于data_t是POD结构体,因此您可以使用offsetof()

const std::vector<std::pair<const char *, std::size_t>> mapping = {
    { "FIRST_FIELD" , offsetof(data_t, field1},
    { "SECOND_FIELD", offsetof(data_t, field2)}
};

那么循环将是:

for (auto const& it : mapping) {
    strcpy(static_cast<char*>(&data) + it.second, getData(it.first));
}

我认为没有办法类似地获取成员的大小。您可以从下一个成员中减去当前成员的偏移量,但这将包括填充字节。您还必须特殊处理最后一个成员,从结构体本身的大小中减去偏移量,因为没有下一个成员。


这对我来说感觉像是未定义行为,但是考虑到offsetof的使用是有效的,而且如果不是为了这个目的,offsetof也不会被提供,所以可能只是我自己的问题。 - Lightness Races in Orbit

0

要遍历结构体成员,你需要:

  1. 指向该成员开头的偏移量/指针
  2. 该成员的大小

struct Map {
    const char *key;
    std::size_t offset;
    std::size_t size;
};

std::vector<Map> map = {
    { field_keys[0], offsetof(data_t, field1), sizeof(data_t::field1), },
    { field_keys[1], offsetof(data_t, field2), sizeof(data_t::field2), },
    { field_keys[2], offsetof(data_t, field3), sizeof(data_t::field3), },
};

一旦我们有了那个,我们就需要 strlcpy

std::size_t mystrlcpy(char *to, const char *from, std::size_t max)
{

    char * const to0 = to;
    if (max == 0) 
        return 0;
    while (--max != 0 && *from) {
        *to++ = *from++;
    }
    *to = '\0';
    return to0 - to - 1;
}

有了那个,我们可以这样做:

data_t data;

for (auto const& it : map) {
    mystrlcpy(reinterpret_cast<char*>(&data) + it.offset, getData(it.key), it.size);
}

reinterpret_cast 看起来有点丑陋,但它只是将 &data 指针移动到所需的字段。

我们还可以创建一个更智能的容器,在构造时接受变量指针,因此与现有变量绑定,并且需要写入一些代码:

struct Map2 {
    static constexpr std::size_t max = sizeof(field_keys)/sizeof(*field_keys);

    Map2(data_t* pnt) : mpnt(pnt) {}

    char* getDest(std::size_t num) {
        std::array<char*, max> arr = {
            mpnt->field1,
            mpnt->field2,
            mpnt->field3,
        };
        return arr[num];
    }

    const char* getKey(std::size_t num) {
        return field_keys[num];
    }

    std::size_t getSize(std::size_t num) {
        std::array<std::size_t, max> arr = {
            sizeof(mpnt->field1),
            sizeof(mpnt->field2),
            sizeof(mpnt->field3),
        };
        return arr[num];
    }

private:
    data_t* mpnt;
};

但这样做可能会使迭代更易读:

Map2 m(&data);
for (std::size_t i = 0; i < m.max; ++i) {
    mystrlcpy(m.getDest(i), getData(m.getKey(i)), m.getSize(i));
}

可以在onlinegdb上找到实时代码。


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