Use struct member pointer to fill-in a struct in C++

971 Views Asked by At

So I have the following available:

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

This code is given to my and I cannot modify it in any way. It comes from some old C project.

I need to fill in the struct using the getData function with the different keys, something like the following:

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

Of course, this is a simplification, and more things are going on in each assignment. The point is that I would like to represent the mapping between keys and struct member in a constant structure, and use that to transform the last code in a loop. I am looking for something like the following:

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

This, however, has two problems:

  1. It does not compile :) But I believe that should be easy to solve.
  2. I am not sure about how to get the sizeof() argument for using strncpy/strlcpy, instead of strcpy. I am using char * as the type of the members, so I am losing the type information about how long each array is. In the other hand, I am not sure how to use the specific char[T] types of each member, because if each struct member pointer has a different type I don't think I will be able to have them in a std::vector<T>.
5

There are 5 best solutions below

2
On BEST ANSWER

As explained in my comment, if you can store enough information to process a field in a mapping, then you can write a function that does the same.

Therefore, write a function to do so, using array references to ensure what you do is safe, e.g.:

template <std::size_t N>
void process_field(char (&dest)[N], const char * src)
{
    strlcpy(dest, getData(src), N);

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

And then simply, instead of your for loop:

process_field(data.field1, "foo");
process_field(data.field2, "bar");
// ...

Note that the amount of lines is the same as with a mapping (one per field), so this is not worse than a mapping solution in terms of repetition.

Now, the advantages:

  • Easier to understand.

  • Faster: no memory needed to keep the mapping, more easily optimizable, etc.

  • Allows you to write different functions for different fields, easily, if needed.


Further, if both of your strings are known at compile-time, you can even do:

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

Which will be always safe, e.g.:

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

Which has even more pros:

  • Way faster than any strcpy variant (if the call is done in run-time).

  • Guaranteed to be safe at compile-time instead of run-time.

0
On

To iterate over struct member you need:

  1. offset / pointer to the beginning of that member
  2. size of that member

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

once we have that we need 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;
}

After having that, we can just:

data_t data;

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

That reinterpret_cast looks a bit ugly, but it just shift &data pointer to the needed field.

We can also create a smarter container which takes variable pointer on construction, thus is bind with an existing variable and it needs a little bit of writing:

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

But probably makes the iterating more readable:

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

Live code available at onlinegdb.

0
On

A variadic templates based solution:

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

The mapping can be a function to write the data into the appropriate member

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));
    }
}
1
On

Since data_t is a POD structure, you can use offsetof() for this.

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

Then the loop would be:

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

I don't think there's any way to get the size of the member similarly. You can subtract the offset of the current member from the next member, but this will include padding bytes. You'd also have to special-case the last member, subtracting the offset from the size of the structure itself, since there's no next member.