Search code examples
c++arraysdata-member-pointers

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


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

Solution

  • 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.